diff --git a/apps/server/src/routes/settings/routes/get-credentials.ts b/apps/server/src/routes/settings/routes/get-credentials.ts index be15b04b..140ccce4 100644 --- a/apps/server/src/routes/settings/routes/get-credentials.ts +++ b/apps/server/src/routes/settings/routes/get-credentials.ts @@ -5,7 +5,7 @@ * Each provider shows: `{ configured: boolean, masked: string }` * Masked shows first 4 and last 4 characters for verification. * - * Response: `{ "success": true, "credentials": { anthropic } }` + * Response: `{ "success": true, "credentials": { anthropic, google, openai } }` */ import type { Request, Response } from 'express'; diff --git a/apps/server/src/routes/settings/routes/update-credentials.ts b/apps/server/src/routes/settings/routes/update-credentials.ts index c08b2445..2b415830 100644 --- a/apps/server/src/routes/settings/routes/update-credentials.ts +++ b/apps/server/src/routes/settings/routes/update-credentials.ts @@ -1,7 +1,7 @@ /** * PUT /api/settings/credentials - Update API credentials * - * Updates API keys for Anthropic. Partial updates supported. + * Updates API keys for supported providers. Partial updates supported. * Returns masked credentials for verification without exposing full keys. * * Request body: `Partial` (usually just apiKeys) diff --git a/apps/server/src/routes/setup/routes/api-keys.ts b/apps/server/src/routes/setup/routes/api-keys.ts index 047b6455..ec870e7b 100644 --- a/apps/server/src/routes/setup/routes/api-keys.ts +++ b/apps/server/src/routes/setup/routes/api-keys.ts @@ -11,6 +11,7 @@ export function createApiKeysHandler() { res.json({ success: true, hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY, + hasGoogleKey: !!getApiKey('google'), hasOpenaiKey: !!getApiKey('openai') || !!process.env.OPENAI_API_KEY, }); } catch (error) { diff --git a/apps/server/src/routes/setup/routes/store-api-key.ts b/apps/server/src/routes/setup/routes/store-api-key.ts index e77a697e..eae2e430 100644 --- a/apps/server/src/routes/setup/routes/store-api-key.ts +++ b/apps/server/src/routes/setup/routes/store-api-key.ts @@ -21,22 +21,25 @@ export function createStoreApiKeyHandler() { return; } - setApiKey(provider, apiKey); - - // Also set as environment variable and persist to .env - if (provider === 'anthropic' || provider === 'anthropic_oauth_token') { - // Both API key and OAuth token use ANTHROPIC_API_KEY - process.env.ANTHROPIC_API_KEY = apiKey; - await persistApiKeyToEnv('ANTHROPIC_API_KEY', apiKey); - logger.info('[Setup] Stored API key as ANTHROPIC_API_KEY'); - } else { + const providerEnvMap: Record = { + anthropic: 'ANTHROPIC_API_KEY', + anthropic_oauth_token: 'ANTHROPIC_API_KEY', + openai: 'OPENAI_API_KEY', + }; + const envKey = providerEnvMap[provider]; + if (!envKey) { res.status(400).json({ success: false, - error: `Unsupported provider: ${provider}. Only anthropic is supported.`, + error: `Unsupported provider: ${provider}. Only anthropic and openai are supported.`, }); return; } + setApiKey(provider, apiKey); + process.env[envKey] = apiKey; + await persistApiKeyToEnv(envKey, apiKey); + logger.info(`[Setup] Stored API key as ${envKey}`); + res.json({ success: true }); } catch (error) { logError(error, 'Store API key failed'); diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts index f1dfd45c..5f57ad83 100644 --- a/apps/server/src/services/settings-service.ts +++ b/apps/server/src/services/settings-service.ts @@ -431,6 +431,8 @@ export class SettingsService { */ async getMaskedCredentials(): Promise<{ anthropic: { configured: boolean; masked: string }; + google: { configured: boolean; masked: string }; + openai: { configured: boolean; masked: string }; }> { const credentials = await this.getCredentials(); @@ -444,6 +446,14 @@ export class SettingsService { configured: !!credentials.apiKeys.anthropic, masked: maskKey(credentials.apiKeys.anthropic), }, + google: { + configured: !!credentials.apiKeys.google, + masked: maskKey(credentials.apiKeys.google), + }, + openai: { + configured: !!credentials.apiKeys.openai, + masked: maskKey(credentials.apiKeys.openai), + }, }; } diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 621e5365..403fc3cf 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -79,6 +79,7 @@ export { type ModelAlias, type CodexModelId, type AgentModel, + type ModelId, } from './model.js'; // Event types diff --git a/libs/types/src/model.ts b/libs/types/src/model.ts index 1a898640..949938c9 100644 --- a/libs/types/src/model.ts +++ b/libs/types/src/model.ts @@ -1,6 +1,9 @@ /** * Model alias mapping for Claude models */ +import type { CursorModelId } from './cursor-models.js'; +import type { OpencodeModelId } from './opencode-models.js'; + export const CLAUDE_MODEL_MAP: Record = { haiku: 'claude-haiku-4-5-20251001', sonnet: 'claude-sonnet-4-5-20250929', @@ -74,3 +77,26 @@ export type CodexModelId = (typeof CODEX_MODEL_MAP)[keyof typeof CODEX_MODEL_MAP * Represents available models across providers */ export type AgentModel = ModelAlias | CodexModelId; + +/** + * Dynamic provider model IDs discovered at runtime (provider/model format) + */ +export type DynamicModelId = `${string}/${string}`; + +/** + * Provider-prefixed model IDs used for routing + */ +export type PrefixedCursorModelId = `cursor-${string}`; +export type PrefixedOpencodeModelId = `opencode-${string}`; + +/** + * ModelId - Unified model identifier across providers + */ +export type ModelId = + | ModelAlias + | CodexModelId + | CursorModelId + | OpencodeModelId + | DynamicModelId + | PrefixedCursorModelId + | PrefixedOpencodeModelId; diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index a4efa469..78496c92 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -6,7 +6,7 @@ * (for file I/O via SettingsService) and the UI (for state management and sync). */ -import type { ModelAlias, AgentModel, CodexModelId } from './model.js'; +import type { ModelAlias, ModelId } from './model.js'; import type { CursorModelId } from './cursor-models.js'; import { CURSOR_MODEL_MAP, getAllCursorModelIds } from './cursor-models.js'; import type { OpencodeModelId } from './opencode-models.js'; @@ -114,8 +114,8 @@ const DEFAULT_CODEX_ADDITIONAL_DIRS: string[] = []; * - Cursor models: Handle thinking internally */ export interface PhaseModelEntry { - /** The model to use (Claude alias, Cursor model ID, or Codex model ID) */ - model: ModelAlias | CursorModelId | CodexModelId; + /** The model to use (supports Claude, Cursor, Codex, OpenCode, and dynamic provider IDs) */ + model: ModelId; /** Extended thinking level (only applies to Claude models, defaults to 'none') */ thinkingLevel?: ThinkingLevel; /** Reasoning effort level (only applies to Codex models, defaults to 'none') */