Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-points-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@srcbook/web': patch
---

- Add essential support for OpenRouter models.
5 changes: 5 additions & 0 deletions .changeset/thin-ears-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@srcbook/web': patch
---

Auto-populate available models in Settings.
5 changes: 5 additions & 0 deletions .changeset/wicked-geckos-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'srcbook': patch
---

Add GitHub Pages documentation repository structure and configuration.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ srcbook/lib/**/*
# Aide
*.code-workspace

# Docs folder
docs/

vite.config.ts.timestamp-*.mjs
11 changes: 11 additions & 0 deletions packages/api/ai/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export async function getModel(): Promise<LanguageModel> {
});
return xai(model);

case 'openrouter':
if (!config.openrouterKey) {
throw new Error('OpenRouter API key is not set');
}
const openrouter = createOpenAI({
compatibility: 'compatible',
baseURL: 'https://openrouter.ai/api/v1',
apiKey: config.openrouterKey,
});
return openrouter(model);

case 'custom':
if (typeof aiBaseUrl !== 'string') {
throw new Error('Local AI base URL is not set');
Expand Down
1 change: 1 addition & 0 deletions packages/api/db/schema.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const configs = sqliteTable('config', {
anthropicKey: text('anthropic_api_key'),
xaiKey: text('xai_api_key'),
geminiKey: text('gemini_api_key'),
openrouterKey: text('openrouter_api_key'),
customApiKey: text('custom_api_key'),
// TODO: This is deprecated in favor of SRCBOOK_DISABLE_ANALYTICS env variable. Remove this.
enabledAnalytics: integer('enabled_analytics', { mode: 'boolean' }).notNull().default(true),
Expand Down
1 change: 1 addition & 0 deletions packages/api/drizzle/0016_add_openrouter_api_key.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `config` ADD `openrouter_api_key` text;
2 changes: 2 additions & 0 deletions packages/shared/src/ai.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const AiProvider = {
Anthropic: 'anthropic',
XAI: 'Xai',
Gemini: 'Gemini',
OpenRouter: 'openrouter',
Custom: 'custom',
} as const;

Expand All @@ -14,6 +15,7 @@ export const defaultModels: Record<AiProviderType, string> = {
[AiProvider.Custom]: 'mistral-nemo',
[AiProvider.XAI]: 'grok-beta',
[AiProvider.Gemini]: 'gemini-1.5-pro-latest',
[AiProvider.OpenRouter]: 'anthropic/claude-3-opus-20240229',
} as const;

export function isValidProvider(provider: string): provider is AiProviderType {
Expand Down
64 changes: 63 additions & 1 deletion packages/web/src/components/use-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import React, { createContext, useContext } from 'react';
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useRevalidator } from 'react-router-dom';
import { updateConfig as updateConfigServer } from '@/lib/server';
import type { SettingsType } from '@/types';

export type OpenRouterModel = {
id: string;
name: string;
provider: string;
description?: string;
pricing?: Record<string, number>;
context_length?: number;
};

export type GroupedOpenRouterModels = Record<string, OpenRouterModel[]>;

export type SettingsContextValue = SettingsType & {
aiEnabled: boolean;
updateConfig: (newConfig: Partial<SettingsType>) => Promise<void>;
openRouterModels: GroupedOpenRouterModels;
isLoadingOpenRouterModels: boolean;
refreshOpenRouterModels: () => Promise<void>;
};

const SettingsContext = createContext<SettingsContextValue | null>(null);
Expand All @@ -15,11 +29,38 @@ type ProviderPropsType = {
children: React.ReactNode;
};

async function fetchOpenRouterModels(): Promise<OpenRouterModel[]> {
try {
const response = await fetch('https://openrouter.ai/api/v1/models');
if (!response.ok) {
throw new Error('Failed to fetch models');
}
const data = await response.json();
return data.data || [];
} catch (error) {
console.error('Error fetching OpenRouter models:', error);
return [];
}
}

function groupModelsByProvider(models: OpenRouterModel[]): GroupedOpenRouterModels {
return models.reduce((grouped, model) => {
const provider = model.provider || 'Unknown';
if (!grouped[provider]) {
grouped[provider] = [];
}
grouped[provider].push(model);
return grouped;
}, {} as GroupedOpenRouterModels);
}

/**
* An interface for working with our config.
*/
export function SettingsProvider({ config, children }: ProviderPropsType) {
const revalidator = useRevalidator();
const [openRouterModels, setOpenRouterModels] = useState<GroupedOpenRouterModels>({});
const [isLoadingOpenRouterModels, setIsLoadingOpenRouterModels] = useState(false);

const updateConfig = async (newConfig: Partial<SettingsType>) => {
// Filter out null values and convert back to an object
Expand All @@ -31,18 +72,39 @@ export function SettingsProvider({ config, children }: ProviderPropsType) {
revalidator.revalidate();
};

const refreshOpenRouterModels = async () => {
setIsLoadingOpenRouterModels(true);
try {
const models = await fetchOpenRouterModels();
const grouped = groupModelsByProvider(models);
setOpenRouterModels(grouped);
} finally {
setIsLoadingOpenRouterModels(false);
}
};

useEffect(() => {
if (config.aiProvider === 'openrouter') {
refreshOpenRouterModels();
}
}, [config.aiProvider]);

const aiEnabled =
(config.openaiKey && config.aiProvider === 'openai') ||
(config.anthropicKey && config.aiProvider === 'anthropic') ||
(config.xaiKey && config.aiProvider === 'Xai') ||
(config.geminiKey && config.aiProvider === 'Gemini') ||
(config.openrouterKey && config.aiProvider === 'openrouter') ||
(config.aiProvider === 'custom' && !!config.aiBaseUrl) ||
false;

const context: SettingsContextValue = {
...config,
aiEnabled,
updateConfig,
openRouterModels,
isLoadingOpenRouterModels,
refreshOpenRouterModels,
};

return <SettingsContext.Provider value={context}>{children}</SettingsContext.Provider>;
Expand Down
Loading