mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Merge pull request #129 from 2228293026/main
provider (api) 指令查询/切换模型api类型 分离OpenAl和Anthropic模型的环境变量 分离 Gemini和Anthropic环境变量 通过github action更新contributors
This commit is contained in:
31
.github/workflows/update-contributors.yml
vendored
Normal file
31
.github/workflows/update-contributors.yml
vendored
Normal 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
|
||||
33
CLAUDE.md
33
CLAUDE.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
34
contributors.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 658 KiB |
@@ -14,7 +14,9 @@ claude-code 支持通过 OpenAI Chat Completions API(`/v1/chat/completions`)
|
||||
| `OPENAI_API_KEY` | 是 | API key(Ollama 等可设为任意值) |
|
||||
| `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. 以上都不匹配 → 原名透传
|
||||
|
||||
### 内置模型映射
|
||||
|
||||
|
||||
@@ -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
146
src/commands/provider.ts
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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.`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user