feat(settings): add OpenAI/Google API key support and unified ModelId type

- Add OpenAI API key storage to store-api-key handler
- Include Google/OpenAI key status in credentials API responses
- Add unified ModelId type for Claude, Codex, Cursor, OpenCode, and dynamic providers
- Update PhaseModelEntry to support all provider model types
This commit is contained in:
DhanushSantosh
2026-01-14 00:49:35 +05:30
parent 690cf1f281
commit fbb3f697e1
8 changed files with 56 additions and 15 deletions

View File

@@ -5,7 +5,7 @@
* Each provider shows: `{ configured: boolean, masked: string }` * Each provider shows: `{ configured: boolean, masked: string }`
* Masked shows first 4 and last 4 characters for verification. * 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'; import type { Request, Response } from 'express';

View File

@@ -1,7 +1,7 @@
/** /**
* PUT /api/settings/credentials - Update API credentials * 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. * Returns masked credentials for verification without exposing full keys.
* *
* Request body: `Partial<Credentials>` (usually just apiKeys) * Request body: `Partial<Credentials>` (usually just apiKeys)

View File

@@ -11,6 +11,7 @@ export function createApiKeysHandler() {
res.json({ res.json({
success: true, success: true,
hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY, hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY,
hasGoogleKey: !!getApiKey('google'),
hasOpenaiKey: !!getApiKey('openai') || !!process.env.OPENAI_API_KEY, hasOpenaiKey: !!getApiKey('openai') || !!process.env.OPENAI_API_KEY,
}); });
} catch (error) { } catch (error) {

View File

@@ -21,22 +21,25 @@ export function createStoreApiKeyHandler() {
return; return;
} }
setApiKey(provider, apiKey); const providerEnvMap: Record<string, string> = {
anthropic: 'ANTHROPIC_API_KEY',
// Also set as environment variable and persist to .env anthropic_oauth_token: 'ANTHROPIC_API_KEY',
if (provider === 'anthropic' || provider === 'anthropic_oauth_token') { openai: 'OPENAI_API_KEY',
// Both API key and OAuth token use ANTHROPIC_API_KEY };
process.env.ANTHROPIC_API_KEY = apiKey; const envKey = providerEnvMap[provider];
await persistApiKeyToEnv('ANTHROPIC_API_KEY', apiKey); if (!envKey) {
logger.info('[Setup] Stored API key as ANTHROPIC_API_KEY');
} else {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: `Unsupported provider: ${provider}. Only anthropic is supported.`, error: `Unsupported provider: ${provider}. Only anthropic and openai are supported.`,
}); });
return; return;
} }
setApiKey(provider, apiKey);
process.env[envKey] = apiKey;
await persistApiKeyToEnv(envKey, apiKey);
logger.info(`[Setup] Stored API key as ${envKey}`);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logError(error, 'Store API key failed'); logError(error, 'Store API key failed');

View File

@@ -431,6 +431,8 @@ export class SettingsService {
*/ */
async getMaskedCredentials(): Promise<{ async getMaskedCredentials(): Promise<{
anthropic: { configured: boolean; masked: string }; anthropic: { configured: boolean; masked: string };
google: { configured: boolean; masked: string };
openai: { configured: boolean; masked: string };
}> { }> {
const credentials = await this.getCredentials(); const credentials = await this.getCredentials();
@@ -444,6 +446,14 @@ export class SettingsService {
configured: !!credentials.apiKeys.anthropic, configured: !!credentials.apiKeys.anthropic,
masked: maskKey(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),
},
}; };
} }

View File

@@ -79,6 +79,7 @@ export {
type ModelAlias, type ModelAlias,
type CodexModelId, type CodexModelId,
type AgentModel, type AgentModel,
type ModelId,
} from './model.js'; } from './model.js';
// Event types // Event types

View File

@@ -1,6 +1,9 @@
/** /**
* Model alias mapping for Claude models * 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<string, string> = { export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: 'claude-haiku-4-5-20251001', haiku: 'claude-haiku-4-5-20251001',
sonnet: 'claude-sonnet-4-5-20250929', 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 * Represents available models across providers
*/ */
export type AgentModel = ModelAlias | CodexModelId; 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;

View File

@@ -6,7 +6,7 @@
* (for file I/O via SettingsService) and the UI (for state management and sync). * (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 type { CursorModelId } from './cursor-models.js';
import { CURSOR_MODEL_MAP, getAllCursorModelIds } from './cursor-models.js'; import { CURSOR_MODEL_MAP, getAllCursorModelIds } from './cursor-models.js';
import type { OpencodeModelId } from './opencode-models.js'; import type { OpencodeModelId } from './opencode-models.js';
@@ -114,8 +114,8 @@ const DEFAULT_CODEX_ADDITIONAL_DIRS: string[] = [];
* - Cursor models: Handle thinking internally * - Cursor models: Handle thinking internally
*/ */
export interface PhaseModelEntry { export interface PhaseModelEntry {
/** The model to use (Claude alias, Cursor model ID, or Codex model ID) */ /** The model to use (supports Claude, Cursor, Codex, OpenCode, and dynamic provider IDs) */
model: ModelAlias | CursorModelId | CodexModelId; model: ModelId;
/** Extended thinking level (only applies to Claude models, defaults to 'none') */ /** Extended thinking level (only applies to Claude models, defaults to 'none') */
thinkingLevel?: ThinkingLevel; thinkingLevel?: ThinkingLevel;
/** Reasoning effort level (only applies to Codex models, defaults to 'none') */ /** Reasoning effort level (only applies to Codex models, defaults to 'none') */