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()