Skip to content

Commit 17848a3

Browse files
authored
Add OpenRouter support (#490)
* chore: add changeset for GitHub Pages documentation * feat: add GitHub Pages documentation structure * docs: add GitHub Pages landing page explaining .src.md format * docs: add navigation links to README * docs: rename API Reference to Technical Reference for better accuracy * Add OpenRouter support. * Provide means for user to select model and enter API key for OpenRouter in UI. * Add CLAUDE.md dev guide for agentic assistants; gitignore docs folder - Created CLAUDE.md with build commands, lint commands, and code style guidelines - Added docs/ folder to .gitignore to keep repo more focused * Remove docs directory from repository This commit removes the docs directory from the repository to keep the codebase focused on the OpenRouter integration feature. The documentation is still preserved in the feat/docs branch. * Autopopulate available OpenRouter models. * Remove CLAUDE.md. fix TypeScript issues in formatModelName funcion in the OpenRouter model selector. * minor: update formatting
1 parent dc93233 commit 17848a3

File tree

11 files changed

+254
-4
lines changed

11 files changed

+254
-4
lines changed

.changeset/tall-points-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@srcbook/web': patch
3+
---
4+
5+
- Add essential support for OpenRouter models.

.changeset/thin-ears-approve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@srcbook/web': patch
3+
---
4+
5+
Auto-populate available models in Settings.

.changeset/wicked-geckos-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'srcbook': patch
3+
---
4+
5+
Add GitHub Pages documentation repository structure and configuration.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,7 @@ srcbook/lib/**/*
4242
# Aide
4343
*.code-workspace
4444

45+
# Docs folder
46+
docs/
47+
4548
vite.config.ts.timestamp-*.mjs

packages/api/ai/config.mts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ export async function getModel(): Promise<LanguageModel> {
4949
});
5050
return xai(model);
5151

52+
case 'openrouter':
53+
if (!config.openrouterKey) {
54+
throw new Error('OpenRouter API key is not set');
55+
}
56+
const openrouter = createOpenAI({
57+
compatibility: 'compatible',
58+
baseURL: 'https://openrouter.ai/api/v1',
59+
apiKey: config.openrouterKey,
60+
});
61+
return openrouter(model);
62+
5263
case 'custom':
5364
if (typeof aiBaseUrl !== 'string') {
5465
throw new Error('Local AI base URL is not set');

packages/api/db/schema.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const configs = sqliteTable('config', {
1010
anthropicKey: text('anthropic_api_key'),
1111
xaiKey: text('xai_api_key'),
1212
geminiKey: text('gemini_api_key'),
13+
openrouterKey: text('openrouter_api_key'),
1314
customApiKey: text('custom_api_key'),
1415
// TODO: This is deprecated in favor of SRCBOOK_DISABLE_ANALYTICS env variable. Remove this.
1516
enabledAnalytics: integer('enabled_analytics', { mode: 'boolean' }).notNull().default(true),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE `config` ADD `openrouter_api_key` text;

packages/shared/src/ai.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const AiProvider = {
33
Anthropic: 'anthropic',
44
XAI: 'Xai',
55
Gemini: 'Gemini',
6+
OpenRouter: 'openrouter',
67
Custom: 'custom',
78
} as const;
89

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

1921
export function isValidProvider(provider: string): provider is AiProviderType {

packages/web/src/components/use-settings.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
import React, { createContext, useContext } from 'react';
1+
import React, { createContext, useContext, useState, useEffect } from 'react';
22
import { useRevalidator } from 'react-router-dom';
33
import { updateConfig as updateConfigServer } from '@/lib/server';
44
import type { SettingsType } from '@/types';
55

6+
export type OpenRouterModel = {
7+
id: string;
8+
name: string;
9+
provider: string;
10+
description?: string;
11+
pricing?: Record<string, number>;
12+
context_length?: number;
13+
};
14+
15+
export type GroupedOpenRouterModels = Record<string, OpenRouterModel[]>;
16+
617
export type SettingsContextValue = SettingsType & {
718
aiEnabled: boolean;
819
updateConfig: (newConfig: Partial<SettingsType>) => Promise<void>;
20+
openRouterModels: GroupedOpenRouterModels;
21+
isLoadingOpenRouterModels: boolean;
22+
refreshOpenRouterModels: () => Promise<void>;
923
};
1024

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

32+
async function fetchOpenRouterModels(): Promise<OpenRouterModel[]> {
33+
try {
34+
const response = await fetch('https://openrouter.ai/api/v1/models');
35+
if (!response.ok) {
36+
throw new Error('Failed to fetch models');
37+
}
38+
const data = await response.json();
39+
return data.data || [];
40+
} catch (error) {
41+
console.error('Error fetching OpenRouter models:', error);
42+
return [];
43+
}
44+
}
45+
46+
function groupModelsByProvider(models: OpenRouterModel[]): GroupedOpenRouterModels {
47+
return models.reduce((grouped, model) => {
48+
const provider = model.provider || 'Unknown';
49+
if (!grouped[provider]) {
50+
grouped[provider] = [];
51+
}
52+
grouped[provider].push(model);
53+
return grouped;
54+
}, {} as GroupedOpenRouterModels);
55+
}
56+
1857
/**
1958
* An interface for working with our config.
2059
*/
2160
export function SettingsProvider({ config, children }: ProviderPropsType) {
2261
const revalidator = useRevalidator();
62+
const [openRouterModels, setOpenRouterModels] = useState<GroupedOpenRouterModels>({});
63+
const [isLoadingOpenRouterModels, setIsLoadingOpenRouterModels] = useState(false);
2364

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

75+
const refreshOpenRouterModels = async () => {
76+
setIsLoadingOpenRouterModels(true);
77+
try {
78+
const models = await fetchOpenRouterModels();
79+
const grouped = groupModelsByProvider(models);
80+
setOpenRouterModels(grouped);
81+
} finally {
82+
setIsLoadingOpenRouterModels(false);
83+
}
84+
};
85+
86+
useEffect(() => {
87+
if (config.aiProvider === 'openrouter') {
88+
refreshOpenRouterModels();
89+
}
90+
}, [config.aiProvider]);
91+
3492
const aiEnabled =
3593
(config.openaiKey && config.aiProvider === 'openai') ||
3694
(config.anthropicKey && config.aiProvider === 'anthropic') ||
3795
(config.xaiKey && config.aiProvider === 'Xai') ||
3896
(config.geminiKey && config.aiProvider === 'Gemini') ||
97+
(config.openrouterKey && config.aiProvider === 'openrouter') ||
3998
(config.aiProvider === 'custom' && !!config.aiBaseUrl) ||
4099
false;
41100

42101
const context: SettingsContextValue = {
43102
...config,
44103
aiEnabled,
45104
updateConfig,
105+
openRouterModels,
106+
isLoadingOpenRouterModels,
107+
refreshOpenRouterModels,
46108
};
47109

48110
return <SettingsContext.Provider value={context}>{children}</SettingsContext.Provider>;

0 commit comments

Comments
 (0)