diff --git a/bun.lock b/bun.lock index f5dfe6836f..be330d103f 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "claude-code", + "dependencies": { + "string-width": "^8.2.0", + }, "devDependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "@ant/claude-for-chrome-mcp": "workspace:*", diff --git a/docs/api-providers.md b/docs/api-providers.md new file mode 100644 index 0000000000..6d40de6bec --- /dev/null +++ b/docs/api-providers.md @@ -0,0 +1,106 @@ +# API 提供商集成 + +> 记录 Claude Code 支持的 API 提供商、配置方式、模型映射和集成细节。 + +## 支持的提供商 + +| 提供商 | modelType | 环境变量前缀 | API 兼容层 | +|--------|-----------|-------------|-----------| +| Anthropic (默认) | anthropic | ANTHROPIC_* | 原生 SDK | +| OpenAI | openai | OPENAI_* | OpenAI Chat Completions | +| Gemini | gemini | GEMINI_* | Gemini Generate Content | +| xAI Grok | grok | GROK_* / XAI_* | OpenAI 兼容 | +| **阿里云百炼 (DashScope)** | **anthropic** | **DASHSCOPE_* → ANTHROPIC_*** | **Anthropic 原生 SDK** | +| Amazon Bedrock | — | — | AWS SDK | +| Vertex AI | — | — | Google Cloud SDK | +| Azure Foundry | — | — | Azure SDK | + +## 阿里云百炼 (DashScope) + +### 概述 + +DashScope 是阿里云百炼平台的 **Anthropic 兼容 API** 端点。URL 路径 `/apps/anthropic` 明确表明其使用 Anthropic 接口协议。代码中将 `DASHSCOPE_*` 环境变量映射为 `ANTHROPIC_*`,然后走 firstParty Anthropic SDK 路径,**不经过 OpenAI 兼容层**。 + +### 默认配置 + +| 项目 | 值 | +|------|-----| +| Base URL | `https://coding.dashscope.aliyuncs.com/apps/anthropic` | +| OPUS 默认模型 | `qwen3-max-2026-01-23` | +| SONNET 默认模型 | `qwen3.6-plus` | +| HAIKU 默认模型 | `qwen3-coder-plus` | + +### 支持的模型 + +| 模型 | 推荐用途 | +|------|---------| +| qwen3-max-2026-01-23 | OPUS 级能力 | +| qwen3.6-plus | SONNET 级能力 | +| qwen3.5-plus | SONNET 级备选 | +| qwen3-coder-next | 编码专用 | +| qwen3-coder-plus | HAIKU 级编码 | +| glm-5 | 智谱最新 | +| glm-4.7 | 智谱稳定 | +| kimi-k2.5 | 月之暗面 | +| MiniMax-M2.5 | MiniMax | + +### 环境变量 + +| 变量名 | 必需 | 默认值 | 说明 | +|--------|------|--------|------| +| `DASHSCOPE_API_KEY` | 是 | — | DashScope API 密钥 → 映射为 `ANTHROPIC_AUTH_TOKEN` | +| `DASHSCOPE_BASE_URL` | 否 | `https://coding.dashscope.aliyuncs.com/apps/anthropic` | 自定义 API 端点 → 映射为 `ANTHROPIC_BASE_URL` | +| `DASHSCOPE_DEFAULT_OPUS_MODEL` | 否 | `qwen3-max-2026-01-23` | OPUS 级模型 | +| `DASHSCOPE_DEFAULT_SONNET_MODEL` | 否 | `qwen3.6-plus` | SONNET 级模型 | +| `DASHSCOPE_DEFAULT_HAIKU_MODEL` | 否 | `qwen3-coder-plus` | HAIKU 级模型 | +| `CLAUDE_CODE_USE_DASHSCOPE` | 否 | — | 环境变量启用 | + +### 架构 + +``` +用户输入 → /login → ConsoleOAuthFlow (DashScope UI) + → 设置 modelType: 'anthropic' + → 写入 ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN + +启动 → getAPIProvider() 返回 'dashscope' + → claude.ts 映射 DASHSCOPE_* → ANTHROPIC_* env + → Anthropic SDK 自动读取 ANTHROPIC_BASE_URL + → API 调用 DashScope Anthropic 兼容端点 +``` + +### 关键文件 + +| 文件 | 作用 | +|------|------| +| `src/utils/model/providers.ts` | 添加 `'dashscope'` 到 APIProvider 类型和 `getAPIProvider()` | +| `src/utils/model/model.ts` | 3 个 getDefault 函数添加 DASHSCOPE_DEFAULT_* env 检查 | +| `src/services/api/claude.ts` | dashscope provider 映射 env → firstParty Anthropic 路径 | +| `src/components/ConsoleOAuthFlow.tsx` | DashScope 登录表单,保存为 `modelType: 'anthropic'` | + +### 通过 /login 配置 + +1. 运行 `/login` +2. 选择 **"阿里云百炼 (DashScope) · Anthropic-compatible API"** +3. 输入 API Key(预填默认值,可直接修改) +4. 可选修改 Base URL、模型名称 +5. 保存后重启生效 + +### 手动配置 + +```bash +# 环境变量方式 +export DASHSCOPE_API_KEY="your-api-key" +export DASHSCOPE_BASE_URL="https://coding.dashscope.aliyuncs.com/apps/anthropic" + +# 或在 ~/.claude/settings.json 中配置 +{ + "modelType": "anthropic", + "env": { + "ANTHROPIC_BASE_URL": "https://coding.dashscope.aliyuncs.com/apps/anthropic", + "ANTHROPIC_AUTH_TOKEN": "your-api-key", + "DASHSCOPE_DEFAULT_SONNET_MODEL": "qwen3.6-plus", + "DASHSCOPE_DEFAULT_OPUS_MODEL": "qwen3-max-2026-01-23", + "DASHSCOPE_DEFAULT_HAIKU_MODEL": "qwen3-coder-plus" + } +} +``` diff --git a/docs/ui-rendering.md b/docs/ui-rendering.md new file mode 100644 index 0000000000..51e5779f9b --- /dev/null +++ b/docs/ui-rendering.md @@ -0,0 +1,121 @@ +# UI 渲染与界面层 + +> 记录 Claude Code 终端 UI(Ink 框架)的渲染行为、布局模式、常见问题及修复。 + +## LogoV2 启动欢迎界面 + +### 组件结构 + +``` +LogoV2.tsx (src/components/LogoV2/) +├── CondensedLogo.tsx — 缩略模式(无边框,日常最常见) +├── LogoV2.tsx — 完整模式(有边框)/ 紧凑模式(有边框) +├── CondensedLogo.tsx — 缩略模式(无边框,日常最常见) +├── Clawd.tsx — ASCII 猫吉祥物 +├── AnimatedClawd.tsx — 动画版 Clawd +├── FeedColumn.tsx — 右侧信息流(活动记录/更新日志) +└── WelcomeV2.tsx — 首次 Onboarding 欢迎文本 +``` + +### 三种渲染模式 + +| 模式 | 触发条件 | 边框 | 说明 | +|------|---------|------|------| +| **缩略模式** (Condensed) | ~~无 Release Notes 且无首次引导~~ | ❌ | **已永久关闭**(2026-04-10),不再进入 | +| **紧凑模式** (Compact) | 终端宽度 < 70 列 | ✅ | 紫色圆角边框,内容居中 | +| **完整模式** (Full) | 默认路径 | ✅ | 紫色圆角边框,双栏布局 + 信息流 | + +> **注**:缩略模式已在 `LogoV2.tsx` 中永久关闭(`isCondensedMode = false`),所有启动都走完整模式或紧凑模式。 + +### 模式判断逻辑(源码:`src/components/LogoV2/LogoV2.tsx`) + +```typescript +// 原始逻辑:三种模式切换 +const isCondensedMode = + !hasReleaseNotes && + !showOnboarding && + !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO) + +// 环境变量强制:始终显示完整模式 +// CLAUDE_CODE_FORCE_FULL_LOGO=1 bun run dev +``` + +**CondensedLogo 内部**(`src/components/LogoV2/CondensedLogo.tsx`):无任何边框,纯文本 + ASCII 猫。 + +### 边框渲染机制 + +边框由 `@anthropic/ink` 的 `Box` 组件通过 `cli-boxes` 库绘制: + +- `borderStyle="round"` → 使用 `cli-boxes` 的 `round` 样式(`╭─╮` `│` `╰─╯`) +- `borderColor="claude"` → 紫色主题色 +- `borderText` → 边框顶部嵌入标题文本 "Claude Code" + +详见 `packages/@ant/ink/src/core/render-border.ts`。 + +## 常见问题 + +### 问题:启动时欢迎边框时有时无 + +**原因**:日常启动(无 Release Notes、非首次使用)走缩略模式,不显示边框。 + +**修复**:在 `LogoV2.tsx` 中永久关闭缩略模式: + +```diff +- const isCondensedMode = +- !hasReleaseNotes && +- !showOnboarding && +- !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO) ++ const isCondensedMode = false + +- if ( +- !hasReleaseNotes && +- !showOnboarding && +- !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO) +- ) { ++ if (false) { + return + } +``` + +修改后,无论何种情况启动,都会显示完整模式的紫色圆角边框。 + +**影响**: +- 每次启动都会渲染完整的边框 + 信息流(活动记录或更新日志) +- 轻微增加启动渲染开销(从 0 边框到有边框双栏) +- 不再走 CondensedLogo(节省了 GuestPassesUpsell 等副作用计数逻辑) + +### 问题:终端宽度不足导致边框变形 + +**原因**:`getLayoutMode(columns)` 在终端 < 70 列时切换到 compact 模式。 + +**参考**:`src/utils/logoV2Utils.ts` — `getLayoutMode(columns: number): LayoutMode` + +### 问题:Recent Activity 显示 "No recent activity" + +**原因**:`src/setup.ts` 中 `getRecentActivity()` 只在 `hasReleaseNotes` 为 true 时才调用。日常启动(无新 Release Notes)时,`cachedActivity` 始终为空数组。 + +**修复**(`src/setup.ts` 第 383-395 行): + +```diff + if (!isBareMode()) { +- const { hasReleaseNotes } = await checkForReleaseNotes( +- getGlobalConfig().lastReleaseNotesSeen, +- ) +- if (hasReleaseNotes) { +- await getRecentActivity() +- } ++ // Populate release notes cache (side effect: fetches changelog if needed) ++ void checkForReleaseNotes(getGlobalConfig().lastReleaseNotesSeen) ++ // Load recent activity unconditionally (not tied to release notes) ++ try { ++ await getRecentActivity() ++ } catch (error) { ++ logError('Failed to load recent activity:', error) ++ } + } +``` + +**关键变化**: +1. `getRecentActivity()` 从 `if (hasReleaseNotes)` 块中移出,无条件调用 +2. 增加 try-catch 防止损坏的会话文件阻塞启动 +3. `checkForReleaseNotes` 改为 `void` 调用(保留 cache 填充的副作用,但不阻塞等待结果) diff --git a/package.json b/package.json index 7ee0b71a15..f0a58a0bb1 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,9 @@ "docs:dev": "npx mintlify dev", "rcs": "bun run scripts/rcs.ts" }, - "dependencies": {}, + "dependencies": { + "string-width": "^8.2.0" + }, "devDependencies": { "openai": "^6.33.0", "@alcalzone/ansi-tokenize": "^0.3.0", diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index bd1dd5d1e7..084198ad1c 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -16,7 +16,38 @@ import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settin import { Select } from './CustomSelect/select.js' import { Spinner } from './Spinner.js' import TextInput from './TextInput.js' -import { fi } from 'zod/v4/locales' +import stringWidth from 'string-width' + +// ─── Chinese UI strings for OAuth forms ────────────────────────────── +const ZH = { + // DashScope form + dashscopeTitle: '阿里云百炼(DashScope)配置', + dashscopeHelp1: '配置阿里云百炼 OpenAI 兼容 API 端点。', + dashscopeHelp2: '默认使用 DashScope 编码计划端点,只需填写 API 密钥即可。', + dashscopeHelp3: '🔗 控制台: https://bailian.console.aliyun.com/', + apiKeyLabel: 'API 密钥', + baseUrlLabel: '接口地址', + haikuLabel: 'Haiku 模型', + sonnetLabel: 'Sonnet 模型', + opusLabel: 'Opus 模型', + navHint: '↑↓/Tab 切换 · 最后一个字段按 Enter 保存 · Esc 返回', + emptyHint: '(未设置)', + // Field borders + borderTop: '─', + // Other forms (optional Chinese support) + customPlatformTitle: '自定义 Anthropic 兼容 API', + customPlatformHelp: '配置自定义的 Anthropic 兼容 API 端点。', + openaiTitle: 'OpenAI 兼容 API 配置', + openaiHelp: '配置 OpenAI Chat Completions 兼容端点(如 Ollama、DeepSeek、vLLM)。', + geminiTitle: 'Gemini API 配置', + geminiHelp: '配置 Gemini 内容生成 API 端点。Base URL 为可选项,默认使用 Google v1beta API。', +} + +function maskApiKey(value: string): string { + if (!value) return '' + if (value.length <= 8) return '•'.repeat(value.length) + return value.slice(0, 4) + '•'.repeat(Math.max(0, value.length - 8)) + value.slice(-4) +} type Props = { onDone(): void @@ -55,6 +86,15 @@ type OAuthStatus = opusModel: string activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' } // Gemini Generate Content API platform + | { + state: 'dashscope_api' + baseUrl: string + apiKey: string + haikuModel: string + sonnetModel: string + opusModel: string + activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' + } // Alibaba Cloud Bailian (DashScope) OpenAI-compatible API | { state: 'ready_to_start' } // Flow started, waiting for browser to open | { state: 'waiting_for_login'; url: string } // Browser opened, waiting for user to login | { state: 'creating_api_key' } // Got access token, creating API key @@ -485,6 +525,16 @@ function OAuthStatusMessage({ ), value: 'gemini_api', }, + { + label: ( + + 阿里云百炼 (DashScope) ·{' '} + Anthropic-compatible API + {'\n'} + + ), + value: 'dashscope_api', + }, { label: ( @@ -563,6 +613,17 @@ function OAuthStatusMessage({ opusModel: process.env.GEMINI_DEFAULT_OPUS_MODEL ?? '', activeField: 'base_url', }) + } else if (value === 'dashscope_api') { + logEvent('tengu_dashscope_api_selected', {}) + setOAuthStatus({ + state: 'dashscope_api', + baseUrl: process.env.DASHSCOPE_BASE_URL ?? 'https://coding.dashscope.aliyuncs.com/apps/anthropic', + apiKey: process.env.DASHSCOPE_API_KEY ?? '', + haikuModel: process.env.DASHSCOPE_DEFAULT_HAIKU_MODEL ?? 'qwen3-coder-plus', + sonnetModel: process.env.DASHSCOPE_DEFAULT_SONNET_MODEL ?? 'qwen3.6-plus', + opusModel: process.env.DASHSCOPE_DEFAULT_OPUS_MODEL ?? 'qwen3-max-2026-01-23', + activeField: 'api_key', + }) } else if (value === 'platform') { logEvent('tengu_oauth_platform_selected', {}) setOAuthStatus({ state: 'platform_setup' }) @@ -636,15 +697,6 @@ function OAuthStatusMessage({ [activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel], ) - const switchTo = useCallback( - (target: Field) => { - setOAuthStatus(buildState(activeField, inputValue, target)) - setInputValue(displayValues[target] ?? '') - setInputCursorOffset((displayValues[target] ?? '').length) - }, - [activeField, inputValue, displayValues, buildState, setOAuthStatus], - ) - const doSave = useCallback(() => { const finalVals = { ...displayValues, [activeField]: inputValue } const env: Record = {} @@ -746,58 +798,100 @@ function OAuthStatusMessage({ { context: 'Confirmation' }, ) - const columns = useTerminalSize().columns - 20 + // Chinese label mapping for custom platform form + const cpFieldLabels: Record = { + base_url: ZH.baseUrlLabel, + api_key: ZH.apiKeyLabel, + haiku_model: ZH.haikuLabel, + sonnet_model: ZH.sonnetLabel, + opus_model: ZH.opusLabel, + } + + // Calculate border width based on actual terminal display width (CJK chars = 2 columns) + const cpContentLen = FIELDS.reduce((maxLen, field) => { + const label = cpFieldLabels[field] + const isFieldActive = field === activeField + const val = isFieldActive ? inputValue : (displayValues[field] || ZH.emptyHint) + const len = stringWidth(label) + stringWidth(val) + 4 + return Math.max(maxLen, len) + }, 30) + const cpBorderLen = cpContentLen const renderRow = ( field: Field, - label: string, + _label: string, opts?: { mask?: boolean; placeholder?: string }, ) => { const active = activeField === field const val = displayValues[field] + const label = cpFieldLabels[field] + + if (active) { + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(inputValue) + const padLen = Math.max(0, cpBorderLen - labelWidth - valueWidth - 5) + return ( + + {'┌' + '─'.repeat(cpBorderLen - 2) + '┐'} + + + + {` ${label} `} + + + {' '.repeat(padLen)} + + + + ) + } + + const showMasked = opts?.mask + const displayVal = showMasked && val ? maskApiKey(val) : val + const valueText = displayVal || ZH.emptyHint + const valueColor = showMasked ? 'success' as const : (val ? ('warning' as const) : undefined) + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(valueText) + const padLen = Math.max(0, cpBorderLen - labelWidth - valueWidth - 5) + return ( - - - {` ${label} `} - - - {active ? ( - - ) : val ? ( - - {opts?.mask - ? val.slice(0, 8) + '\u00b7'.repeat(Math.max(0, val.length - 8)) - : val} - - ) : null} + + {'├' + '─'.repeat(cpBorderLen - 2) + '┤'} + + + {` ${label} `} + {valueText} + {' '.repeat(padLen)} + + ) } return ( - Anthropic Compatible Setup - - {renderRow('base_url', 'Base URL ')} - {renderRow('api_key', 'API Key ', { mask: true })} - {renderRow('haiku_model', 'Haiku ')} - {renderRow('sonnet_model', 'Sonnet ')} - {renderRow('opus_model', 'Opus ')} + {ZH.customPlatformTitle} + {ZH.customPlatformHelp} + + {renderRow('base_url', '')} + {renderRow('api_key', '', { mask: true })} + {renderRow('haiku_model', '')} + {renderRow('sonnet_model', '')} + {renderRow('opus_model', '')} + {'└' + '─'.repeat(cpBorderLen - 2) + '┘'} - ↑↓/Tab to switch · Enter on last field to save · Esc to go back + {ZH.navHint} ) @@ -981,62 +1075,100 @@ function OAuthStatusMessage({ { context: 'Confirmation' }, ) - const openaiColumns = useTerminalSize().columns - 20 + // Chinese label mapping for OpenAI form + const openaiFieldLabels: Record = { + base_url: ZH.baseUrlLabel, + api_key: ZH.apiKeyLabel, + haiku_model: ZH.haikuLabel, + sonnet_model: ZH.sonnetLabel, + opus_model: ZH.opusLabel, + } + + // Calculate border width based on actual terminal display width (CJK chars = 2 columns) + const openaiContentLen = OPENAI_FIELDS.reduce((maxLen, field) => { + const label = openaiFieldLabels[field] + const isFieldActive = field === activeField + const val = isFieldActive ? openaiInputValue : (openaiDisplayValues[field] || ZH.emptyHint) + const len = stringWidth(label) + stringWidth(val) + 4 + return Math.max(maxLen, len) + }, 30) + const openaiBorderLen = openaiContentLen const renderOpenAIRow = ( field: OpenAIField, - label: string, + _label: string, opts?: { mask?: boolean }, ) => { const active = activeField === field const val = openaiDisplayValues[field] + const label = openaiFieldLabels[field] + + if (active) { + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(openaiInputValue) + const padLen = Math.max(0, openaiBorderLen - labelWidth - valueWidth - 5) + return ( + + {'┌' + '─'.repeat(openaiBorderLen - 2) + '┐'} + + + + {` ${label} `} + + + {' '.repeat(padLen)} + + + + ) + } + + const showMasked = opts?.mask + const displayVal = showMasked && val ? maskApiKey(val) : val + const valueText = displayVal || ZH.emptyHint + const valueColor = showMasked ? 'success' as const : (val ? ('warning' as const) : undefined) + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(valueText) + const padLen = Math.max(0, openaiBorderLen - labelWidth - valueWidth - 5) + return ( - - - {` ${label} `} - - - {active ? ( - - ) : val ? ( - - {opts?.mask - ? val.slice(0, 8) + '\u00b7'.repeat(Math.max(0, val.length - 8)) - : val} - - ) : null} + + {'├' + '─'.repeat(openaiBorderLen - 2) + '┤'} + + + {` ${label} `} + {valueText} + {' '.repeat(padLen)} + + ) } return ( - OpenAI Compatible API Setup - - Configure an OpenAI Chat Completions compatible endpoint (e.g. - Ollama, DeepSeek, vLLM). - - - {renderOpenAIRow('base_url', 'Base URL ')} - {renderOpenAIRow('api_key', 'API Key ', { mask: true })} - {renderOpenAIRow('haiku_model', 'Haiku ')} - {renderOpenAIRow('sonnet_model', 'Sonnet ')} - {renderOpenAIRow('opus_model', 'Opus ')} + {ZH.openaiTitle} + {ZH.openaiHelp} + + {renderOpenAIRow('base_url', '')} + {renderOpenAIRow('api_key', '', { mask: true })} + {renderOpenAIRow('haiku_model', '')} + {renderOpenAIRow('sonnet_model', '')} + {renderOpenAIRow('opus_model', '')} + {'└' + '─'.repeat(openaiBorderLen - 2) + '┘'} - ↑↓/Tab to switch · Enter on last field to save · Esc to go back + {ZH.navHint} ) @@ -1214,63 +1346,411 @@ function OAuthStatusMessage({ { context: 'Confirmation' }, ) - const geminiColumns = useTerminalSize().columns - 20 + // Chinese label mapping for Gemini form + const geminiFieldLabels: Record = { + base_url: ZH.baseUrlLabel, + api_key: ZH.apiKeyLabel, + haiku_model: ZH.haikuLabel, + sonnet_model: ZH.sonnetLabel, + opus_model: ZH.opusLabel, + } + + // Calculate border width based on actual terminal display width (CJK chars = 2 columns) + const geminiContentLen = GEMINI_FIELDS.reduce((maxLen, field) => { + const label = geminiFieldLabels[field] + const isFieldActive = field === activeField + const val = isFieldActive ? geminiInputValue : (geminiDisplayValues[field] || ZH.emptyHint) + const len = stringWidth(label) + stringWidth(val) + 4 + return Math.max(maxLen, len) + }, 30) + const geminiBorderLen = geminiContentLen const renderGeminiRow = ( field: GeminiField, - label: string, + _label: string, opts?: { mask?: boolean }, ) => { const active = activeField === field const val = geminiDisplayValues[field] + const label = geminiFieldLabels[field] + + if (active) { + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(geminiInputValue) + const padLen = Math.max(0, geminiBorderLen - labelWidth - valueWidth - 5) + return ( + + {'┌' + '─'.repeat(geminiBorderLen - 2) + '┐'} + + + + {` ${label} `} + + + {' '.repeat(padLen)} + + + + ) + } + + const showMasked = opts?.mask + const displayVal = showMasked && val ? maskApiKey(val) : val + const valueText = displayVal || ZH.emptyHint + const valueColor = showMasked ? 'success' as const : (val ? ('warning' as const) : undefined) + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(valueText) + const padLen = Math.max(0, geminiBorderLen - labelWidth - valueWidth - 5) + return ( - - - {` ${label} `} - - - {active ? ( - - ) : val ? ( - - {opts?.mask - ? val.slice(0, 8) + '\u00b7'.repeat(Math.max(0, val.length - 8)) - : val} - - ) : null} + + {'├' + '─'.repeat(geminiBorderLen - 2) + '┤'} + + + {` ${label} `} + {valueText} + {' '.repeat(padLen)} + + ) } return ( - Gemini API Setup + {ZH.geminiTitle} + {ZH.geminiHelp} + + {renderGeminiRow('base_url', '')} + {renderGeminiRow('api_key', '', { mask: true })} + {renderGeminiRow('haiku_model', '')} + {renderGeminiRow('sonnet_model', '')} + {renderGeminiRow('opus_model', '')} + {'└' + '─'.repeat(geminiBorderLen - 2) + '┘'} + - Configure a Gemini Generate Content compatible endpoint. Base URL is - optional and defaults to Google's v1beta API. + {ZH.navHint} + + ) + } + + case 'dashscope_api': + { + type DashScopeField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' + const DASHSCOPE_FIELDS: DashScopeField[] = [ + 'api_key', + 'base_url', + 'haiku_model', + 'sonnet_model', + 'opus_model', + ] + const ds = oauthStatus as { + state: 'dashscope_api' + activeField: DashScopeField + baseUrl: string + apiKey: string + haikuModel: string + sonnetModel: string + opusModel: string + } + const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = ds + const dsDisplayValues: Record = { + base_url: baseUrl, + api_key: apiKey, + haiku_model: haikuModel, + sonnet_model: sonnetModel, + opus_model: opusModel, + } + + const [dsInputValue, setDsInputValue] = useState( + () => dsDisplayValues[activeField], + ) + const [dsInputCursorOffset, setDsInputCursorOffset] = useState( + () => dsDisplayValues[activeField].length, + ) + + const buildDashScopeState = useCallback( + (field: DashScopeField, value: string, newActive?: DashScopeField) => { + const s = { + state: 'dashscope_api' as const, + activeField: newActive ?? activeField, + baseUrl, + apiKey, + haikuModel, + sonnetModel, + opusModel, + } + switch (field) { + case 'base_url': + return { ...s, baseUrl: value } + case 'api_key': + return { ...s, apiKey: value } + case 'haiku_model': + return { ...s, haikuModel: value } + case 'sonnet_model': + return { ...s, sonnetModel: value } + case 'opus_model': + return { ...s, opusModel: value } + } + }, + [activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel], + ) + + const doDashScopeSave = useCallback(() => { + const finalVals = { ...dsDisplayValues, [activeField]: dsInputValue } + if (!finalVals.api_key) { + setOAuthStatus({ + state: 'error', + message: 'DashScope requires an API Key.', + toRetry: { + state: 'dashscope_api', + baseUrl: finalVals.base_url, + apiKey: '', + haikuModel: finalVals.haiku_model, + sonnetModel: finalVals.sonnet_model, + opusModel: finalVals.opus_model, + activeField: 'api_key', + }, + }) + return + } + + const env: Record = {} + // 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: 'dashscope_api', + baseUrl: '', + apiKey: finalVals.api_key, + haikuModel: finalVals.haiku_model, + sonnetModel: finalVals.sonnet_model, + opusModel: finalVals.opus_model, + activeField: 'base_url', + }, + }) + return + } + env.ANTHROPIC_BASE_URL = finalVals.base_url + } + + env.ANTHROPIC_AUTH_TOKEN = finalVals.api_key + + if (finalVals.haiku_model) env.DASHSCOPE_DEFAULT_HAIKU_MODEL = finalVals.haiku_model + if (finalVals.sonnet_model) env.DASHSCOPE_DEFAULT_SONNET_MODEL = finalVals.sonnet_model + if (finalVals.opus_model) env.DASHSCOPE_DEFAULT_OPUS_MODEL = finalVals.opus_model + const { error } = updateSettingsForSource('userSettings', { + modelType: 'anthropic', + env, + } as any) + if (error) { + setOAuthStatus({ + state: 'error', + message: 'Failed to save settings. Please try again.', + toRetry: { + state: 'dashscope_api', + baseUrl: finalVals.base_url ?? '', + apiKey: finalVals.api_key ?? '', + haikuModel: finalVals.haiku_model ?? '', + sonnetModel: finalVals.sonnet_model ?? '', + opusModel: finalVals.opus_model ?? '', + activeField: 'api_key', + }, + }) + } else { + for (const [k, v] of Object.entries(env)) process.env[k] = v + setOAuthStatus({ state: 'success' }) + void onDone() + } + }, [activeField, dsInputValue, dsDisplayValues, setOAuthStatus, onDone]) + + const handleDashScopeEnter = useCallback(() => { + const idx = DASHSCOPE_FIELDS.indexOf(activeField) + if (idx === DASHSCOPE_FIELDS.length - 1) { + setOAuthStatus(buildDashScopeState(activeField, dsInputValue)) + doDashScopeSave() + } else { + const next = DASHSCOPE_FIELDS[idx + 1]! + setOAuthStatus(buildDashScopeState(activeField, dsInputValue, next)) + setDsInputValue(dsDisplayValues[next] ?? '') + setDsInputCursorOffset((dsDisplayValues[next] ?? '').length) + } + }, [ + activeField, + buildDashScopeState, + doDashScopeSave, + dsDisplayValues, + dsInputValue, + setOAuthStatus, + ]) + + useKeybinding( + 'tabs:next', + () => { + const idx = DASHSCOPE_FIELDS.indexOf(activeField) + if (idx < DASHSCOPE_FIELDS.length - 1) { + setOAuthStatus( + buildDashScopeState(activeField, dsInputValue, DASHSCOPE_FIELDS[idx + 1]), + ) + setDsInputValue(dsDisplayValues[DASHSCOPE_FIELDS[idx + 1]!] ?? '') + setDsInputCursorOffset( + (dsDisplayValues[DASHSCOPE_FIELDS[idx + 1]!] ?? '').length, + ) + } + }, + { context: 'FormField' }, + ) + useKeybinding( + 'tabs:previous', + () => { + const idx = DASHSCOPE_FIELDS.indexOf(activeField) + if (idx > 0) { + setOAuthStatus( + buildDashScopeState(activeField, dsInputValue, DASHSCOPE_FIELDS[idx - 1]), + ) + setDsInputValue(dsDisplayValues[DASHSCOPE_FIELDS[idx - 1]!] ?? '') + setDsInputCursorOffset( + (dsDisplayValues[DASHSCOPE_FIELDS[idx - 1]!] ?? '').length, + ) + } + }, + { context: 'FormField' }, + ) + useKeybinding( + 'confirm:no', + () => { + setOAuthStatus({ state: 'idle' }) + }, + { context: 'Confirmation' }, + ) + + + // Chinese label mapping + const fieldLabels: Record = { + api_key: ZH.apiKeyLabel, + base_url: ZH.baseUrlLabel, + haiku_model: ZH.haikuLabel, + sonnet_model: ZH.sonnetLabel, + opus_model: ZH.opusLabel, + } + + // Calculate border width based on actual terminal display width (CJK chars = 2 columns) + const dsContentLen = DASHSCOPE_FIELDS.reduce((maxLen, field) => { + const label = fieldLabels[field] + const isFieldActive = field === activeField + const val = isFieldActive ? dsInputValue : (dsDisplayValues[field] || ZH.emptyHint) + // Row: │(1) + ' '(1) + label + ' '(1) + value + │(1) + // Use stringWidth for correct CJK width + const len = stringWidth(label) + stringWidth(val) + 4 + return Math.max(maxLen, len) + }, 30) + const dsBorderLen = dsContentLen + + const renderDashScopeRow = ( + field: DashScopeField, + _label: string, + opts: { mask?: boolean } = {}, + ): React.ReactNode => { + const isActive = field === activeField + const displayValue = dsDisplayValues[field] + const label = fieldLabels[field] + + // Build a visual border line with content-based width and right-side closing + const topLine = isActive ? '┌' + '─'.repeat(dsBorderLen - 2) + '┐' : '├' + '─'.repeat(dsBorderLen - 2) + '┤' + const sideBar = '│' + + if (isActive) { + // Pad to match border width using terminal display width + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(dsInputValue) + const padLen = Math.max(0, dsBorderLen - labelWidth - valueWidth - 5) + return ( + + {topLine} + + {sideBar} + + {` ${label} `} + + + {' '.repeat(padLen)} + {sideBar} + + + ) + } + + // Inactive: show value with border, padded to align with right border + const showMasked = opts.mask + const maskedValue = showMasked && displayValue ? maskApiKey(displayValue) : displayValue + const valueDisplay = maskedValue || ZH.emptyHint + const labelWidth = stringWidth(label) + const valueWidth = stringWidth(valueDisplay) + const padLen = Math.max(0, dsBorderLen - labelWidth - valueWidth - 4) + + return ( + + {topLine} + + {sideBar} + {` ${label} `} + {valueDisplay} + {' '.repeat(padLen)} + {sideBar} + + + ) + } + + return ( + - {renderGeminiRow('base_url', 'Base URL ')} - {renderGeminiRow('api_key', 'API Key ', { mask: true })} - {renderGeminiRow('haiku_model', 'Haiku ')} - {renderGeminiRow('sonnet_model', 'Sonnet ')} - {renderGeminiRow('opus_model', 'Opus ')} + {ZH.dashscopeTitle} + + {ZH.dashscopeHelp1} + {'\n'} + {ZH.dashscopeHelp2} + {'\n'} + {ZH.dashscopeHelp3} + + + {renderDashScopeRow('api_key', '', { mask: true })} + {renderDashScopeRow('base_url', '')} + {renderDashScopeRow('haiku_model', '')} + {renderDashScopeRow('sonnet_model', '')} + {renderDashScopeRow('opus_model', '')} + {'└' + '─'.repeat(dsBorderLen - 2) + '┘'} + + + {ZH.navHint} + - - ↑↓/Tab to switch · Enter on last field to save · Esc to go back - ) } diff --git a/src/components/LogoV2/CondensedLogo.tsx b/src/components/LogoV2/CondensedLogo.tsx index eb048ec2d4..1e159c92ad 100644 --- a/src/components/LogoV2/CondensedLogo.tsx +++ b/src/components/LogoV2/CondensedLogo.tsx @@ -79,39 +79,56 @@ export function CondensedLogo(): ReactNode { : textWidth const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)) + const userTheme = resolveThemeSetting(getGlobalConfig().theme) + const borderTitle = color('claude', userTheme)(' Claude Code ') + // OffscreenFreeze: the logo sits at the top of the message list and is the // first thing to enter scrollback. useMainLoopModel() subscribes to model // changes and getLogoDisplayData() reads getCwd()/subscription state — any // of which changing while in scrollback would force a full terminal reset. return ( - - {isFullscreenEnvEnabled() ? : } + + + {isFullscreenEnvEnabled() ? : } - {/* Info */} - - - Claude Code{' '} - v{truncatedVersion} - - {shouldSplit ? ( - <> - {truncatedModel} - {truncatedBilling} - - ) : ( + {/* Info */} + + + Claude Code{' '} + v{truncatedVersion} + + {shouldSplit ? ( + <> + {truncatedModel} + {truncatedBilling} + + ) : ( + + {truncatedModel} · {truncatedBilling} + + )} - {truncatedModel} · {truncatedBilling} + {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd} - )} - - {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd} - - {showGuestPassesUpsell && } - {!showGuestPassesUpsell && showOverageCreditUpsell && ( - - )} - + {showGuestPassesUpsell && } + {!showGuestPassesUpsell && showOverageCreditUpsell && ( + + )} + + ) diff --git a/src/components/LogoV2/LogoV2.tsx b/src/components/LogoV2/LogoV2.tsx index c7dcf41392..3f8048b442 100644 --- a/src/components/LogoV2/LogoV2.tsx +++ b/src/components/LogoV2/LogoV2.tsx @@ -40,7 +40,6 @@ import { CondensedLogo } from './CondensedLogo.js' import { OffscreenFreeze } from '../OffscreenFreeze.js' import { checkForReleaseNotesSync } from '../../utils/releaseNotes.js' import { getDumpPromptsPath } from 'src/services/api/dumpPrompts.js' -import { isEnvTruthy } from 'src/utils/envUtils.js' import { getStartupPerfLogPath, isDetailedProfilingEnabled, @@ -130,13 +129,8 @@ export function LogoV2(): React.ReactNode { } }, [config, showOnboarding]) - // In condensed mode (early-return below renders ), - // CondensedLogo's own useEffect handles the impression count. Skipping - // here avoids double-counting since hooks fire before the early return. - const isCondensedMode = - !hasReleaseNotes && - !showOnboarding && - !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO) + // Always show full logo with border (force full logo permanently enabled) + const isCondensedMode = false useEffect(() => { if (showGuestPassesUpsell && !showOnboarding && !isCondensedMode) { @@ -177,12 +171,8 @@ export function LogoV2(): React.ReactNode { LEFT_PANEL_MAX_WIDTH - 20, ) - // Show condensed logo if no new changelog and not showing onboarding and not forcing full logo - if ( - !hasReleaseNotes && - !showOnboarding && - !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO) - ) { + // Show full logo with border (force full logo permanently enabled) + if (false) { return ( <> diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index ee7f2e3611..1064da5b95 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -1355,6 +1355,17 @@ async function* queryModel( return } + // DashScope (Alibaba Cloud Bailian) uses Anthropic-compatible API protocol. + // Map DASHSCOPE_* env vars → ANTHROPIC_* so the firstParty path works natively. + if (getAPIProvider() === 'dashscope') { + if (process.env.DASHSCOPE_BASE_URL && !process.env.ANTHROPIC_BASE_URL) { + process.env.ANTHROPIC_BASE_URL = process.env.DASHSCOPE_BASE_URL + } + if (process.env.DASHSCOPE_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) { + process.env.ANTHROPIC_AUTH_TOKEN = process.env.DASHSCOPE_API_KEY + } + } + // Instrumentation: Track message count after normalization logEvent('tengu_api_after_normalize', { postNormalizedMessageCount: messagesForAPI.length, diff --git a/src/services/api/openai/modelMapping.ts b/src/services/api/openai/modelMapping.ts index 7cb49c7f99..dcce363395 100644 --- a/src/services/api/openai/modelMapping.ts +++ b/src/services/api/openai/modelMapping.ts @@ -48,7 +48,7 @@ export function resolveOpenAIModel(anthropicModel: string): string { // Check family-specific overrides const family = getModelFamily(cleanModel) if (family) { - // OpenAI-specific family override (preferred for openai provider) + // OpenAI-specific family override const openaiEnvVar = `OPENAI_DEFAULT_${family.toUpperCase()}_MODEL` const openaiOverride = process.env[openaiEnvVar] if (openaiOverride) return openaiOverride diff --git a/src/setup.ts b/src/setup.ts index 985e8577a6..f4977e5551 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -384,11 +384,13 @@ export async function setup( // --bare / SIMPLE: skip — release notes are interactive-UI display data, // and getRecentActivity() reads up to 10 session JSONL files. if (!isBareMode()) { - const { hasReleaseNotes } = await checkForReleaseNotes( - getGlobalConfig().lastReleaseNotesSeen, - ) - if (hasReleaseNotes) { + // Populate release notes cache (side effect: fetches changelog if needed) + void checkForReleaseNotes(getGlobalConfig().lastReleaseNotesSeen) + // Load recent activity unconditionally (not tied to release notes) + try { await getRecentActivity() + } catch (error) { + logError('Failed to load recent activity:', error) } } diff --git a/src/utils/model/configs.ts b/src/utils/model/configs.ts index d3fac9b07d..39cc0d559f 100644 --- a/src/utils/model/configs.ts +++ b/src/utils/model/configs.ts @@ -14,6 +14,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = { openai: 'claude-3-7-sonnet-20250219', gemini: 'claude-3-7-sonnet-20250219', grok: 'claude-3-7-sonnet-20250219', + dashscope: 'qwen3.6-plus', } as const satisfies ModelConfig export const CLAUDE_3_5_V2_SONNET_CONFIG = { @@ -24,6 +25,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = { openai: 'claude-3-5-sonnet-20241022', gemini: 'claude-3-5-sonnet-20241022', grok: 'claude-3-5-sonnet-20241022', + dashscope: 'qwen3.6-plus', } as const satisfies ModelConfig export const CLAUDE_3_5_HAIKU_CONFIG = { @@ -34,6 +36,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = { openai: 'claude-3-5-haiku-20241022', gemini: 'claude-3-5-haiku-20241022', grok: 'claude-3-5-haiku-20241022', + dashscope: 'qwen3-coder-plus', } as const satisfies ModelConfig export const CLAUDE_HAIKU_4_5_CONFIG = { @@ -44,6 +47,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = { openai: 'claude-haiku-4-5-20251001', gemini: 'claude-haiku-4-5-20251001', grok: 'claude-haiku-4-5-20251001', + dashscope: 'qwen3-coder-plus', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_CONFIG = { @@ -54,6 +58,7 @@ export const CLAUDE_SONNET_4_CONFIG = { openai: 'claude-sonnet-4-20250514', gemini: 'claude-sonnet-4-20250514', grok: 'claude-sonnet-4-20250514', + dashscope: 'qwen3.6-plus', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_5_CONFIG = { @@ -64,6 +69,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = { openai: 'claude-sonnet-4-5-20250929', gemini: 'claude-sonnet-4-5-20250929', grok: 'claude-sonnet-4-5-20250929', + dashscope: 'qwen3.6-plus', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_CONFIG = { @@ -74,6 +80,7 @@ export const CLAUDE_OPUS_4_CONFIG = { openai: 'claude-opus-4-20250514', gemini: 'claude-opus-4-20250514', grok: 'claude-opus-4-20250514', + dashscope: 'qwen3-max-2026-01-23', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_1_CONFIG = { @@ -84,6 +91,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = { openai: 'claude-opus-4-1-20250805', gemini: 'claude-opus-4-1-20250805', grok: 'claude-opus-4-1-20250805', + dashscope: 'qwen3-max-2026-01-23', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_5_CONFIG = { @@ -94,6 +102,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = { openai: 'claude-opus-4-5-20251101', gemini: 'claude-opus-4-5-20251101', grok: 'claude-opus-4-5-20251101', + dashscope: 'qwen3-max-2026-01-23', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_6_CONFIG = { @@ -104,6 +113,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = { openai: 'claude-opus-4-6', gemini: 'claude-opus-4-6', grok: 'claude-opus-4-6', + dashscope: 'qwen3-max-2026-01-23', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_6_CONFIG = { @@ -114,6 +124,7 @@ export const CLAUDE_SONNET_4_6_CONFIG = { openai: 'claude-sonnet-4-6', gemini: 'claude-sonnet-4-6', grok: 'claude-sonnet-4-6', + dashscope: 'qwen3.6-plus', } as const satisfies ModelConfig // @[MODEL LAUNCH]: Register the new config here. diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 791daeb6b4..de9cee3df6 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -122,10 +122,18 @@ export function getDefaultOpusModel(): ModelName { if (provider === 'gemini' && process.env.GEMINI_DEFAULT_OPUS_MODEL) { return process.env.GEMINI_DEFAULT_OPUS_MODEL } + // For DashScope provider, check DASHSCOPE_DEFAULT_OPUS_MODEL + if (provider === 'dashscope' && process.env.DASHSCOPE_DEFAULT_OPUS_MODEL) { + return process.env.DASHSCOPE_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 } + // DashScope default models (OpenAI-compatible API) + if (provider === 'dashscope') { + return 'qwen3-max-2026-01-23' + } // 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. @@ -149,10 +157,18 @@ export function getDefaultSonnetModel(): ModelName { if (provider === 'gemini' && process.env.GEMINI_DEFAULT_SONNET_MODEL) { return process.env.GEMINI_DEFAULT_SONNET_MODEL } + // For DashScope provider, check DASHSCOPE_DEFAULT_SONNET_MODEL + if (provider === 'dashscope' && process.env.DASHSCOPE_DEFAULT_SONNET_MODEL) { + return process.env.DASHSCOPE_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 } + // DashScope default models (OpenAI-compatible API) + if (provider === 'dashscope') { + return 'qwen3.6-plus' + } // Default to Sonnet 4.5 for 3P since they may not have 4.6 yet if (provider !== 'firstParty') { return getModelStrings().sonnet45 @@ -171,11 +187,20 @@ export function getDefaultHaikuModel(): ModelName { if (provider === 'gemini' && process.env.GEMINI_DEFAULT_HAIKU_MODEL) { return process.env.GEMINI_DEFAULT_HAIKU_MODEL } + // For DashScope provider, check DASHSCOPE_DEFAULT_HAIKU_MODEL + if (provider === 'dashscope' && process.env.DASHSCOPE_DEFAULT_HAIKU_MODEL) { + return process.env.DASHSCOPE_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 } + // DashScope default models (OpenAI-compatible API) + if (provider === 'dashscope') { + return 'qwen3-coder-plus' + } + // Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex) return getModelStrings().haiku45 } diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index 823384f2d7..b778375f30 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -10,12 +10,14 @@ export type APIProvider = | 'openai' | 'gemini' | 'grok' + | 'dashscope' export function getAPIProvider(): APIProvider { const modelType = getInitialSettings().modelType if (modelType === 'openai') return 'openai' if (modelType === 'gemini') return 'gemini' if (modelType === 'grok') return 'grok' + if (modelType === 'dashscope') return 'dashscope' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) return 'bedrock' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)) return 'vertex' @@ -24,6 +26,7 @@ export function getAPIProvider(): APIProvider { if (isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) return 'openai' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) return 'gemini' if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GROK)) return 'grok' + if (isEnvTruthy(process.env.CLAUDE_CODE_USE_DASHSCOPE)) return 'dashscope' return 'firstParty' } diff --git a/src/utils/settings/types.ts b/src/utils/settings/types.ts index edeecb1901..0feccde0b8 100644 --- a/src/utils/settings/types.ts +++ b/src/utils/settings/types.ts @@ -373,11 +373,11 @@ export const SettingsSchema = lazySchema(() => .optional() .describe('Tool usage permissions configuration'), modelType: z - .enum(['anthropic', 'openai', 'gemini', 'grok']) + .enum(['anthropic', 'openai', 'gemini', 'grok', 'dashscope']) .optional() .describe( - 'API provider type. "anthropic" uses the Anthropic API (default), "openai" uses the OpenAI Chat Completions API, "gemini" uses the Gemini API, and "grok" uses the xAI Grok API (OpenAI-compatible). ' + - 'When set to "openai", configure OPENAI_API_KEY, OPENAI_BASE_URL, and OPENAI_MODEL. When set to "gemini", configure GEMINI_API_KEY and optional GEMINI_BASE_URL. When set to "grok", configure GROK_API_KEY (or XAI_API_KEY), optional GROK_BASE_URL, GROK_MODEL, and GROK_MODEL_MAP.', + 'API provider type. "anthropic" uses the Anthropic API (default), "openai" uses the OpenAI Chat Completions API, "gemini" uses the Gemini API, "grok" uses the xAI Grok API (OpenAI-compatible), and "dashscope" uses Alibaba Cloud Bailian DashScope API (OpenAI-compatible). ' + + 'When set to "openai", configure OPENAI_API_KEY, OPENAI_BASE_URL, and OPENAI_MODEL. When set to "gemini", configure GEMINI_API_KEY and optional GEMINI_BASE_URL. When set to "grok", configure GROK_API_KEY (or XAI_API_KEY), optional GROK_BASE_URL, GROK_MODEL, and GROK_MODEL_MAP. When set to "dashscope", configure DASHSCOPE_API_KEY and optional DASHSCOPE_BASE_URL.', ), model: z .string()