diff --git a/.changeset/dirty-grapes-buy.md b/.changeset/dirty-grapes-buy.md new file mode 100644 index 00000000..f4f7e0d0 --- /dev/null +++ b/.changeset/dirty-grapes-buy.md @@ -0,0 +1,7 @@ +--- +'@srcbook/api': patch +'@srcbook/web': patch +'srcbook': patch +--- + +Add support for API keys on openAI compatible models diff --git a/packages/api/ai/config.mts b/packages/api/ai/config.mts index faf25af9..499b37d0 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 set, otherwise use a 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..5ad66f0a --- /dev/null +++ b/packages/api/drizzle/meta/0015_snapshot.json @@ -0,0 +1,283 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e4b05cbe-90d2-41a7-96fc-2130bd54bc16", + "prevId": "c148b92f-4dbc-4a31-887d-dfaebd4db615", + "tables": { + "apps": { + "name": "apps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "history": { + "name": "history", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "history_version": { + "name": "history_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "apps_external_id_unique": { + "name": "apps_external_id_unique", + "columns": [ + "external_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "base_dir": { + "name": "base_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_language": { + "name": "default_language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'typescript'" + }, + "openai_api_key": { + "name": "openai_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "anthropic_api_key": { + "name": "anthropic_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "xai_api_key": { + "name": "xai_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gemini_api_key": { + "name": "gemini_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_api_key": { + "name": "custom_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled_analytics": { + "name": "enabled_analytics", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "srcbook_installation_id": { + "name": "srcbook_installation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'q2rr0ckhmrca5rt22825iv43p4'" + }, + "ai_provider": { + "name": "ai_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openai'" + }, + "ai_model": { + "name": "ai_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'gpt-4o'" + }, + "ai_base_url": { + "name": "ai_base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_email": { + "name": "subscription_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "secrets": { + "name": "secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "secrets_name_unique": { + "name": "secrets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "secrets_to_sessions": { + "name": "secrets_to_sessions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "secrets_to_sessions_session_id_secret_id_unique": { + "name": "secrets_to_sessions_session_id_secret_id_unique", + "columns": [ + "session_id", + "secret_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "secrets_to_sessions_secret_id_secrets_id_fk": { + "name": "secrets_to_sessions_secret_id_secrets_id_fk", + "tableFrom": "secrets_to_sessions", + "tableTo": "secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json index fb21da73..2154593e 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": 1737324288698, + "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..c1775557 100644 --- a/packages/web/src/routes/settings.tsx +++ b/packages/web/src/routes/settings.tsx @@ -241,6 +241,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { aiBaseUrl, openaiKey: configOpenaiKey, anthropicKey: configAnthropicKey, + customApiKey: configCustomApiKey, xaiKey: configXaiKey, geminiKey: configGeminiKey, updateConfig: updateConfigContext, @@ -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,36 @@ 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 or using a third party like togetherAI), choose this option and set the + baseUrl. Optionally add an API key if needed.

-
- 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..356f9812 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -15,6 +15,7 @@ export type SettingsType = { xaiKey?: string | null; geminiKey?: string | null; aiProvider: AiProviderType; + customApiKey: string | null; aiModel: string; aiBaseUrl?: string | null; subscriptionEmail?: string | null;