diff --git a/packages/api/ai/config.mts b/packages/api/ai/config.mts index faf25af9..8f8d90ec 100644 --- a/packages/api/ai/config.mts +++ b/packages/api/ai/config.mts @@ -55,7 +55,7 @@ export async function getModel(): Promise { } const openaiCompatible = createOpenAI({ compatibility: 'compatible', - apiKey: 'bogus', // required but unused + apiKey: config.customApiKey || 'bogus', // Use custom API key if provided, otherwise use bogus key baseURL: aiBaseUrl, }); return openaiCompatible(model); diff --git a/packages/api/db/schema.mts b/packages/api/db/schema.mts index eba6b7d5..512bbe4a 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'), + 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), // Stable ID for posthog diff --git a/packages/api/drizzle/0015_add_custom_api_key.sql b/packages/api/drizzle/0015_add_custom_api_key.sql new file mode 100644 index 00000000..6a4e6193 --- /dev/null +++ b/packages/api/drizzle/0015_add_custom_api_key.sql @@ -0,0 +1 @@ +ALTER TABLE `config` ADD `custom_api_key` text; diff --git a/packages/api/drizzle/meta/0015_snapshot.json b/packages/api/drizzle/meta/0015_snapshot.json new file mode 100644 index 00000000..140c34bc --- /dev/null +++ b/packages/api/drizzle/meta/0015_snapshot.json @@ -0,0 +1,42 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0015_add_custom_api_key", + "prevId": "0014_Gemini_Integration", + "tables": { + "config": { + "name": "config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "openai_api_key": { + "name": "openai_api_key", + "type": "text" + }, + "posthog_api_key": { + "name": "posthog_api_key", + "type": "text" + }, + "subscription_email": { + "name": "subscription_email", + "type": "text" + }, + "custom_api_key": { + "name": "custom_api_key", + "type": "text" + } + } + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } + } \ No newline at end of file diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json index fb21da73..321904a8 100644 --- a/packages/api/drizzle/meta/_journal.json +++ b/packages/api/drizzle/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1732197490638, "tag": "0014_Gemini_Integration", "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1733076427000, + "tag": "0015_add_custom_api_key", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/web/src/routes/settings.tsx b/packages/web/src/routes/settings.tsx index 8f3079c1..8ff2b7e5 100644 --- a/packages/web/src/routes/settings.tsx +++ b/packages/web/src/routes/settings.tsx @@ -161,7 +161,7 @@ function AiInfoBanner() { case 'custom': return (
-

Base URL required

+

Base URL required, API key optional

); } @@ -243,6 +243,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { anthropicKey: configAnthropicKey, xaiKey: configXaiKey, geminiKey: configGeminiKey, + customApiKey: configCustomApiKey, updateConfig: updateConfigContext, } = useSettings(); @@ -250,6 +251,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { const [anthropicKey, setAnthropicKey] = useState(configAnthropicKey ?? ''); const [xaiKey, setXaiKey] = useState(configXaiKey ?? ''); const [geminiKey, setGeminiKey] = useState(configGeminiKey ?? ''); + const [customApiKey, setCustomApiKey] = useState(configCustomApiKey ?? ''); const [model, setModel] = useState(aiModel); const [baseUrl, setBaseUrl] = useState(aiBaseUrl || ''); @@ -283,6 +285,9 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { model !== aiModel; const customModelSaveEnabled = + (typeof configCustomApiKey === 'string' && customApiKey !== configCustomApiKey) || + ((configCustomApiKey === null || configCustomApiKey === undefined) && + customApiKey.length > 0) || (typeof aiBaseUrl === 'string' && baseUrl !== aiBaseUrl) || ((aiBaseUrl === null || aiBaseUrl === undefined) && baseUrl.length > 0) || model !== aiModel; @@ -394,22 +399,35 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) {

If you want to use an openai-compatible model (for example when running local models - with Ollama), choose this option and set the baseUrl. + with Ollama), choose this option and set the baseUrl and API key.

-
- setBaseUrl(e.target.value)} - /> - +
+
+ setBaseUrl(e.target.value)} + /> + setCustomApiKey(e.target.value)} + /> +
+
+ +
)} diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index 19c6d7a5..4ce764d6 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; + customApiKey?: string | null; aiProvider: AiProviderType; aiModel: string; aiBaseUrl?: string | null;