Merge pull request #129 from 2228293026/main

provider  (api) 指令查询/切换模型api类型 分离OpenAl和Anthropic模型的环境变量 分离 Gemini和Anthropic环境变量 通过github action更新contributors
This commit is contained in:
claude-code-best
2026-04-06 14:18:47 +08:00
committed by GitHub
18 changed files with 614 additions and 104 deletions

View File

@@ -0,0 +1,31 @@
name: Update Contributors
on:
push:
branches:
- main
schedule:
- cron: '0 0 * * *' # 每天更新一次
permissions:
contents: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: jaywcjlove/github-action-contributors@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
output: "contributors.svg"
repository: ${{ github.repository }}
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: update contributors"
file_pattern: "contributors.svg"
branch: main

View File

@@ -180,7 +180,38 @@ Feature flag `VOICE_MODE`dev/build 默认启用。Push-to-Talk 语音输入
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射 - **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
- **`src/utils/model/providers.ts`** — 添加 `'openai'` provider 类型(最高优先级) - **`src/utils/model/providers.ts`** — 添加 `'openai'` provider 类型(最高优先级)
关键环境变量:`CLAUDE_CODE_USE_OPENAI``OPENAI_API_KEY``OPENAI_BASE_URL``OPENAI_MODEL``OPENAI_MODEL_MAP`。详见 `docs/plans/openai-compatibility.md` 关键环境变量:`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 ### Key Type Files

View File

@@ -127,7 +127,7 @@ TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动
## Contributors ## Contributors
<a href="https://github.com/claude-code-best/claude-code/graphs/contributors"> <a href="https://github.com/claude-code-best/claude-code/graphs/contributors">
<img src="https://contrib.rocks/image?repo=claude-code-best/claude-code" /> <img src="contributors.svg" alt="Contributors" />
</a> </a>
## Star History ## Star History

34
contributors.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 658 KiB

View File

@@ -14,7 +14,9 @@ claude-code 支持通过 OpenAI Chat Completions API`/v1/chat/completions`
| `OPENAI_API_KEY` | 是 | API keyOllama 等可设为任意值) | | `OPENAI_API_KEY` | 是 | API keyOllama 等可设为任意值) |
| `OPENAI_BASE_URL` | 推荐 | 端点 URL`http://localhost:11434/v1` | | `OPENAI_BASE_URL` | 推荐 | 端点 URL`http://localhost:11434/v1` |
| `OPENAI_MODEL` | 可选 | 覆盖所有请求的模型名(跳过映射) | | `OPENAI_MODEL` | 可选 | 覆盖所有请求的模型名(跳过映射) |
| `OPENAI_MODEL_MAP` | 可选 | JSON 映射,如 `{"claude-sonnet-4-6":"gpt-4o"}` | | `OPENAI_DEFAULT_OPUS_MODEL` | 可选 | 覆盖 opus 家族对应的模型(如 `o3`, `o3-mini`, `o1-pro` |
| `OPENAI_DEFAULT_SONNET_MODEL` | 可选 | 覆盖 sonnet 家族对应的模型(如 `gpt-4o`, `gpt-4.1` |
| `OPENAI_DEFAULT_HAIKU_MODEL` | 可选 | 覆盖 haiku 家族对应的模型(如 `gpt-4o-mini`, `gpt-4.0-mini` |
| `OPENAI_ORG_ID` | 可选 | Organization ID | | `OPENAI_ORG_ID` | 可选 | Organization ID |
| `OPENAI_PROJECT_ID` | 可选 | Project ID | | `OPENAI_PROJECT_ID` | 可选 | Project ID |
@@ -49,11 +51,12 @@ OPENAI_BASE_URL=https://your-one-api.example.com/v1 \
OPENAI_MODEL=gpt-4o \ OPENAI_MODEL=gpt-4o \
bun run dev bun run dev
# 自定义模型映射 # 自定义模型映射(使用家族变量)
CLAUDE_CODE_USE_OPENAI=1 \ CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \ OPENAI_API_KEY=sk-xxx \
OPENAI_BASE_URL=https://my-gateway.example.com/v1 \ OPENAI_BASE_URL=https://my-gateway.example.com/v1 \
OPENAI_MODEL_MAP='{"claude-sonnet-4-6":"gpt-4o-2024-11-20","claude-haiku-4-5":"gpt-4o-mini"}' \ OPENAI_DEFAULT_SONNET_MODEL="gpt-4o-2024-11-20" \
OPENAI_DEFAULT_HAIKU_MODEL="gpt-4o-mini" \
bun run dev bun run dev
``` ```
@@ -85,9 +88,10 @@ queryModel() [claude.ts]
`resolveOpenAIModel()` 的解析顺序: `resolveOpenAIModel()` 的解析顺序:
1. `OPENAI_MODEL` 环境变量 → 直接使用,覆盖所有 1. `OPENAI_MODEL` 环境变量 → 直接使用,覆盖所有
2. `OPENAI_MODEL_MAP` JSON 查表 → 自定义映射 2. `OPENAI_DEFAULT_{FAMILY}_MODEL` 变量(如 `OPENAI_DEFAULT_SONNET_MODEL`)→ 按模型家族覆盖
3. 内置默认映射(见下表 3. `ANTHROPIC_DEFAULT_{FAMILY}_MODEL` 变量(向后兼容
4. 以上都不匹配 → 原名透传 4. 内置默认映射(见下表)
5. 以上都不匹配 → 原名透传
### 内置模型映射 ### 内置模型映射

View File

@@ -150,6 +150,7 @@ import sandboxToggle from './commands/sandbox-toggle/index.js'
import chrome from './commands/chrome/index.js' import chrome from './commands/chrome/index.js'
import stickers from './commands/stickers/index.js' import stickers from './commands/stickers/index.js'
import advisor from './commands/advisor.js' import advisor from './commands/advisor.js'
import provider from './commands/provider.js'
import { logError } from './utils/log.js' import { logError } from './utils/log.js'
import { toError } from './utils/errors.js' import { toError } from './utils/errors.js'
import { logForDebugging } from './utils/debug.js' import { logForDebugging } from './utils/debug.js'
@@ -258,6 +259,7 @@ export const INTERNAL_ONLY_COMMANDS = [
const COMMANDS = memoize((): Command[] => [ const COMMANDS = memoize((): Command[] => [
addDir, addDir,
advisor, advisor,
provider,
agents, agents,
branch, branch,
btw, btw,

146
src/commands/provider.ts Normal file
View File

@@ -0,0 +1,146 @@
import type { Command } from '../commands.js'
import type { LocalCommandCall } from '../types/command.js'
import { getAPIProvider } from '../utils/model/providers.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'
import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
import { applyConfigEnvironmentVariables } from '../utils/managedEnv.js'
function getEnvVarForProvider(provider: string): string {
switch (provider) {
case 'bedrock':
return 'CLAUDE_CODE_USE_BEDROCK'
case 'vertex':
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}`)
}
}
// Get merged env: process.env + settings.env (from userSettings)
function getMergedEnv(): Record<string, string> {
const settings = getSettings_DEPRECATED()
const merged = { ...process.env }
if (settings?.env) {
Object.assign(merged, settings.env)
}
return merged
}
const call: LocalCommandCall = async (args, context) => {
const arg = args.trim().toLowerCase()
// No argument: show current provider
if (!arg) {
const current = getAPIProvider()
return { type: 'text', value: `Current API provider: ${current}` }
}
// unset - clear settings, fallback to env vars
if (arg === 'unset') {
updateSettingsForSource('userSettings', { modelType: undefined })
// Also clear all provider-specific env vars to prevent 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
return {
type: 'text',
value: 'API provider cleared (will use environment variables).',
}
}
// Validate provider
const validProviders = [
'anthropic',
'openai',
'gemini',
'bedrock',
'vertex',
'foundry',
]
if (!validProviders.includes(arg)) {
return {
type: 'text',
value: `Invalid provider: ${arg}\nValid: ${validProviders.join(', ')}`,
}
}
// Check env vars when switching to openai (including settings.env)
if (arg === 'openai') {
const mergedEnv = getMergedEnv()
const hasKey = !!mergedEnv.OPENAI_API_KEY
const hasUrl = !!mergedEnv.OPENAI_BASE_URL
if (!hasKey || !hasUrl) {
updateSettingsForSource('userSettings', { modelType: 'openai' })
const missing = []
if (!hasKey) missing.push('OPENAI_API_KEY')
if (!hasUrl) missing.push('OPENAI_BASE_URL')
return {
type: 'text',
value: `Switched to OpenAI provider.\nWarning: Missing env vars: ${missing.join(', ')}\nConfigure them via /login or set manually.`,
}
}
}
// 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', 'openai', 'gemini' are stored in settings.json (persistent)
// - 'bedrock', 'vertex', 'foundry' are env-only (do NOT touch settings.json)
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.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).`,
}
}
}
const provider = {
type: 'local',
name: 'provider',
description:
'Switch API provider (anthropic/openai/gemini/bedrock/vertex/foundry)',
aliases: ['api'],
argumentHint: '[anthropic|openai|gemini|bedrock|vertex|foundry|unset]',
supportsNonInteractive: true,
load: () => Promise.resolve({ call }),
} satisfies Command
export default provider

View File

@@ -19,6 +19,7 @@ import { Select } from './CustomSelect/select.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
import { Spinner } from './Spinner.js' import { Spinner } from './Spinner.js'
import TextInput from './TextInput.js' import TextInput from './TextInput.js'
import { fi } from 'zod/v4/locales'
type Props = { type Props = {
onDone(): void onDone(): void
@@ -311,17 +312,10 @@ export function ConsoleOAuthFlow({
!pendingOAuthStartRef.current !pendingOAuthStartRef.current
) { ) {
pendingOAuthStartRef.current = true pendingOAuthStartRef.current = true
process.nextTick( // Start OAuth flow and reset the pending flag when complete
( void startOAuth().finally(() => {
startOAuth: () => Promise<void>, pendingOAuthStartRef.current = false
pendingOAuthStartRef: React.MutableRefObject<boolean>, })
) => {
void startOAuth()
pendingOAuthStartRef.current = false
},
startOAuth,
pendingOAuthStartRef,
)
} }
}, [oauthStatus.state, startOAuth]) }, [oauthStatus.state, startOAuth])
@@ -556,9 +550,9 @@ function OAuthStatusMessage({
state: 'openai_chat_api', state: 'openai_chat_api',
baseUrl: process.env.OPENAI_BASE_URL ?? '', baseUrl: process.env.OPENAI_BASE_URL ?? '',
apiKey: process.env.OPENAI_API_KEY ?? '', apiKey: process.env.OPENAI_API_KEY ?? '',
haikuModel: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '', haikuModel: process.env.OPENAI_DEFAULT_HAIKU_MODEL ?? '',
sonnetModel: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? '', sonnetModel: process.env.OPENAI_DEFAULT_SONNET_MODEL ?? '',
opusModel: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? '', opusModel: process.env.OPENAI_DEFAULT_OPUS_MODEL ?? '',
activeField: 'base_url', activeField: 'base_url',
}) })
} else if (value === 'gemini_api') { } else if (value === 'gemini_api') {
@@ -567,9 +561,9 @@ function OAuthStatusMessage({
state: 'gemini_api', state: 'gemini_api',
baseUrl: process.env.GEMINI_BASE_URL ?? '', baseUrl: process.env.GEMINI_BASE_URL ?? '',
apiKey: process.env.GEMINI_API_KEY ?? '', apiKey: process.env.GEMINI_API_KEY ?? '',
haikuModel: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '', haikuModel: process.env.GEMINI_DEFAULT_HAIKU_MODEL ?? '',
sonnetModel: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? '', sonnetModel: process.env.GEMINI_DEFAULT_SONNET_MODEL ?? '',
opusModel: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? '', opusModel: process.env.GEMINI_DEFAULT_OPUS_MODEL ?? '',
activeField: 'base_url', activeField: 'base_url',
}) })
} else if (value === 'platform') { } else if (value === 'platform') {
@@ -657,7 +651,30 @@ function OAuthStatusMessage({
const doSave = useCallback(() => { const doSave = useCallback(() => {
const finalVals = { ...displayValues, [activeField]: inputValue } const finalVals = { ...displayValues, [activeField]: inputValue }
const env: Record<string, string> = {} const env: Record<string, string> = {}
if (finalVals.base_url) env.ANTHROPIC_BASE_URL = finalVals.base_url
// Validate base_url if provided
if (finalVals.base_url) {
try {
new URL(finalVals.base_url)
} catch {
setOAuthStatus({
state: 'error',
message: 'Invalid base URL: please enter a full URL including protocol (e.g., https://api.example.com)',
toRetry: {
state: 'custom_platform',
baseUrl: '',
apiKey: '',
haikuModel: '',
sonnetModel: '',
opusModel: '',
activeField: 'base_url',
},
})
return
}
env.ANTHROPIC_BASE_URL = finalVals.base_url
}
if (finalVals.api_key) env.ANTHROPIC_AUTH_TOKEN = finalVals.api_key if (finalVals.api_key) env.ANTHROPIC_AUTH_TOKEN = finalVals.api_key
if (finalVals.haiku_model) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals.haiku_model 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.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model
@@ -669,14 +686,14 @@ function OAuthStatusMessage({
if (error) { if (error) {
setOAuthStatus({ setOAuthStatus({
state: 'error', state: 'error',
message: `Failed to save: ${error.message}`, message: 'Failed to save settings. Please try again.',
toRetry: { toRetry: {
state: 'custom_platform', state: 'custom_platform',
baseUrl: '', baseUrl: finalVals.base_url ?? '',
apiKey: '', apiKey: finalVals.api_key ?? '',
haikuModel: '', haikuModel: finalVals.haiku_model ?? '',
sonnetModel: '', sonnetModel: finalVals.sonnet_model ?? '',
opusModel: '', opusModel: finalVals.opus_model ?? '',
activeField: 'base_url', activeField: 'base_url',
}, },
}) })
@@ -854,11 +871,34 @@ function OAuthStatusMessage({
const doOpenAISave = useCallback(() => { const doOpenAISave = useCallback(() => {
const finalVals = { ...openaiDisplayValues, [activeField]: openaiInputValue } const finalVals = { ...openaiDisplayValues, [activeField]: openaiInputValue }
const env: Record<string, string> = {} const env: Record<string, string> = {}
if (finalVals.base_url) env.OPENAI_BASE_URL = finalVals.base_url
// Validate base_url if provided
if (finalVals.base_url) {
try {
new URL(finalVals.base_url)
} catch {
setOAuthStatus({
state: 'error',
message: 'Invalid base URL: please enter a full URL including protocol (e.g., https://api.example.com)',
toRetry: {
state: 'openai_chat_api',
baseUrl: '',
apiKey: '',
haikuModel: '',
sonnetModel: '',
opusModel: '',
activeField: 'base_url',
},
})
return
}
env.OPENAI_BASE_URL = finalVals.base_url
}
if (finalVals.api_key) env.OPENAI_API_KEY = finalVals.api_key if (finalVals.api_key) env.OPENAI_API_KEY = finalVals.api_key
if (finalVals.haiku_model) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals.haiku_model if (finalVals.haiku_model) env.OPENAI_DEFAULT_HAIKU_MODEL = finalVals.haiku_model
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model if (finalVals.sonnet_model) env.OPENAI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model if (finalVals.opus_model) env.OPENAI_DEFAULT_OPUS_MODEL = finalVals.opus_model
const { error } = updateSettingsForSource('userSettings', { const { error } = updateSettingsForSource('userSettings', {
modelType: 'openai' as any, modelType: 'openai' as any,
env, env,
@@ -866,14 +906,14 @@ function OAuthStatusMessage({
if (error) { if (error) {
setOAuthStatus({ setOAuthStatus({
state: 'error', state: 'error',
message: `Failed to save: ${error.message}`, message: 'Failed to save settings. Please try again.',
toRetry: { toRetry: {
state: 'openai_chat_api', state: 'openai_chat_api',
baseUrl: '', baseUrl: finalVals.base_url ?? '',
apiKey: '', apiKey: finalVals.api_key ?? '',
haikuModel: '', haikuModel: finalVals.haiku_model ?? '',
sonnetModel: '', sonnetModel: finalVals.sonnet_model ?? '',
opusModel: '', opusModel: finalVals.opus_model ?? '',
activeField: 'base_url', activeField: 'base_url',
}, },
}) })
@@ -1089,9 +1129,9 @@ function OAuthStatusMessage({
const env: Record<string, string> = {} const env: Record<string, string> = {}
if (finalVals.base_url) env.GEMINI_BASE_URL = finalVals.base_url if (finalVals.base_url) env.GEMINI_BASE_URL = finalVals.base_url
if (finalVals.api_key) env.GEMINI_API_KEY = finalVals.api_key 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.haiku_model) env.GEMINI_DEFAULT_HAIKU_MODEL = finalVals.haiku_model
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model
const { error } = updateSettingsForSource('userSettings', { const { error } = updateSettingsForSource('userSettings', {
modelType: 'gemini' as any, modelType: 'gemini' as any,
env, env,

View File

@@ -4,6 +4,9 @@ import { resolveGeminiModel } from '../modelMapping.js'
describe('resolveGeminiModel', () => { describe('resolveGeminiModel', () => {
const originalEnv = { const originalEnv = {
GEMINI_MODEL: process.env.GEMINI_MODEL, 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_HAIKU_MODEL: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
ANTHROPIC_DEFAULT_SONNET_MODEL: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL,
ANTHROPIC_DEFAULT_OPUS_MODEL: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL,
@@ -11,6 +14,9 @@ describe('resolveGeminiModel', () => {
beforeEach(() => { beforeEach(() => {
delete process.env.GEMINI_MODEL 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_HAIKU_MODEL
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
delete process.env.ANTHROPIC_DEFAULT_OPUS_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') 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' process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash'
expect(resolveGeminiModel('claude-sonnet-4-6')).toBe('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' process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = 'gemini-2.5-flash-lite'
expect(resolveGeminiModel('claude-haiku-4-5-20251001')).toBe( expect(resolveGeminiModel('claude-haiku-4-5-20251001')).toBe(
'gemini-2.5-flash-lite', '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' process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'gemini-2.5-pro'
expect(resolveGeminiModel('claude-opus-4-6')).toBe('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' process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'legacy-gemini-sonnet'
expect(resolveGeminiModel('claude-sonnet-4-6')).toBe( expect(resolveGeminiModel('claude-sonnet-4-6')).toBe('legacy-gemini-sonnet')
'legacy-gemini-sonnet',
)
}) })
test('strips [1m] suffix before resolving', () => { test('strips [1m] suffix before resolving', () => {
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash' process.env.GEMINI_DEFAULT_SONNET_MODEL = 'gemini-2.5-flash'
expect(resolveGeminiModel('claude-sonnet-4-6[1m]')).toBe( expect(resolveGeminiModel('claude-sonnet-4-6[1m]')).toBe('gemini-2.5-flash')
'gemini-2.5-flash',
)
}) })
test('passes through explicit Gemini model names', () => { 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( 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.',
) )
}) })
}) })

View File

@@ -17,6 +17,14 @@ export function resolveGeminiModel(anthropicModel: string): string {
return cleanModel 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 sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
const resolvedModel = process.env[sharedEnvVar] const resolvedModel = process.env[sharedEnvVar]
if (resolvedModel) { if (resolvedModel) {
@@ -24,7 +32,6 @@ export function resolveGeminiModel(anthropicModel: string): string {
} }
throw new Error( 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.`,
) )
} }

View File

@@ -4,6 +4,9 @@ import { resolveOpenAIModel } from '../modelMapping.js'
describe('resolveOpenAIModel', () => { describe('resolveOpenAIModel', () => {
const originalEnv = { const originalEnv = {
OPENAI_MODEL: process.env.OPENAI_MODEL, OPENAI_MODEL: process.env.OPENAI_MODEL,
OPENAI_DEFAULT_HAIKU_MODEL: process.env.OPENAI_DEFAULT_HAIKU_MODEL,
OPENAI_DEFAULT_SONNET_MODEL: process.env.OPENAI_DEFAULT_SONNET_MODEL,
OPENAI_DEFAULT_OPUS_MODEL: process.env.OPENAI_DEFAULT_OPUS_MODEL,
ANTHROPIC_DEFAULT_HAIKU_MODEL: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
ANTHROPIC_DEFAULT_SONNET_MODEL: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL,
ANTHROPIC_DEFAULT_OPUS_MODEL: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL,
@@ -11,6 +14,9 @@ describe('resolveOpenAIModel', () => {
beforeEach(() => { beforeEach(() => {
delete process.env.OPENAI_MODEL delete process.env.OPENAI_MODEL
delete process.env.OPENAI_DEFAULT_HAIKU_MODEL
delete process.env.OPENAI_DEFAULT_SONNET_MODEL
delete process.env.OPENAI_DEFAULT_OPUS_MODEL
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL

View File

@@ -31,9 +31,10 @@ function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
* *
* Priority: * Priority:
* 1. OPENAI_MODEL env var (override all) * 1. OPENAI_MODEL env var (override all)
* 2. ANTHROPIC_DEFAULT_{FAMILY}_MODEL env var (e.g. ANTHROPIC_DEFAULT_SONNET_MODEL) * 2. OPENAI_DEFAULT_{FAMILY}_MODEL env var (e.g. OPENAI_DEFAULT_SONNET_MODEL)
* 3. DEFAULT_MODEL_MAP lookup * 3. ANTHROPIC_DEFAULT_{FAMILY}_MODEL env var (backward compatibility)
* 4. Pass through original model name * 4. DEFAULT_MODEL_MAP lookup
* 5. Pass through original model name
*/ */
export function resolveOpenAIModel(anthropicModel: string): string { export function resolveOpenAIModel(anthropicModel: string): string {
// Highest priority: explicit override // Highest priority: explicit override
@@ -44,12 +45,18 @@ export function resolveOpenAIModel(anthropicModel: string): string {
// Strip [1m] suffix if present (Claude-specific modifier) // Strip [1m] suffix if present (Claude-specific modifier)
const cleanModel = anthropicModel.replace(/\[1m\]$/, '') const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
// Check ANTHROPIC_DEFAULT_*_MODEL env vars based on model family // Check family-specific overrides
const family = getModelFamily(cleanModel) const family = getModelFamily(cleanModel)
if (family) { if (family) {
const envVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL` // OpenAI-specific family override (preferred for openai provider)
const override = process.env[envVar] const openaiEnvVar = `OPENAI_DEFAULT_${family.toUpperCase()}_MODEL`
if (override) return override const openaiOverride = process.env[openaiEnvVar]
if (openaiOverride) return openaiOverride
// Anthropic env var (backward compatibility)
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
const anthropicOverride = process.env[anthropicEnvVar]
if (anthropicOverride) return anthropicOverride
} }
return DEFAULT_MODEL_MAP[cleanModel] ?? cleanModel return DEFAULT_MODEL_MAP[cleanModel] ?? cleanModel

View File

@@ -10,6 +10,10 @@
* @[MODEL LAUNCH]: New models usually don't need changes here — * @[MODEL LAUNCH]: New models usually don't need changes here —
* VERTEX_REGION_CLAUDE_* is prefix-matched. New providers or new routing * VERTEX_REGION_CLAUDE_* is prefix-matched. New providers or new routing
* config vars (endpoint, project, region, auth) do. * config vars (endpoint, project, region, auth) do.
*
* Note: OpenAI provider uses OPENAI_* env vars (OPENAI_API_KEY, OPENAI_BASE_URL,
* OPENAI_MODEL, OPENAI_DEFAULT_*_MODEL, OPENAI_SMALL_FAST_MODEL) which are all
* provider-managed to keep routing config isolated from Anthropic settings.
*/ */
const PROVIDER_MANAGED_ENV_VARS = new Set([ const PROVIDER_MANAGED_ENV_VARS = new Set([
// The flag itself — settings can't unset it once the host set it // The flag itself — settings can't unset it once the host set it
@@ -53,10 +57,41 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION', 'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME', 'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', 'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
// OpenAI provider specific
'OPENAI_API_KEY',
'OPENAI_BASE_URL',
'OPENAI_MODEL',
'OPENAI_DEFAULT_HAIKU_MODEL',
'OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION',
'OPENAI_DEFAULT_HAIKU_MODEL_NAME',
'OPENAI_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'OPENAI_DEFAULT_OPUS_MODEL',
'OPENAI_DEFAULT_OPUS_MODEL_DESCRIPTION',
'OPENAI_DEFAULT_OPUS_MODEL_NAME',
'OPENAI_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
'OPENAI_DEFAULT_SONNET_MODEL',
'OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION',
'OPENAI_DEFAULT_SONNET_MODEL_NAME',
'OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
'OPENAI_SMALL_FAST_MODEL',
'ANTHROPIC_SMALL_FAST_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL',
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION', 'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
'CLAUDE_CODE_SUBAGENT_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL',
'GEMINI_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 = [ const PROVIDER_MANAGED_ENV_PREFIXES = [
@@ -126,6 +161,19 @@ export const SAFE_ENV_VARS = new Set([
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION', 'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME', 'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', 'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
// OpenAI provider specific
'OPENAI_DEFAULT_HAIKU_MODEL',
'OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION',
'OPENAI_DEFAULT_HAIKU_MODEL_NAME',
'OPENAI_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'OPENAI_DEFAULT_OPUS_MODEL',
'OPENAI_DEFAULT_OPUS_MODEL_DESCRIPTION',
'OPENAI_DEFAULT_OPUS_MODEL_NAME',
'OPENAI_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
'OPENAI_DEFAULT_SONNET_MODEL',
'OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION',
'OPENAI_DEFAULT_SONNET_MODEL_NAME',
'OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_FOUNDRY_API_KEY', 'ANTHROPIC_FOUNDRY_API_KEY',
'ANTHROPIC_MODEL', 'ANTHROPIC_MODEL',
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION', 'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
@@ -154,6 +202,19 @@ export const SAFE_ENV_VARS = new Set([
'CLAUDE_CODE_USE_GEMINI', 'CLAUDE_CODE_USE_GEMINI',
'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_VERTEX',
'GEMINI_MODEL', '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_AUTOUPDATER',
'DISABLE_BUG_COMMAND', 'DISABLE_BUG_COMMAND',
'DISABLE_COST_WARNINGS', 'DISABLE_COST_WARNINGS',

View File

@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mock } from "bun:test";
let mockedModelType: "gemini" | undefined; let mockedModelType: "gemini" | undefined;
@@ -16,10 +17,13 @@ describe("getAPIProvider", () => {
"CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_VERTEX",
"CLAUDE_CODE_USE_FOUNDRY", "CLAUDE_CODE_USE_FOUNDRY",
"CLAUDE_CODE_USE_OPENAI",
] as const; ] as const;
const savedEnv: Record<string, string | undefined> = {}; const savedEnv: Record<string, string | undefined> = {};
beforeEach(() => { beforeEach(() => {
// Save and clear environment variables
mockedModelType = undefined; mockedModelType = undefined;
for (const key of envKeys) { for (const key of envKeys) {
savedEnv[key] = process.env[key]; savedEnv[key] = process.env[key];
@@ -28,6 +32,7 @@ describe("getAPIProvider", () => {
}); });
afterEach(() => { afterEach(() => {
// Restore environment variables
mockedModelType = undefined; mockedModelType = undefined;
for (const key of envKeys) { for (const key of envKeys) {
if (savedEnv[key] !== undefined) { if (savedEnv[key] !== undefined) {

View File

@@ -35,6 +35,15 @@ export type ModelName = string
export type ModelSetting = ModelName | ModelAlias | null export type ModelSetting = ModelName | ModelAlias | null
export function getSmallFastModel(): ModelName { 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() return process.env.ANTHROPIC_SMALL_FAST_MODEL || getDefaultHaikuModel()
} }
@@ -104,13 +113,23 @@ export function getBestModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged). // @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged).
export function getDefaultOpusModel(): ModelName { export function getDefaultOpusModel(): ModelName {
const provider = getAPIProvider()
// For OpenAI provider, check OPENAI_DEFAULT_OPUS_MODEL first
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) { if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
} }
// 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch // 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch
// even when values match, since 3P availability lags firstParty and // even when values match, since 3P availability lags firstParty and
// these will diverge again at the next model launch. // these will diverge again at the next model launch.
if (getAPIProvider() !== 'firstParty') { if (provider !== 'firstParty') {
return getModelStrings().opus46 return getModelStrings().opus46
} }
return getModelStrings().opus46 return getModelStrings().opus46
@@ -118,11 +137,24 @@ export function getDefaultOpusModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged). // @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
export function getDefaultSonnetModel(): ModelName { export function getDefaultSonnetModel(): ModelName {
const provider = getAPIProvider()
// For OpenAI provider, check OPENAI_DEFAULT_SONNET_MODEL first
if (
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) { if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return 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 // 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().sonnet45
} }
return getModelStrings().sonnet46 return getModelStrings().sonnet46
@@ -130,6 +162,16 @@ export function getDefaultSonnetModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged). // @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged).
export function getDefaultHaikuModel(): ModelName { export function getDefaultHaikuModel(): ModelName {
const provider = getAPIProvider()
// For OpenAI provider, check OPENAI_DEFAULT_HAIKU_MODEL first
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) { if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
} }

View File

@@ -76,18 +76,36 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
function getCustomSonnetOption(): ModelOption | undefined { function getCustomSonnetOption(): ModelOption | undefined {
const is3P = getAPIProvider() !== 'firstParty' const is3P = getAPIProvider() !== 'firstParty'
const customSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL const provider = getAPIProvider()
// Use provider-specific DEFAULT_SONNET_MODEL
const customSonnetModel =
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 // When a 3P user has a custom sonnet model string, show it directly
if (is3P && customSonnetModel) { if (is3P && customSonnetModel) {
const is1m = has1mContext(customSonnetModel) const is1m = has1mContext(customSonnetModel)
// Use appropriate NAME/DESCRIPTION env vars based on provider
const nameEnv =
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 =
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 { return {
value: 'sonnet', value: 'sonnet',
label: label: nameEnv ?? customSonnetModel,
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME ?? customSonnetModel,
description: description:
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ?? descEnv ?? `Custom Sonnet model${is1m ? ' (1M context)' : ''}`,
`Custom Sonnet model${is1m ? ' (1M context)' : ''}`, descriptionForModel: `${descEnv ?? `Custom Sonnet model${is1m ? ' with 1M context' : ''}`} (${customSonnetModel})`,
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ?? `Custom Sonnet model${is1m ? ' with 1M context' : ''}`} (${customSonnetModel})`,
} }
} }
} }
@@ -107,17 +125,35 @@ function getSonnet46Option(): ModelOption {
function getCustomOpusOption(): ModelOption | undefined { function getCustomOpusOption(): ModelOption | undefined {
const is3P = getAPIProvider() !== 'firstParty' const is3P = getAPIProvider() !== 'firstParty'
const customOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL const provider = getAPIProvider()
// Use provider-specific DEFAULT_OPUS_MODEL
const customOpusModel =
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 // When a 3P user has a custom opus model string, show it directly
if (is3P && customOpusModel) { if (is3P && customOpusModel) {
const is1m = has1mContext(customOpusModel) const is1m = has1mContext(customOpusModel)
// Use appropriate NAME/DESCRIPTION env vars based on provider
const nameEnv =
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 =
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 { return {
value: 'opus', value: 'opus',
label: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME ?? customOpusModel, label: nameEnv ?? customOpusModel,
description: description: descEnv ?? `Custom Opus model${is1m ? ' (1M context)' : ''}`,
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ?? descriptionForModel: `${descEnv ?? `Custom Opus model${is1m ? ' with 1M context' : ''}`} (${customOpusModel})`,
`Custom Opus model${is1m ? ' (1M context)' : ''}`,
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ?? `Custom Opus model${is1m ? ' with 1M context' : ''}`} (${customOpusModel})`,
} }
} }
} }
@@ -165,16 +201,34 @@ export function getOpus46_1MOption(fastMode = false): ModelOption {
function getCustomHaikuOption(): ModelOption | undefined { function getCustomHaikuOption(): ModelOption | undefined {
const is3P = getAPIProvider() !== 'firstParty' const is3P = getAPIProvider() !== 'firstParty'
const customHaikuModel = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL const provider = getAPIProvider()
// Use provider-specific DEFAULT_HAIKU_MODEL
const customHaikuModel =
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 // When a 3P user has a custom haiku model string, show it directly
if (is3P && customHaikuModel) { if (is3P && customHaikuModel) {
// Use appropriate NAME/DESCRIPTION env vars based on provider
const nameEnv =
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 =
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 { return {
value: 'haiku', value: 'haiku',
label: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME ?? customHaikuModel, label: nameEnv ?? customHaikuModel,
description: description: descEnv ?? 'Custom Haiku model',
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ?? descriptionForModel: `${descEnv ?? 'Custom Haiku model'} (${customHaikuModel})`,
'Custom Haiku model',
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ?? 'Custom Haiku model'} (${customHaikuModel})`,
} }
} }
} }

View File

@@ -8,7 +8,7 @@ export type ModelCapabilityOverride =
| 'adaptive_thinking' | 'adaptive_thinking'
| 'interleaved_thinking' | 'interleaved_thinking'
const TIERS = [ const ANTHROPIC_TIERS = [
{ {
modelEnvVar: 'ANTHROPIC_DEFAULT_OPUS_MODEL', modelEnvVar: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
capabilitiesEnvVar: 'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES', capabilitiesEnvVar: 'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
@@ -23,9 +23,24 @@ const TIERS = [
}, },
] as const ] as const
const OPENAI_TIERS = [
{
modelEnvVar: 'OPENAI_DEFAULT_OPUS_MODEL',
capabilitiesEnvVar: 'OPENAI_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
},
{
modelEnvVar: 'OPENAI_DEFAULT_SONNET_MODEL',
capabilitiesEnvVar: 'OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
},
{
modelEnvVar: 'OPENAI_DEFAULT_HAIKU_MODEL',
capabilitiesEnvVar: 'OPENAI_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
},
] as const
/** /**
* Check whether a 3p model capability override is set for a model that matches one of * Check whether a 3p model capability override is set for a model that matches one of
* the pinned ANTHROPIC_DEFAULT_*_MODEL env vars. * the pinned ANTHROPIC_DEFAULT_*_MODEL or OPENAI_DEFAULT_*_MODEL env vars.
*/ */
export const get3PModelCapabilityOverride = memoize( export const get3PModelCapabilityOverride = memoize(
(model: string, capability: ModelCapabilityOverride): boolean | undefined => { (model: string, capability: ModelCapabilityOverride): boolean | undefined => {
@@ -33,7 +48,9 @@ export const get3PModelCapabilityOverride = memoize(
return undefined return undefined
} }
const m = model.toLowerCase() const m = model.toLowerCase()
for (const tier of TIERS) { // Choose the appropriate tier list based on provider
const tiers = getAPIProvider() === 'openai' ? OPENAI_TIERS : ANTHROPIC_TIERS
for (const tier of tiers) {
const pinned = process.env[tier.modelEnvVar] const pinned = process.env[tier.modelEnvVar]
const capabilities = process.env[tier.capabilitiesEnvVar] const capabilities = process.env[tier.capabilitiesEnvVar]
if (!pinned || capabilities === undefined) continue if (!pinned || capabilities === undefined) continue

View File

@@ -11,23 +11,18 @@ export type APIProvider =
| 'gemini' | 'gemini'
export function getAPIProvider(): APIProvider { export function getAPIProvider(): APIProvider {
// 1. Check settings.json modelType field (highest priority)
const modelType = getInitialSettings().modelType const modelType = getInitialSettings().modelType
if (modelType === 'openai') return 'openai' if (modelType === 'openai') return 'openai'
if (modelType === 'gemini') return 'gemini' if (modelType === 'gemini') return 'gemini'
// 2. Check environment variables (backward compatibility) if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) return 'bedrock'
return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)) return 'vertex'
? 'bedrock' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)) return 'foundry'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)
? 'vertex' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) return 'openai'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) return 'gemini'
? 'foundry'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) return 'firstParty'
? 'openai'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
? 'gemini'
: 'firstParty'
} }
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS { export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {