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/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

View File

@@ -127,7 +127,7 @@ TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动
## 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>
## 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_BASE_URL` | 推荐 | 端点 URL`http://localhost:11434/v1` |
| `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_PROJECT_ID` | 可选 | Project ID |
@@ -49,11 +51,12 @@ OPENAI_BASE_URL=https://your-one-api.example.com/v1 \
OPENAI_MODEL=gpt-4o \
bun run dev
# 自定义模型映射
# 自定义模型映射(使用家族变量)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \
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
```
@@ -85,9 +88,10 @@ queryModel() [claude.ts]
`resolveOpenAIModel()` 的解析顺序:
1. `OPENAI_MODEL` 环境变量 → 直接使用,覆盖所有
2. `OPENAI_MODEL_MAP` JSON 查表 → 自定义映射
3. 内置默认映射(见下表
4. 以上都不匹配 → 原名透传
2. `OPENAI_DEFAULT_{FAMILY}_MODEL` 变量(如 `OPENAI_DEFAULT_SONNET_MODEL`)→ 按模型家族覆盖
3. `ANTHROPIC_DEFAULT_{FAMILY}_MODEL` 变量(向后兼容
4. 内置默认映射(见下表)
5. 以上都不匹配 → 原名透传
### 内置模型映射

View File

@@ -150,6 +150,7 @@ import sandboxToggle from './commands/sandbox-toggle/index.js'
import chrome from './commands/chrome/index.js'
import stickers from './commands/stickers/index.js'
import advisor from './commands/advisor.js'
import provider from './commands/provider.js'
import { logError } from './utils/log.js'
import { toError } from './utils/errors.js'
import { logForDebugging } from './utils/debug.js'
@@ -258,6 +259,7 @@ export const INTERNAL_ONLY_COMMANDS = [
const COMMANDS = memoize((): Command[] => [
addDir,
advisor,
provider,
agents,
branch,
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 { Spinner } from './Spinner.js'
import TextInput from './TextInput.js'
import { fi } from 'zod/v4/locales'
type Props = {
onDone(): void
@@ -311,17 +312,10 @@ export function ConsoleOAuthFlow({
!pendingOAuthStartRef.current
) {
pendingOAuthStartRef.current = true
process.nextTick(
(
startOAuth: () => Promise<void>,
pendingOAuthStartRef: React.MutableRefObject<boolean>,
) => {
void startOAuth()
pendingOAuthStartRef.current = false
},
startOAuth,
pendingOAuthStartRef,
)
// Start OAuth flow and reset the pending flag when complete
void startOAuth().finally(() => {
pendingOAuthStartRef.current = false
})
}
}, [oauthStatus.state, startOAuth])
@@ -556,9 +550,9 @@ function OAuthStatusMessage({
state: 'openai_chat_api',
baseUrl: process.env.OPENAI_BASE_URL ?? '',
apiKey: process.env.OPENAI_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.OPENAI_DEFAULT_HAIKU_MODEL ?? '',
sonnetModel: process.env.OPENAI_DEFAULT_SONNET_MODEL ?? '',
opusModel: process.env.OPENAI_DEFAULT_OPUS_MODEL ?? '',
activeField: 'base_url',
})
} else if (value === 'gemini_api') {
@@ -567,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') {
@@ -657,7 +651,30 @@ function OAuthStatusMessage({
const doSave = useCallback(() => {
const finalVals = { ...displayValues, [activeField]: inputValue }
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.haiku_model) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals.haiku_model
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model
@@ -669,14 +686,14 @@ function OAuthStatusMessage({
if (error) {
setOAuthStatus({
state: 'error',
message: `Failed to save: ${error.message}`,
message: 'Failed to save settings. Please try again.',
toRetry: {
state: 'custom_platform',
baseUrl: '',
apiKey: '',
haikuModel: '',
sonnetModel: '',
opusModel: '',
baseUrl: finalVals.base_url ?? '',
apiKey: finalVals.api_key ?? '',
haikuModel: finalVals.haiku_model ?? '',
sonnetModel: finalVals.sonnet_model ?? '',
opusModel: finalVals.opus_model ?? '',
activeField: 'base_url',
},
})
@@ -854,11 +871,34 @@ function OAuthStatusMessage({
const doOpenAISave = useCallback(() => {
const finalVals = { ...openaiDisplayValues, [activeField]: openaiInputValue }
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.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.OPENAI_DEFAULT_HAIKU_MODEL = finalVals.haiku_model
if (finalVals.sonnet_model) env.OPENAI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model
if (finalVals.opus_model) env.OPENAI_DEFAULT_OPUS_MODEL = finalVals.opus_model
const { error } = updateSettingsForSource('userSettings', {
modelType: 'openai' as any,
env,
@@ -866,14 +906,14 @@ function OAuthStatusMessage({
if (error) {
setOAuthStatus({
state: 'error',
message: `Failed to save: ${error.message}`,
message: 'Failed to save settings. Please try again.',
toRetry: {
state: 'openai_chat_api',
baseUrl: '',
apiKey: '',
haikuModel: '',
sonnetModel: '',
opusModel: '',
baseUrl: finalVals.base_url ?? '',
apiKey: finalVals.api_key ?? '',
haikuModel: finalVals.haiku_model ?? '',
sonnetModel: finalVals.sonnet_model ?? '',
opusModel: finalVals.opus_model ?? '',
activeField: 'base_url',
},
})
@@ -1089,9 +1129,9 @@ function OAuthStatusMessage({
const env: Record<string, string> = {}
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,

View File

@@ -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.',
)
})
})

View File

@@ -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.`,
)
}

