Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SrcBook Development Guide
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put this in a separate PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed CLAUDE.md from this PR.


## 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 <package> vitest run <test-file> [-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`
11 changes: 11 additions & 0 deletions packages/api/ai/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ export async function getModel(): Promise<LanguageModel> {
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') {
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
Loading