From 14dc54a093bec0f3bf95c873668bd5a6a1a72cca Mon Sep 17 00:00:00 2001 From: HitMargin Date: Mon, 6 Apr 2026 11:23:05 +0800 Subject: [PATCH] =?UTF-8?q?gemini=E6=A8=A1=E5=9E=8B=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E5=88=86=E7=A6=BB=20provider=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E6=94=AF=E6=8C=81=E5=88=87=E6=8D=A2gemini?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 31 ++++++ src/commands/__tests__/provider.test.ts | 102 ++++++++++++++++++ src/commands/provider.ts | 55 ++++++++-- src/components/ConsoleOAuthFlow.tsx | 12 +-- .../api/gemini/__tests__/modelMapping.test.ts | 54 +++++++--- src/services/api/gemini/modelMapping.ts | 11 +- src/utils/managedEnvConstants.ts | 27 +++++ src/utils/model/model.ts | 34 +++++- src/utils/model/modelOptions.ts | 45 +++++--- 9 files changed, 324 insertions(+), 47 deletions(-) create mode 100644 src/commands/__tests__/provider.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 4cd5556ff..d68ad015c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -182,6 +182,37 @@ Feature flag `VOICE_MODE`,dev/build 默认启用。Push-to-Talk 语音输入 关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`、`OPENAI_DEFAULT_OPUS_MODEL`、`OPENAI_DEFAULT_SONNET_MODEL`、`OPENAI_DEFAULT_HAIKU_MODEL`。详见 `docs/plans/openai-compatibility.md`。 +### Gemini 兼容层 + +通过 `CLAUDE_CODE_USE_GEMINI=1` 环境变量或 `modelType: "gemini"` 设置启用,支持 Google Gemini API。独立的环境变量体系,不与 OpenAI 或 Anthropic 配置混杂。 + +- **`src/services/api/gemini/`** — client、模型映射、类型定义 +- **`src/utils/model/providers.ts`** — 添加 `'gemini'` provider 类型 +- **`src/utils/managedEnvConstants.ts`** — Gemini 专用的 managed env vars + +关键环境变量: +- `CLAUDE_CODE_USE_GEMINI` - 启用 Gemini provider +- `GEMINI_API_KEY` - API 密钥(必填) +- `GEMINI_BASE_URL` - API 端点(可选,默认 `https://generativelanguage.googleapis.com/v1beta`) +- `GEMINI_MODEL` - 直接指定模型(最高优先级) +- `GEMINI_DEFAULT_HAIKU_MODEL` / `GEMINI_DEFAULT_SONNET_MODEL` / `GEMINI_DEFAULT_OPUS_MODEL` - 按能力级别映射 +- `GEMINI_DEFAULT_HAIKU_MODEL_NAME` / `DESCRIPTION` / `SUPPORTED_CAPABILITIES` - 显示名称和描述 +- `GEMINI_SMALL_FAST_MODEL` - 快速任务使用的模型(可选) + +模型映射优先级(`src/services/api/gemini/modelMapping.ts`): +1. `GEMINI_MODEL` - 直接覆盖 +2. `GEMINI_DEFAULT_*_MODEL` - 独立配置(推荐) +3. `ANTHROPIC_DEFAULT_*_MODEL` - 向后兼容 fallback(已废弃) +4. 原样返回 Anthropic 模型名 + +使用示例: +```bash +export CLAUDE_CODE_USE_GEMINI=1 +export GEMINI_API_KEY="your-api-key" +export GEMINI_DEFAULT_SONNET_MODEL="gemini-2.5-flash" +export GEMINI_DEFAULT_OPUS_MODEL="gemini-2.5-pro" +``` + ### Key Type Files - **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers. diff --git a/src/commands/__tests__/provider.test.ts b/src/commands/__tests__/provider.test.ts new file mode 100644 index 000000000..f2f67aeee --- /dev/null +++ b/src/commands/__tests__/provider.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"; +import { readFileSync, writeFileSync } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { homedir } from "os"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function getSettingsPath(): string { + return path.join(homedir(), ".claude", "settings.json"); +} + +// Mock settings module +mock.module("../utils/settings/settings.js", () => ({ + getInitialSettings: () => ({}), + getSettings_DEPRECATED: () => ({}), + getSettingsForSource: () => ({}), + updateSettingsForSource: () => {}, +})); +mock.module("../utils/managedEnv.js", () => ({ + applyConfigEnvironmentVariables: () => {}, +})); + +const { default: providerCommand } = await import("../provider.ts"); + +describe("provider command", () => { + const envKeys = [ + "CLAUDE_CODE_USE_GEMINI", + "CLAUDE_CODE_USE_BEDROCK", + "CLAUDE_CODE_USE_VERTEX", + "CLAUDE_CODE_USE_FOUNDRY", + "CLAUDE_CODE_USE_OPENAI", + "GEMINI_API_KEY", + "OPENAI_API_KEY", + "OPENAI_BASE_URL", + ] as const; + + const savedEnv: Record = {}; + + beforeEach(() => { + // Save and clear environment variables + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + // Restore environment variables + for (const key of envKeys) { + if (savedEnv[key] !== undefined) { + process.env[key] = savedEnv[key]; + } else { + delete process.env[key]; + } + } + }); + + test("validates gemini as a valid provider", async () => { + const result = await providerCommand.load().then(cmd => cmd.call("gemini", {} as any)); + expect(result).toBeDefined(); + // Should not return an error about invalid provider + if (result && typeof result === 'object' && 'value' in result) { + expect(result.value as string).toContain("gemini"); + } + }); + + test("switches to gemini without API key warning", async () => { + const result = await providerCommand.load().then(cmd => cmd.call("gemini", {} as any)); + expect(result).toBeDefined(); + if (result && typeof result === 'object' && 'value' in result) { + const value = result.value as string; + // Should either succeed or show warning about missing API key + expect(value).toMatch(/API provider set to gemini|Switched to Gemini provider/); + } + }); + + test("switches to gemini with API key set", async () => { + process.env.GEMINI_API_KEY = "test-key"; + const result = await providerCommand.load().then(cmd => cmd.call("gemini", {} as any)); + expect(result).toBeDefined(); + if (result && typeof result === 'object' && 'value' in result) { + const value = result.value as string; + expect(value).toContain("API provider set to gemini"); + expect(value).not.toContain("Warning"); + } + }); + + test("provider list includes gemini", async () => { + // Test that help or description shows gemini is supported + expect(providerCommand.description).toContain("gemini"); + expect(providerCommand.argumentHint).toContain("gemini"); + }); + + test("unset clears gemini env var", async () => { + process.env.CLAUDE_CODE_USE_GEMINI = "1"; + const result = await providerCommand.load().then(cmd => cmd.call("unset", {} as any)); + expect(result).toBeDefined(); + expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined(); + }); +}); diff --git a/src/commands/provider.ts b/src/commands/provider.ts index 4379dba78..a111555c8 100644 --- a/src/commands/provider.ts +++ b/src/commands/provider.ts @@ -13,6 +13,8 @@ function getEnvVarForProvider(provider: string): string { return 'CLAUDE_CODE_USE_VERTEX' case 'foundry': return 'CLAUDE_CODE_USE_FOUNDRY' + case 'gemini': + return 'CLAUDE_CODE_USE_GEMINI' default: throw new Error(`Unknown provider: ${provider}`) } @@ -45,13 +47,27 @@ const call: LocalCommandCall = async (args, context) => { delete process.env.CLAUDE_CODE_USE_VERTEX delete process.env.CLAUDE_CODE_USE_FOUNDRY delete process.env.CLAUDE_CODE_USE_OPENAI - return { type: 'text', value: 'API provider cleared (will use environment variables).' } + delete process.env.CLAUDE_CODE_USE_GEMINI + return { + type: 'text', + value: 'API provider cleared (will use environment variables).', + } } // Validate provider - const validProviders = ['anthropic', 'openai', 'bedrock', 'vertex', 'foundry'] + const validProviders = [ + 'anthropic', + 'openai', + 'gemini', + 'bedrock', + 'vertex', + 'foundry', + ] if (!validProviders.includes(arg)) { - return { type: 'text', value: `Invalid provider: ${arg}\nValid: ${validProviders.join(', ')}` } + return { + type: 'text', + value: `Invalid provider: ${arg}\nValid: ${validProviders.join(', ')}`, + } } // Check env vars when switching to openai (including settings.env) @@ -71,37 +87,58 @@ const call: LocalCommandCall = async (args, context) => { } } + // Check env vars when switching to gemini (including settings.env) + if (arg === 'gemini') { + const mergedEnv = getMergedEnv() + const hasKey = !!mergedEnv.GEMINI_API_KEY + // GEMINI_BASE_URL is optional (has default) + if (!hasKey) { + updateSettingsForSource('userSettings', { modelType: 'gemini' }) + return { + type: 'text', + value: `Switched to Gemini provider.\nWarning: Missing env var: GEMINI_API_KEY\nConfigure it via /login or set manually.`, + } + } + } + // Handle different provider types - // - 'anthropic' and 'openai' are stored in settings.json (persistent) + // - 'anthropic', 'openai', 'gemini' are stored in settings.json (persistent) // - 'bedrock', 'vertex', 'foundry' are env-only (do NOT touch settings.json) - if (arg === 'anthropic' || arg === 'openai') { + if (arg === 'anthropic' || arg === 'openai' || arg === 'gemini') { // Clear any cloud provider env vars to avoid conflicts delete process.env.CLAUDE_CODE_USE_BEDROCK delete process.env.CLAUDE_CODE_USE_VERTEX delete process.env.CLAUDE_CODE_USE_FOUNDRY + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI // Update settings.json updateSettingsForSource('userSettings', { modelType: arg }) // Ensure settings.env gets applied to process.env applyConfigEnvironmentVariables() return { type: 'text', value: `API provider set to ${arg}.` } } else { - // Cloud providers: set env vars only, do NOT touch settings.modelType + // Cloud providers: set env vars only, do NOT touch settings.json delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.OPENAI_API_KEY delete process.env.OPENAI_BASE_URL + delete process.env.CLAUDE_CODE_USE_GEMINI process.env[getEnvVarForProvider(arg)] = '1' // Do not modify settings.json - cloud providers controlled solely by env vars applyConfigEnvironmentVariables() - return { type: 'text', value: `API provider set to ${arg} (via environment variable).` } + return { + type: 'text', + value: `API provider set to ${arg} (via environment variable).`, + } } } const provider = { type: 'local', name: 'provider', - description: 'Switch API provider (anthropic/openai/bedrock/vertex/foundry)', + description: + 'Switch API provider (anthropic/openai/gemini/bedrock/vertex/foundry)', aliases: ['api'], - argumentHint: '[anthropic|openai|bedrock|vertex|foundry|unset]', + argumentHint: '[anthropic|openai|gemini|bedrock|vertex|foundry|unset]', supportsNonInteractive: true, load: () => Promise.resolve({ call }), } satisfies Command diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 65816d56a..f520d3664 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -561,9 +561,9 @@ function OAuthStatusMessage({ state: 'gemini_api', baseUrl: process.env.GEMINI_BASE_URL ?? '', apiKey: process.env.GEMINI_API_KEY ?? '', - haikuModel: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '', - sonnetModel: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? '', - opusModel: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? '', + haikuModel: process.env.GEMINI_DEFAULT_HAIKU_MODEL ?? '', + sonnetModel: process.env.GEMINI_DEFAULT_SONNET_MODEL ?? '', + opusModel: process.env.GEMINI_DEFAULT_OPUS_MODEL ?? '', activeField: 'base_url', }) } else if (value === 'platform') { @@ -1127,9 +1127,9 @@ function OAuthStatusMessage({ const env: Record = {} if (finalVals.base_url) env.GEMINI_BASE_URL = finalVals.base_url if (finalVals.api_key) env.GEMINI_API_KEY = finalVals.api_key - if (finalVals.haiku_model) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals.haiku_model - if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model - if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model + if (finalVals.haiku_model) env.GEMINI_DEFAULT_HAIKU_MODEL = finalVals.haiku_model + if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model + if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model const { error } = updateSettingsForSource('userSettings', { modelType: 'gemini' as any, env, diff --git a/src/services/api/gemini/__tests__/modelMapping.test.ts b/src/services/api/gemini/__tests__/modelMapping.test.ts index 18846b23c..37a285ab8 100644 --- a/src/services/api/gemini/__tests__/modelMapping.test.ts +++ b/src/services/api/gemini/__tests__/modelMapping.test.ts @@ -4,6 +4,9 @@ import { resolveGeminiModel } from '../modelMapping.js' describe('resolveGeminiModel', () => { const originalEnv = { GEMINI_MODEL: process.env.GEMINI_MODEL, + GEMINI_DEFAULT_HAIKU_MODEL: process.env.GEMINI_DEFAULT_HAIKU_MODEL, + GEMINI_DEFAULT_SONNET_MODEL: process.env.GEMINI_DEFAULT_SONNET_MODEL, + GEMINI_DEFAULT_OPUS_MODEL: process.env.GEMINI_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL, @@ -11,6 +14,9 @@ describe('resolveGeminiModel', () => { beforeEach(() => { delete process.env.GEMINI_MODEL + delete process.env.GEMINI_DEFAULT_HAIKU_MODEL + delete process.env.GEMINI_DEFAULT_SONNET_MODEL + delete process.env.GEMINI_DEFAULT_OPUS_MODEL delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL @@ -27,35 +33,57 @@ describe('resolveGeminiModel', () => { expect(resolveGeminiModel('claude-sonnet-4-6')).toBe('gemini-2.5-pro') }) - test('resolves sonnet model from shared family override', () => { + test('GEMINI_DEFAULT_*_MODEL takes precedence over ANTHROPIC_DEFAULT_*', () => { + process.env.GEMINI_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash-priority' + process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash-fallback' + + expect(resolveGeminiModel('claude-sonnet-4-6')).toBe( + 'gemini-2.5-flash-priority', + ) + }) + + test('resolves sonnet model from GEMINI_DEFAULT_SONNET_MODEL', () => { + process.env.GEMINI_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash' + expect(resolveGeminiModel('claude-sonnet-4-6')).toBe('gemini-2.5-flash') + }) + + test('resolves haiku model from GEMINI_DEFAULT_HAIKU_MODEL', () => { + process.env.GEMINI_DEFAULT_HAIKU_MODEL = 'gemini-2.5-flash-lite' + expect(resolveGeminiModel('claude-haiku-4-5-20251001')).toBe( + 'gemini-2.5-flash-lite', + ) + }) + + test('resolves opus model from GEMINI_DEFAULT_OPUS_MODEL', () => { + process.env.GEMINI_DEFAULT_OPUS_MODEL = 'gemini-2.5-pro' + expect(resolveGeminiModel('claude-opus-4-6')).toBe('gemini-2.5-pro') + }) + + test('falls back to ANTHROPIC_DEFAULT_* when GEMINI_DEFAULT_* not set', () => { process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash' expect(resolveGeminiModel('claude-sonnet-4-6')).toBe('gemini-2.5-flash') }) - test('resolves haiku model from shared family override', () => { + test('resolves haiku from ANTHROPIC_DEFAULT_HAIKU_MODEL as fallback', () => { process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = 'gemini-2.5-flash-lite' expect(resolveGeminiModel('claude-haiku-4-5-20251001')).toBe( 'gemini-2.5-flash-lite', ) }) - test('resolves opus model from shared family override', () => { + test('resolves opus from ANTHROPIC_DEFAULT_OPUS_MODEL as fallback', () => { process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'gemini-2.5-pro' expect(resolveGeminiModel('claude-opus-4-6')).toBe('gemini-2.5-pro') }) - test('uses shared family override', () => { + test('uses backward compatible family override', () => { process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'legacy-gemini-sonnet' - expect(resolveGeminiModel('claude-sonnet-4-6')).toBe( - 'legacy-gemini-sonnet', - ) + expect(resolveGeminiModel('claude-sonnet-4-6')).toBe('legacy-gemini-sonnet') }) test('strips [1m] suffix before resolving', () => { - process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash' - expect(resolveGeminiModel('claude-sonnet-4-6[1m]')).toBe( - 'gemini-2.5-flash', - ) + process.env.GEMINI_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash' + expect(resolveGeminiModel('claude-sonnet-4-6[1m]')).toBe('gemini-2.5-flash') }) test('passes through explicit Gemini model names', () => { @@ -64,9 +92,9 @@ describe('resolveGeminiModel', () => { ) }) - test('throws when family mapping is missing', () => { + test('throws when no Gemini model configuration is available', () => { expect(() => resolveGeminiModel('claude-sonnet-4-6')).toThrow( - 'Gemini provider requires GEMINI_MODEL or ANTHROPIC_DEFAULT_SONNET_MODEL to be configured.', + 'Gemini provider requires GEMINI_MODEL or GEMINI_DEFAULT_SONNET_MODEL (or ANTHROPIC_DEFAULT_SONNET_MODEL for backward compatibility) to be configured.', ) }) }) diff --git a/src/services/api/gemini/modelMapping.ts b/src/services/api/gemini/modelMapping.ts index a18571174..1d372e026 100644 --- a/src/services/api/gemini/modelMapping.ts +++ b/src/services/api/gemini/modelMapping.ts @@ -17,6 +17,14 @@ export function resolveGeminiModel(anthropicModel: string): string { return cleanModel } + // First, try Gemini-specific DEFAULT variables (separated from Anthropic) + const geminiEnvVar = `GEMINI_DEFAULT_${family.toUpperCase()}_MODEL` + const geminiModel = process.env[geminiEnvVar] + if (geminiModel) { + return geminiModel + } + + // Fallback to Anthropic DEFAULT variables for backward compatibility const sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL` const resolvedModel = process.env[sharedEnvVar] if (resolvedModel) { @@ -24,7 +32,6 @@ export function resolveGeminiModel(anthropicModel: string): string { } throw new Error( - `Gemini provider requires GEMINI_MODEL or ${sharedEnvVar} to be configured.`, + `Gemini provider requires GEMINI_MODEL or ${geminiEnvVar} (or ${sharedEnvVar} for backward compatibility) to be configured.`, ) } - diff --git a/src/utils/managedEnvConstants.ts b/src/utils/managedEnvConstants.ts index e1ea48bb5..d1976c114 100644 --- a/src/utils/managedEnvConstants.ts +++ b/src/utils/managedEnvConstants.ts @@ -78,6 +78,20 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([ 'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION', 'CLAUDE_CODE_SUBAGENT_MODEL', 'GEMINI_MODEL', + 'GEMINI_SMALL_FAST_MODEL', + // Gemini provider specific - separate from Anthropic/OpenAI + 'GEMINI_DEFAULT_HAIKU_MODEL', + 'GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION', + 'GEMINI_DEFAULT_HAIKU_MODEL_NAME', + 'GEMINI_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES', + 'GEMINI_DEFAULT_OPUS_MODEL', + 'GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION', + 'GEMINI_DEFAULT_OPUS_MODEL_NAME', + 'GEMINI_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES', + 'GEMINI_DEFAULT_SONNET_MODEL', + 'GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION', + 'GEMINI_DEFAULT_SONNET_MODEL_NAME', + 'GEMINI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', ]) const PROVIDER_MANAGED_ENV_PREFIXES = [ @@ -188,6 +202,19 @@ export const SAFE_ENV_VARS = new Set([ 'CLAUDE_CODE_USE_GEMINI', 'CLAUDE_CODE_USE_VERTEX', 'GEMINI_MODEL', + 'GEMINI_SMALL_FAST_MODEL', + 'GEMINI_DEFAULT_HAIKU_MODEL', + 'GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION', + 'GEMINI_DEFAULT_HAIKU_MODEL_NAME', + 'GEMINI_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES', + 'GEMINI_DEFAULT_OPUS_MODEL', + 'GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION', + 'GEMINI_DEFAULT_OPUS_MODEL_NAME', + 'GEMINI_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES', + 'GEMINI_DEFAULT_SONNET_MODEL', + 'GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION', + 'GEMINI_DEFAULT_SONNET_MODEL_NAME', + 'GEMINI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', 'DISABLE_AUTOUPDATER', 'DISABLE_BUG_COMMAND', 'DISABLE_COST_WARNINGS', diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index c21b0ba73..791daeb6b 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -35,6 +35,15 @@ export type ModelName = string export type ModelSetting = ModelName | ModelAlias | null export function getSmallFastModel(): ModelName { + const provider = getAPIProvider() + // Provider-specific small fast model + if (provider === 'openai' && process.env.OPENAI_SMALL_FAST_MODEL) { + return process.env.OPENAI_SMALL_FAST_MODEL + } + if (provider === 'gemini' && process.env.GEMINI_SMALL_FAST_MODEL) { + return process.env.GEMINI_SMALL_FAST_MODEL + } + // Anthropic-specific or fallback return process.env.ANTHROPIC_SMALL_FAST_MODEL || getDefaultHaikuModel() } @@ -104,10 +113,15 @@ export function getBestModel(): ModelName { // @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged). export function getDefaultOpusModel(): ModelName { + const provider = getAPIProvider() // For OpenAI provider, check OPENAI_DEFAULT_OPUS_MODEL first - if (getAPIProvider() === 'openai' && process.env.OPENAI_DEFAULT_OPUS_MODEL) { + if (provider === 'openai' && process.env.OPENAI_DEFAULT_OPUS_MODEL) { return process.env.OPENAI_DEFAULT_OPUS_MODEL } + // For Gemini provider, check GEMINI_DEFAULT_OPUS_MODEL + if (provider === 'gemini' && process.env.GEMINI_DEFAULT_OPUS_MODEL) { + return process.env.GEMINI_DEFAULT_OPUS_MODEL + } // Anthropic-specific override (for first-party and other 3P providers) if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) { return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL @@ -115,7 +129,7 @@ export function getDefaultOpusModel(): ModelName { // 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch // even when values match, since 3P availability lags firstParty and // these will diverge again at the next model launch. - if (getAPIProvider() !== 'firstParty') { + if (provider !== 'firstParty') { return getModelStrings().opus46 } return getModelStrings().opus46 @@ -123,19 +137,24 @@ export function getDefaultOpusModel(): ModelName { // @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged). export function getDefaultSonnetModel(): ModelName { + const provider = getAPIProvider() // For OpenAI provider, check OPENAI_DEFAULT_SONNET_MODEL first if ( - getAPIProvider() === 'openai' && + provider === 'openai' && process.env.OPENAI_DEFAULT_SONNET_MODEL ) { return process.env.OPENAI_DEFAULT_SONNET_MODEL } + // For Gemini provider, check GEMINI_DEFAULT_SONNET_MODEL + if (provider === 'gemini' && process.env.GEMINI_DEFAULT_SONNET_MODEL) { + return process.env.GEMINI_DEFAULT_SONNET_MODEL + } // Anthropic-specific override (for first-party and other 3P providers) if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) { return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL } // Default to Sonnet 4.5 for 3P since they may not have 4.6 yet - if (getAPIProvider() !== 'firstParty') { + if (provider !== 'firstParty') { return getModelStrings().sonnet45 } return getModelStrings().sonnet46 @@ -143,10 +162,15 @@ export function getDefaultSonnetModel(): ModelName { // @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged). export function getDefaultHaikuModel(): ModelName { + const provider = getAPIProvider() // For OpenAI provider, check OPENAI_DEFAULT_HAIKU_MODEL first - if (getAPIProvider() === 'openai' && process.env.OPENAI_DEFAULT_HAIKU_MODEL) { + if (provider === 'openai' && process.env.OPENAI_DEFAULT_HAIKU_MODEL) { return process.env.OPENAI_DEFAULT_HAIKU_MODEL } + // For Gemini provider, check GEMINI_DEFAULT_HAIKU_MODEL + if (provider === 'gemini' && process.env.GEMINI_DEFAULT_HAIKU_MODEL) { + return process.env.GEMINI_DEFAULT_HAIKU_MODEL + } // Anthropic-specific override (for first-party and other 3P providers) if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) { return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL diff --git a/src/utils/model/modelOptions.ts b/src/utils/model/modelOptions.ts index 33ed77735..6d84a187f 100644 --- a/src/utils/model/modelOptions.ts +++ b/src/utils/model/modelOptions.ts @@ -76,22 +76,29 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption { function getCustomSonnetOption(): ModelOption | undefined { const is3P = getAPIProvider() !== 'firstParty' - // For OpenAI provider, use OPENAI_DEFAULT_SONNET_MODEL; for other 3P, use ANTHROPIC_DEFAULT_SONNET_MODEL + const provider = getAPIProvider() + // Use provider-specific DEFAULT_SONNET_MODEL const customSonnetModel = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_SONNET_MODEL + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_SONNET_MODEL : process.env.ANTHROPIC_DEFAULT_SONNET_MODEL // When a 3P user has a custom sonnet model string, show it directly if (is3P && customSonnetModel) { const is1m = has1mContext(customSonnetModel) // Use appropriate NAME/DESCRIPTION env vars based on provider const nameEnv = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_SONNET_MODEL_NAME + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_SONNET_MODEL_NAME : process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME const descEnv = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION : process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION return { value: 'sonnet', @@ -118,22 +125,29 @@ function getSonnet46Option(): ModelOption { function getCustomOpusOption(): ModelOption | undefined { const is3P = getAPIProvider() !== 'firstParty' - // For OpenAI provider, use OPENAI_DEFAULT_OPUS_MODEL; for other 3P, use ANTHROPIC_DEFAULT_OPUS_MODEL + const provider = getAPIProvider() + // Use provider-specific DEFAULT_OPUS_MODEL const customOpusModel = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_OPUS_MODEL + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_OPUS_MODEL : process.env.ANTHROPIC_DEFAULT_OPUS_MODEL // When a 3P user has a custom opus model string, show it directly if (is3P && customOpusModel) { const is1m = has1mContext(customOpusModel) // Use appropriate NAME/DESCRIPTION env vars based on provider const nameEnv = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_OPUS_MODEL_NAME + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_OPUS_MODEL_NAME : process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME const descEnv = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_OPUS_MODEL_DESCRIPTION + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION : process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION return { value: 'opus', @@ -187,21 +201,28 @@ export function getOpus46_1MOption(fastMode = false): ModelOption { function getCustomHaikuOption(): ModelOption | undefined { const is3P = getAPIProvider() !== 'firstParty' - // For OpenAI provider, use OPENAI_DEFAULT_HAIKU_MODEL; for other 3P, use ANTHROPIC_DEFAULT_HAIKU_MODEL + const provider = getAPIProvider() + // Use provider-specific DEFAULT_HAIKU_MODEL const customHaikuModel = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_HAIKU_MODEL + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_HAIKU_MODEL : process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL // When a 3P user has a custom haiku model string, show it directly if (is3P && customHaikuModel) { // Use appropriate NAME/DESCRIPTION env vars based on provider const nameEnv = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_HAIKU_MODEL_NAME + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_HAIKU_MODEL_NAME : process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME const descEnv = - getAPIProvider() === 'openai' + provider === 'openai' ? process.env.OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION + : provider === 'gemini' + ? process.env.GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION : process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION return { value: 'haiku',