View File

@@ -4,6 +4,9 @@ import { resolveOpenAIModel } from '../modelMapping.js'
describe('resolveOpenAIModel', () => {
const originalEnv = {
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_SONNET_MODEL: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL,
ANTHROPIC_DEFAULT_OPUS_MODEL: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL,
@@ -11,6 +14,9 @@ describe('resolveOpenAIModel', () => {
beforeEach(() => {
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_SONNET_MODEL
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL

View File

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

View File

@@ -10,6 +10,10 @@
* @[MODEL LAUNCH]: New models usually don't need changes here —
* VERTEX_REGION_CLAUDE_* is prefix-matched. New providers or new routing
* 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([
// 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_NAME',
'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_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 = [
@@ -126,6 +161,19 @@ export const SAFE_ENV_VARS = new Set([
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
'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_MODEL',
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
@@ -154,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',

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

View File

@@ -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,13 +113,23 @@ 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 (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
}
// 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
@@ -118,11 +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 (
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
@@ -130,6 +162,16 @@ 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 (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
}

View File

@@ -76,18 +76,36 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
function getCustomSonnetOption(): ModelOption | undefined {
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
if (is3P && 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 {
value: 'sonnet',
label:
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME ?? customSonnetModel,
label: nameEnv ?? customSonnetModel,
description:
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ??
`Custom Sonnet model${is1m ? ' (1M context)' : ''}`,
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ?? `Custom Sonnet model${is1m ? ' with 1M context' : ''}`} (${customSonnetModel})`,
descEnv ?? `Custom Sonnet model${is1m ? ' (1M context)' : ''}`,
descriptionForModel: `${descEnv ?? `Custom Sonnet model${is1m ? ' with 1M context' : ''}`} (${customSonnetModel})`,
}
}
}
@@ -107,17 +125,35 @@ function getSonnet46Option(): ModelOption {
function getCustomOpusOption(): ModelOption | undefined {
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
if (is3P && 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 {
value: 'opus',
label: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME ?? customOpusModel,
description:
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ??
`Custom Opus model${is1m ? ' (1M context)' : ''}`,
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ?? `Custom Opus model${is1m ? ' with 1M context' : ''}`} (${customOpusModel})`,
label: nameEnv ?? customOpusModel,
description: descEnv ?? `Custom Opus model${is1m ? ' (1M context)' : ''}`,
descriptionForModel: `${descEnv ?? `Custom Opus model${is1m ? ' with 1M context' : ''}`} (${customOpusModel})`,
}
}
}
@@ -165,16 +201,34 @@ export function getOpus46_1MOption(fastMode = false): ModelOption {
function getCustomHaikuOption(): ModelOption | undefined {
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
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 {
value: 'haiku',
label: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME ?? customHaikuModel,
description:
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ??
'Custom Haiku model',
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ?? 'Custom Haiku model'} (${customHaikuModel})`,
label: nameEnv ?? customHaikuModel,
description: descEnv ?? 'Custom Haiku model',
descriptionForModel: `${descEnv ?? 'Custom Haiku model'} (${customHaikuModel})`,
}
}
}

View File

@@ -8,7 +8,7 @@ export type ModelCapabilityOverride =
| 'adaptive_thinking'
| 'interleaved_thinking'
const TIERS = [
const ANTHROPIC_TIERS = [
{
modelEnvVar: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
capabilitiesEnvVar: 'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
@@ -23,9 +23,24 @@ const TIERS = [
},
] 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
* the pinned ANTHROPIC_DEFAULT_*_MODEL env vars.
* the pinned ANTHROPIC_DEFAULT_*_MODEL or OPENAI_DEFAULT_*_MODEL env vars.
*/
export const get3PModelCapabilityOverride = memoize(
(model: string, capability: ModelCapabilityOverride): boolean | undefined => {
@@ -33,7 +48,9 @@ export const get3PModelCapabilityOverride = memoize(
return undefined
}
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 capabilities = process.env[tier.capabilitiesEnvVar]
if (!pinned || capabilities === undefined) continue

View File

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