diff --git a/.changeset/tall-points-hang.md b/.changeset/tall-points-hang.md new file mode 100644 index 00000000..46d57104 --- /dev/null +++ b/.changeset/tall-points-hang.md @@ -0,0 +1,5 @@ +--- +'@srcbook/web': patch +--- + +- Add essential support for OpenRouter models. diff --git a/.changeset/wicked-geckos-relate.md b/.changeset/wicked-geckos-relate.md new file mode 100644 index 00000000..32efbb7c --- /dev/null +++ b/.changeset/wicked-geckos-relate.md @@ -0,0 +1,5 @@ +--- +'srcbook': patch +--- + +Add GitHub Pages documentation repository structure and configuration. diff --git a/.gitignore b/.gitignore index 7efe4c7b..c37aebf0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ srcbook/lib/**/* # Aide *.code-workspace +# Docs folder +docs/ + vite.config.ts.timestamp-*.mjs \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a26bcd08 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# SrcBook Development Guide + +## Build & Development Commands +- **Install deps**: `pnpm install` +- **Development**: `pnpm dev` +- **Build**: `pnpm build` +- **Lint**: `pnpm lint` +- **Format check**: `pnpm check-format` +- **Format code**: `pnpm format` +- **Tests**: `pnpm test` +- **Single test**: `pnpm --filter vitest run [-t "test name"]` + +## Code Style Guidelines +- **Package manager**: pnpm with workspace support +- **Structure**: Monorepo using Turborepo +- **TypeScript**: Strict typing, ES2022 target, ESNext modules +- **Formatting**: Prettier with 2-space indentation, 100 char line limit, semicolons required +- **Imports**: Group imports by external/internal, no unused imports +- **Naming**: camelCase for variables/functions, PascalCase for classes/components/types +- **Error handling**: Prefer Result types over try/catch when appropriate +- **React**: Functional components with hooks, prefer composition over inheritance +- **File extensions**: `.mts` for TypeScript modules, `.tsx` for React components +- **Testing**: Vitest for unit tests, files named `*.test.mts` \ No newline at end of file diff --git a/packages/api/ai/config.mts b/packages/api/ai/config.mts index 499b37d0..7d080461 100644 --- a/packages/api/ai/config.mts +++ b/packages/api/ai/config.mts @@ -48,6 +48,17 @@ export async function getModel(): Promise { apiKey: config.xaiKey, }); 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') { diff --git a/packages/api/db/schema.mts b/packages/api/db/schema.mts index 512bbe4a..7d0700f8 100644 --- a/packages/api/db/schema.mts +++ b/packages/api/db/schema.mts @@ -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), diff --git a/packages/api/drizzle/0016_add_openrouter_api_key.sql b/packages/api/drizzle/0016_add_openrouter_api_key.sql new file mode 100644 index 00000000..51803a60 --- /dev/null +++ b/packages/api/drizzle/0016_add_openrouter_api_key.sql @@ -0,0 +1 @@ +ALTER TABLE `config` ADD `openrouter_api_key` text; \ No newline at end of file diff --git a/packages/shared/src/ai.mts b/packages/shared/src/ai.mts index b6753f5a..52db8df4 100644 --- a/packages/shared/src/ai.mts +++ b/packages/shared/src/ai.mts @@ -3,6 +3,7 @@ export const AiProvider = { Anthropic: 'anthropic', XAI: 'Xai', Gemini: 'Gemini', + OpenRouter: 'openrouter', Custom: 'custom', } as const; @@ -14,6 +15,7 @@ export const defaultModels: Record = { [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 { diff --git a/packages/web/src/components/use-settings.tsx b/packages/web/src/components/use-settings.tsx index c9e99992..fae2f077 100644 --- a/packages/web/src/components/use-settings.tsx +++ b/packages/web/src/components/use-settings.tsx @@ -36,6 +36,7 @@ export function SettingsProvider({ config, children }: ProviderPropsType) { (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; diff --git a/packages/web/src/routes/settings.tsx b/packages/web/src/routes/settings.tsx index c1775557..8b76373f 100644 --- a/packages/web/src/routes/settings.tsx +++ b/packages/web/src/routes/settings.tsx @@ -158,6 +158,16 @@ function AiInfoBanner() { ); + case 'openrouter': + return ( +
+

API key required

+ + Go to {aiProvider} + +
+ ); + case 'custom': return (
@@ -244,6 +254,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { customApiKey: configCustomApiKey, xaiKey: configXaiKey, geminiKey: configGeminiKey, + openrouterKey: configOpenrouterKey, updateConfig: updateConfigContext, } = useSettings(); @@ -251,6 +262,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { const [anthropicKey, setAnthropicKey] = useState(configAnthropicKey ?? ''); const [xaiKey, setXaiKey] = useState(configXaiKey ?? ''); const [geminiKey, setGeminiKey] = useState(configGeminiKey ?? ''); + const [openrouterKey, setOpenrouterKey] = useState(configOpenrouterKey ?? ''); const [customApiKey, setCustomApiKey] = useState(configCustomApiKey ?? ''); const [model, setModel] = useState(aiModel); const [baseUrl, setBaseUrl] = useState(aiBaseUrl || ''); @@ -280,10 +292,15 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { model !== aiModel; const geminiKeySaveEnabled = - (typeof configGeminiKey === 'string' && geminiKey !== configXaiKey) || + (typeof configGeminiKey === 'string' && geminiKey !== configGeminiKey) || ((configGeminiKey === null || configGeminiKey === undefined) && geminiKey.length > 0) || model !== aiModel; + const openrouterKeySaveEnabled = + (typeof configOpenrouterKey === 'string' && openrouterKey !== configOpenrouterKey) || + ((configOpenrouterKey === null || configOpenrouterKey === undefined) && openrouterKey.length > 0) || + model !== aiModel; + const customModelSaveEnabled = (typeof configCustomApiKey === 'string' && customApiKey !== configCustomApiKey) || ((configCustomApiKey === null || configCustomApiKey === undefined) && @@ -305,6 +322,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { anthropic Xai Gemini + openrouter custom @@ -395,6 +413,25 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) {
)} + {aiProvider === 'openrouter' && ( +
+ setOpenrouterKey(e.target.value)} + /> + +
+ )} + {aiProvider === 'custom' && (

diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index 356f9812..60c357c0 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -14,6 +14,7 @@ export type SettingsType = { anthropicKey?: string | null; xaiKey?: string | null; geminiKey?: string | null; + openrouterKey?: string | null; aiProvider: AiProviderType; customApiKey: string | null; aiModel: string;