Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9528,6 +9528,12 @@ async function applyToClaudeSettings(config = {}) {
};
delete nextEnv.ANTHROPIC_AUTH_TOKEN;
delete nextEnv.CLAUDE_CODE_USE_KEY;
const subModels = {
ANTHROPIC_DEFAULT_HAIKU_MODEL: (config.haikuModel || model).trim(),
ANTHROPIC_DEFAULT_SONNET_MODEL: (config.sonnetModel || model).trim(),
ANTHROPIC_DEFAULT_OPUS_MODEL: (config.opusModel || model).trim()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};
Object.assign(nextEnv, subModels);

const nextSettings = {
...currentSettings,
Expand All @@ -9546,7 +9552,10 @@ async function applyToClaudeSettings(config = {}) {
updatedKeys: [
'env.ANTHROPIC_API_KEY',
'env.ANTHROPIC_BASE_URL',
'env.ANTHROPIC_MODEL'
'env.ANTHROPIC_MODEL',
'env.ANTHROPIC_DEFAULT_HAIKU_MODEL',
'env.ANTHROPIC_DEFAULT_SONNET_MODEL',
'env.ANTHROPIC_DEFAULT_OPUS_MODEL'
]
};
if (proxyResult) {
Expand Down Expand Up @@ -9597,6 +9606,9 @@ function readClaudeSettingsInfo() {
useKey: typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY : '',
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
haikuModel: typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === 'string' ? env.ANTHROPIC_DEFAULT_HAIKU_MODEL : '',
sonnetModel: typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL === 'string' ? env.ANTHROPIC_DEFAULT_SONNET_MODEL : '',
opusModel: typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL === 'string' ? env.ANTHROPIC_DEFAULT_OPUS_MODEL : '',
env
};
}
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/web-ui-behavior-parity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,10 @@ test('captured bundled app skeleton only exposes expected data key drift versus
'providersHealthSummary',
'providersHealthTone',
'sessionContextUtilization',
'isLocalProviderDisabled'
'isLocalProviderDisabled',
'currentClaudeHaikuModel',
'currentClaudeSonnetModel',
'currentClaudeOpusModel'
];
const allowedMissingCurrentComputedKeys = [
'hasLocalAndProxy',
Expand Down
14 changes: 13 additions & 1 deletion tests/unit/web-ui-logic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ test('normalizeClaudeValue trims strings and ignores non-string', () => {

test('normalizeClaudeConfig trims all fields', () => {
const cfg = normalizeClaudeConfig({ apiKey: ' key ', baseUrl: ' url ', model: ' model ', authToken: ' token ', useKey: ' yes ', externalCredentialType: ' auth-token ' });
assert.deepStrictEqual(cfg, { apiKey: 'key', baseUrl: 'url', model: 'model', authToken: 'token', useKey: 'yes', externalCredentialType: 'auth-token', targetApi: 'responses' });
assert.deepStrictEqual(cfg, { apiKey: 'key', baseUrl: 'url', model: 'model', haikuModel: '', sonnetModel: '', opusModel: '', authToken: 'token', useKey: 'yes', externalCredentialType: 'auth-token', targetApi: 'responses' });
});

test('normalizeClaudeConfig infers external credential type from authToken and useKey', () => {
Expand All @@ -61,6 +61,9 @@ test('normalizeClaudeConfig infers external credential type from authToken and u
apiKey: '',
baseUrl: '',
model: '',
haikuModel: '',
sonnetModel: '',
opusModel: '',
authToken: 'token',
useKey: '',
externalCredentialType: 'auth-token',
Expand All @@ -73,6 +76,9 @@ test('normalizeClaudeConfig infers external credential type from authToken and u
apiKey: '',
baseUrl: '',
model: '',
haikuModel: '',
sonnetModel: '',
opusModel: '',
authToken: '',
useKey: '1',
externalCredentialType: 'claude-code-use-key',
Expand All @@ -99,6 +105,9 @@ test('normalizeClaudeSettingsEnv trims settings env', () => {
apiKey: 'key',
baseUrl: 'url',
model: 'model',
haikuModel: '',
sonnetModel: '',
opusModel: '',
authToken: 'token',
useKey: 'true',
externalCredentialType: ''
Expand All @@ -111,6 +120,9 @@ test('normalizeClaudeSettingsEnv fills missing fields with empty strings', () =>
apiKey: 'k',
baseUrl: '',
model: 'glm-4.7',
haikuModel: '',
sonnetModel: '',
opusModel: '',
authToken: '',
useKey: '',
externalCredentialType: ''
Expand Down
6 changes: 6 additions & 0 deletions web-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ document.addEventListener('DOMContentLoaded', () => {
apiKey: '',
baseUrl: 'https://open.bigmodel.cn/api/anthropic',
model: 'glm-4.7',
haikuModel: '',
sonnetModel: '',
opusModel: '',
targetApi: 'responses',
hasKey: false
}
Expand Down Expand Up @@ -586,6 +589,9 @@ document.addEventListener('DOMContentLoaded', () => {
config.apiKey = '';
config.hasKey = false;
}
if (!('haikuModel' in config)) this.$set(config, 'haikuModel', '');
if (!('sonnetModel' in config)) this.$set(config, 'sonnetModel', '');
if (!('opusModel' in config)) this.$set(config, 'opusModel', '');
const targetApiRaw = typeof config.targetApi === 'string' ? config.targetApi.trim().toLowerCase() : '';
if (targetApiRaw === 'chat_completions' || targetApiRaw === 'chat-completions' || targetApiRaw === 'chat/completions') {
config.targetApi = 'chat_completions';
Expand Down
6 changes: 6 additions & 0 deletions web-ui/logic.claude.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export function normalizeClaudeConfig(config) {
apiKey,
baseUrl: normalizeClaudeValue(safe.baseUrl),
model: normalizeClaudeValue(safe.model),
haikuModel: normalizeClaudeValue(safe.haikuModel),
sonnetModel: normalizeClaudeValue(safe.sonnetModel),
opusModel: normalizeClaudeValue(safe.opusModel),
authToken,
useKey,
externalCredentialType,
Expand All @@ -96,6 +99,9 @@ export function normalizeClaudeSettingsEnv(env) {
apiKey,
baseUrl: normalizeClaudeValue(safe.ANTHROPIC_BASE_URL),
model: normalizeClaudeValue(safe.ANTHROPIC_MODEL) || 'glm-4.7',
haikuModel: normalizeClaudeValue(safe.ANTHROPIC_DEFAULT_HAIKU_MODEL),
sonnetModel: normalizeClaudeValue(safe.ANTHROPIC_DEFAULT_SONNET_MODEL),
opusModel: normalizeClaudeValue(safe.ANTHROPIC_DEFAULT_OPUS_MODEL),
authToken,
useKey,
externalCredentialType: apiKey
Expand Down
42 changes: 42 additions & 0 deletions web-ui/modules/app.computed.dashboard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,48 @@ export function createDashboardComputed() {
}
return list;
},
currentClaudeHaikuModel: {
get() {
const config = this.claudeConfigs && this.claudeConfigs[this.currentClaudeConfig];
return (config && config.haikuModel) || this.currentClaudeModel || '';
},
set(val) {
const config = this.claudeConfigs && this.claudeConfigs[this.currentClaudeConfig];
if (config) {
this.$set(config, 'haikuModel', (val || '').trim());
this.saveClaudeConfigs();
if (this.currentClaudeConfig) this.applyClaudeConfig(this.currentClaudeConfig);
}
}
},
currentClaudeSonnetModel: {
get() {
const config = this.claudeConfigs && this.claudeConfigs[this.currentClaudeConfig];
return (config && config.sonnetModel) || this.currentClaudeModel || '';
},
set(val) {
const config = this.claudeConfigs && this.claudeConfigs[this.currentClaudeConfig];
if (config) {
this.$set(config, 'sonnetModel', (val || '').trim());
this.saveClaudeConfigs();
if (this.currentClaudeConfig) this.applyClaudeConfig(this.currentClaudeConfig);
}
}
},
currentClaudeOpusModel: {
get() {
const config = this.claudeConfigs && this.claudeConfigs[this.currentClaudeConfig];
return (config && config.opusModel) || this.currentClaudeModel || '';
},
set(val) {
const config = this.claudeConfigs && this.claudeConfigs[this.currentClaudeConfig];
if (config) {
this.$set(config, 'opusModel', (val || '').trim());
this.saveClaudeConfigs();
if (this.currentClaudeConfig) this.applyClaudeConfig(this.currentClaudeConfig);
}
}
},
activeProviderModel() {
return (name) => {
const target = String(name || '').trim();
Expand Down
6 changes: 3 additions & 3 deletions web-ui/modules/app.methods.startup-claude.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ export function createStartupClaudeMethods(options = {}) {
this.maybeShowStarPrompt();
return true;
} catch (e) {
this.initError = e && e.message === 'timeout'
? '读取配置超时'
: '连接失败: ' + (e && e.message ? e.message : '');
if (e && e.message !== 'timeout') {
this.initError = '连接失败: ' + (e.message || '');
}
Comment on lines +148 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Timeout errors are now silent—users will see no failure indication.

When e.message === 'timeout', the condition e.message !== 'timeout' is false, so the assignment to this.initError is skipped entirely. Since initError was cleared to an empty string at line 50, it remains empty. Combined with loading = false (line 154), the UI will display an "ok" health status (per the inspectorHealthStatus computed property) even though initialization failed due to timeout.

Additionally, line 402 in refreshClaudeSelectionFromSettings within this same file still treats timeout errors specially:

this.showMessage(e && e.message === 'timeout' ? '读取配置超时' : '读取配置失败', 'error');

This creates an inconsistency across methods.

🔧 Proposed fix to restore timeout error visibility

Option 1: Restore distinct timeout error message (maintains previous behavior):

                } catch (e) {
-                    if (e && e.message !== 'timeout') {
+                    if (e && e.message === 'timeout') {
+                        this.initError = '读取配置超时';
+                    } else if (e) {
                        this.initError = '连接失败: ' + (e.message || '');
                    }
                    return false;

Option 2: Consolidate all errors under one message (if timeout distinction is unnecessary):

                } catch (e) {
-                    if (e && e.message !== 'timeout') {
+                    if (e) {
                        this.initError = '连接失败: ' + (e.message || '');
                    }
                    return false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.startup-claude.mjs` around lines 148 - 150, The
init flow currently suppresses timeout errors (the block with this.initError
assignment skips when e.message === 'timeout'), leaving initError empty and
causing inspectorHealthStatus to report OK; update the error handling in the
method that sets this.initError so that timeouts also set a visible message
(e.g., set this.initError = e && e.message === 'timeout' ? '连接超时' : '连接失败: ' +
(e.message || '')) and make the message consistent with
refreshClaudeSelectionFromSettings (or alternatively unify both places to use a
single generic failure message) so timeout and other errors are shown uniformly;
touch the initError assignment and refreshClaudeSelectionFromSettings logic to
ensure consistency and preserve loading state handling.

return false;
} finally {
if (!preserveLoading) {
Expand Down
4 changes: 4 additions & 0 deletions web-ui/modules/i18n/locales/en.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,10 @@ const en = Object.freeze({
'claude.model': 'Model',
'claude.model.placeholder': 'e.g. claude-3-7-sonnet',
'claude.model.hint': 'Model changes are saved and applied to the current config automatically.',
'claude.model.haiku': 'Haiku Model',
'claude.model.sonnet': 'Sonnet Model',
'claude.model.opus': 'Opus Model',
'claude.model.sub.placeholder': 'Defaults to the main model if left empty',
'claude.targetApi.label': 'Target API',
'claude.targetApi.responses': 'Anthropic',
'claude.targetApi.chatCompletions': 'OpenAI Chat Completions (/v1/chat/completions)',
Expand Down
4 changes: 4 additions & 0 deletions web-ui/modules/i18n/locales/ja.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,10 @@ const ja = Object.freeze({
'claude.model': 'モデル',
'claude.model.placeholder': '例: claude-3-7-sonnet',
'claude.model.hint': 'モデル変更後は自動保存され、現在の設定に適用されます。',
'claude.model.haiku': 'Haiku モデル',
'claude.model.sonnet': 'Sonnet モデル',
'claude.model.opus': 'Opus モデル',
'claude.model.sub.placeholder': '空欄の場合、メインモデルに従います',
'claude.targetApi.label': 'ターゲット API',
'claude.targetApi.responses': 'Anthropic',
'claude.targetApi.chatCompletions': 'OpenAI Chat Completions (/v1/chat/completions)',
Expand Down
4 changes: 4 additions & 0 deletions web-ui/modules/i18n/locales/vi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ const vi = Object.freeze({
'validation.claude.baseUrlRequired': 'Base URL là bắt buộc',
'validation.claude.baseUrlHttpOnly': 'Base URL chỉ hỗ trợ http/https',
'validation.claude.modelRequired': 'Tên mô hình là bắt buộc',
'claude.model.haiku': 'Mô hình Haiku',
'claude.model.sonnet': 'Mô hình Sonnet',
'claude.model.opus': 'Mô hình Opus',
'claude.model.sub.placeholder': 'Để trống sẽ dùng mô hình chính',
'modal.claudeDelete.title': 'Xóa cấu hình Claude',
'modal.claudeDelete.message': 'Xóa cấu hình "{name}"?',
'modal.claudeDelete.confirm': 'Xóa',
Expand Down
4 changes: 4 additions & 0 deletions web-ui/modules/i18n/locales/zh-tw.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,10 @@ const zhTw = Object.freeze({
'claude.model': '模型',
'claude.model.placeholder': '例如: claude-3-7-sonnet',
'claude.model.hint': '模型修改後會自動保存並應用到目前設定。',
'claude.model.haiku': 'Haiku 模型',
'claude.model.sonnet': 'Sonnet 模型',
'claude.model.opus': 'Opus 模型',
'claude.model.sub.placeholder': '留空則跟隨主模型',
'claude.targetApi.label': '目標 API',
'claude.targetApi.responses': 'Anthropic',
'claude.targetApi.chatCompletions': 'OpenAI Chat Completions (/v1/chat/completions)',
Expand Down
4 changes: 4 additions & 0 deletions web-ui/modules/i18n/locales/zh.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,10 @@ const zh = Object.freeze({
'claude.model': '模型',
'claude.model.placeholder': '例如: claude-3-7-sonnet',
'claude.model.hint': '模型修改后会自动保存并应用到当前配置。',
'claude.model.haiku': 'Haiku 模型',
'claude.model.sonnet': 'Sonnet 模型',
'claude.model.opus': 'Opus 模型',
'claude.model.sub.placeholder': '留空则跟随主模型',
'claude.targetApi.label': '目标 API',
'claude.targetApi.responses': 'Anthropic',
'claude.targetApi.chatCompletions': 'OpenAI Chat Completions (/v1/chat/completions)',
Expand Down
33 changes: 33 additions & 0 deletions web-ui/partials/index/panel-config-claude.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,39 @@
<button class="btn-tool btn-template-editor" @click="openClaudeConfigTemplateEditor" :disabled="loading || !!initError">{{ t('config.template.openEditor') }}</button>
</div>

<div class="selector-section">
<div class="selector-header"><span class="selector-title">{{ t('claude.model.haiku') }}</span></div>
<input
class="model-input"
v-model.lazy="currentClaudeHaikuModel"
:placeholder="t('claude.model.sub.placeholder')"
:readonly="currentClaudeConfig === 'claude-local'"
list="claude-model-options"
>
</div>

<div class="selector-section">
<div class="selector-header"><span class="selector-title">{{ t('claude.model.sonnet') }}</span></div>
<input
class="model-input"
v-model.lazy="currentClaudeSonnetModel"
:placeholder="t('claude.model.sub.placeholder')"
:readonly="currentClaudeConfig === 'claude-local'"
list="claude-model-options"
>
</div>

<div class="selector-section">
<div class="selector-header"><span class="selector-title">{{ t('claude.model.opus') }}</span></div>
<input
class="model-input"
v-model.lazy="currentClaudeOpusModel"
:placeholder="t('claude.model.sub.placeholder')"
:readonly="currentClaudeConfig === 'claude-local'"
list="claude-model-options"
>
</div>

<div class="selector-section">
<div class="selector-header"><span class="selector-title">{{ t('config.health.title') }}</span></div>
<button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">{{ healthCheckLoading ? t('config.health.running') : t('config.health.run') }}</button>
Expand Down
57 changes: 57 additions & 0 deletions web-ui/res/web-ui-render.precompiled.js
Original file line number Diff line number Diff line change
Expand Up @@ -1882,6 +1882,63 @@ return function render(_ctx, _cache) {
disabled: _ctx.loading || !!_ctx.initError
}, _toDisplayString(_ctx.t('config.template.openEditor')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])
]),
_createElementVNode("div", { class: "selector-section" }, [
_createElementVNode("div", { class: "selector-header" }, [
_createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('claude.model.haiku')), 1 /* TEXT */)
]),
_withDirectives(_createElementVNode("input", {
class: "model-input",
"onUpdate:modelValue": $event => ((_ctx.currentClaudeHaikuModel) = $event),
placeholder: _ctx.t('claude.model.sub.placeholder'),
readonly: _ctx.currentClaudeConfig === 'claude-local',
list: "claude-model-options"
}, null, 8 /* PROPS */, ["onUpdate:modelValue", "placeholder", "readonly"]), [
[
_vModelText,
_ctx.currentClaudeHaikuModel,
void 0,
{ lazy: true }
]
])
]),
_createElementVNode("div", { class: "selector-section" }, [
_createElementVNode("div", { class: "selector-header" }, [
_createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('claude.model.sonnet')), 1 /* TEXT */)
]),
_withDirectives(_createElementVNode("input", {
class: "model-input",
"onUpdate:modelValue": $event => ((_ctx.currentClaudeSonnetModel) = $event),
placeholder: _ctx.t('claude.model.sub.placeholder'),
readonly: _ctx.currentClaudeConfig === 'claude-local',
list: "claude-model-options"
}, null, 8 /* PROPS */, ["onUpdate:modelValue", "placeholder", "readonly"]), [
[
_vModelText,
_ctx.currentClaudeSonnetModel,
void 0,
{ lazy: true }
]
])
]),
_createElementVNode("div", { class: "selector-section" }, [
_createElementVNode("div", { class: "selector-header" }, [
_createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('claude.model.opus')), 1 /* TEXT */)
]),
_withDirectives(_createElementVNode("input", {
class: "model-input",
"onUpdate:modelValue": $event => ((_ctx.currentClaudeOpusModel) = $event),
placeholder: _ctx.t('claude.model.sub.placeholder'),
readonly: _ctx.currentClaudeConfig === 'claude-local',
list: "claude-model-options"
}, null, 8 /* PROPS */, ["onUpdate:modelValue", "placeholder", "readonly"]), [
[
_vModelText,
_ctx.currentClaudeOpusModel,
void 0,
{ lazy: true }
]
])
]),
_createElementVNode("div", { class: "selector-section" }, [
_createElementVNode("div", { class: "selector-header" }, [
_createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('config.health.title')), 1 /* TEXT */)
Expand Down
Loading