Skip to content

Commit 4fa83be

Browse files
authored
feat(claude): tier model inputs, silent config timeout (#194)
* feat(claude): add tier-specific model inputs (Haiku/Sonnet/Opus) Add three configurable model fields for Claude Code's tier-specific environment variables (ANTHROPIC_DEFAULT_HAIKU_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL) in the Claude config panel. Each defaults to the main model value when left empty. Existing provider configs are backward-compatible via runtime normalization. * fix(web-ui): silently retry on config load timeout Do not show initError when config load times out; keep existing UI state and let background requests complete naturally. * fix(web-ui): include sub-models in apply cache key Add haikuModel/sonnetModel/opusModel to _claudeKey2 so changing only sub-models correctly triggers API sync instead of being skipped. * fix(web-ui): remove tier model inputs, fix Claude template editor JSON diff - Remove Haiku/Sonnet/Opus tier model input fields from Claude config UI - Sync ANTHROPIC_DEFAULT_HAIKU/SONNET/OPUS_MODEL env vars from main model - Fix Claude template editor diff preview parsing JSON as TOML - Add buildClaudeSettingsDiff endpoint for JSON-based settings diff - Branch prepareConfigTemplateDiff by configTemplateContext * chore: bump version to 0.0.48 * fix(web-ui): rebuild precompiled render with locked compiler version The precompiled render was built with @vue/compiler-dom@3.5.34 but CI uses npm ci which installs 3.5.30 from package-lock.json, producing a different output. Rebuilt with the locked version. --------- Co-authored-by: ymkiux <ymkiux@users.noreply.github.com>
1 parent fffd1cc commit 4fa83be

14 files changed

Lines changed: 188 additions & 13 deletions

cli.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2319,6 +2319,42 @@ function buildConfigTemplateDiff(params = {}) {
23192319
};
23202320
}
23212321

2322+
function buildClaudeSettingsDiff(params = {}) {
2323+
const content = typeof params.content === 'string' ? params.content : '';
2324+
if (!content.trim()) {
2325+
return { error: 'JSON 内容不能为空' };
2326+
}
2327+
if (content.length > 1024 * 1024) {
2328+
return { error: '内容过大(最大 1MB)' };
2329+
}
2330+
let parsed;
2331+
try {
2332+
parsed = JSON.parse(content);
2333+
} catch (e) {
2334+
return { error: `JSON 解析失败: ${e.message}` };
2335+
}
2336+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2337+
return { error: 'JSON 内容必须是一个对象' };
2338+
}
2339+
let beforeText = '';
2340+
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
2341+
try {
2342+
beforeText = fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
2343+
} catch (e) {
2344+
return { error: `读取 settings.json 失败: ${e.message}` };
2345+
}
2346+
}
2347+
const afterText = JSON.stringify(parsed, null, 2) + '\n';
2348+
const diff = buildLineDiff(beforeText, afterText);
2349+
const hasChanges = (diff.stats.added || 0) + (diff.stats.removed || 0) > 0;
2350+
return {
2351+
diff: {
2352+
...diff,
2353+
hasChanges
2354+
}
2355+
};
2356+
}
2357+
23222358
function addProviderToConfig(params = {}) {
23232359
const name = typeof params.name === 'string' ? params.name.trim() : '';
23242360
const url = typeof params.url === 'string' ? params.url.trim() : '';
@@ -9528,6 +9564,12 @@ async function applyToClaudeSettings(config = {}) {
95289564
};
95299565
delete nextEnv.ANTHROPIC_AUTH_TOKEN;
95309566
delete nextEnv.CLAUDE_CODE_USE_KEY;
9567+
const subModels = {
9568+
ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
9569+
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
9570+
ANTHROPIC_DEFAULT_OPUS_MODEL: model
9571+
};
9572+
Object.assign(nextEnv, subModels);
95319573

95329574
const nextSettings = {
95339575
...currentSettings,
@@ -9546,7 +9588,10 @@ async function applyToClaudeSettings(config = {}) {
95469588
updatedKeys: [
95479589
'env.ANTHROPIC_API_KEY',
95489590
'env.ANTHROPIC_BASE_URL',
9549-
'env.ANTHROPIC_MODEL'
9591+
'env.ANTHROPIC_MODEL',
9592+
'env.ANTHROPIC_DEFAULT_HAIKU_MODEL',
9593+
'env.ANTHROPIC_DEFAULT_SONNET_MODEL',
9594+
'env.ANTHROPIC_DEFAULT_OPUS_MODEL'
95509595
]
95519596
};
95529597
if (proxyResult) {
@@ -11910,6 +11955,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1191011955
case 'get-claude-settings-raw':
1191111956
result = readClaudeSettingsRaw();
1191211957
break;
11958+
case 'preview-claude-settings-diff':
11959+
result = buildClaudeSettingsDiff(params || {});
11960+
break;
1191311961
case 'apply-claude-settings-raw':
1191411962
result = applyClaudeSettingsRaw(params || {});
1191511963
break;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codexmate",
3-
"version": "0.0.47",
3+
"version": "0.0.48",
44
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
55
"main": "cli.js",
66
"bin": {

tests/unit/claude-settings-sync.test.mjs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const __dirname = path.dirname(__filename);
99
const { createI18nMethods } = await import(
1010
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
1111
);
12+
const { createCodexConfigMethods } = await import(
13+
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'app.methods.codex-config.mjs'))
14+
);
1215
const {
1316
isLikelyBuiltinClaudeProxySettingsEnv,
1417
matchBuiltinClaudeProxyConfigFromSettings
@@ -1424,3 +1427,96 @@ test('MCP Claude config schema allows Ollama without API key only for ollama tar
14241427
assert.match(schemaSource, /then:\s*\{ required:\s*\['apiKey'\] \}/);
14251428
assert.doesNotMatch(schemaSource, /required:\s*\['apiKey'\],\s*additionalProperties/);
14261429
});
1430+
1431+
test('buildClaudeSettingsDiff uses JSON.parse instead of TOML for Claude settings', () => {
1432+
const fnMatch = cliSource.match(/function buildClaudeSettingsDiff\([\s\S]*?\n\}/);
1433+
assert(fnMatch, 'buildClaudeSettingsDiff function should exist in cli.js');
1434+
const fnBody = fnMatch[0];
1435+
assert.match(fnBody, /JSON\.parse\(content\)/);
1436+
assert.doesNotMatch(fnBody, /toml\.parse/);
1437+
assert.match(fnBody, /CLAUDE_SETTINGS_FILE/);
1438+
assert.match(fnBody, /buildLineDiff/);
1439+
});
1440+
1441+
test('buildClaudeSettingsDiff is wired up as preview-claude-settings-diff route', () => {
1442+
const routeIndex = cliSource.indexOf("'preview-claude-settings-diff'");
1443+
assert.notStrictEqual(routeIndex, -1, 'route should be registered');
1444+
const routeSnippet = cliSource.slice(routeIndex, routeIndex + 120);
1445+
assert.match(routeSnippet, /buildClaudeSettingsDiff/);
1446+
});
1447+
1448+
test('prepareConfigTemplateDiff calls preview-claude-settings-diff for Claude context', async () => {
1449+
const capturedCalls = [];
1450+
const methods = createCodexConfigMethods({
1451+
api: async (action, params) => {
1452+
capturedCalls.push({ action, params });
1453+
return {
1454+
diff: {
1455+
lines: [{ type: 'add', value: '{"env": {}}' }],
1456+
stats: { added: 1, removed: 0, unchanged: 0 },
1457+
hasChanges: true
1458+
}
1459+
};
1460+
},
1461+
getProviderConfigModeMeta() { return null; }
1462+
});
1463+
const context = {
1464+
...createI18nMethods(),
1465+
...methods,
1466+
lang: 'zh',
1467+
configTemplateContext: 'claude',
1468+
configTemplateContent: '{"env":{"ANTHROPIC_MODEL":"test"}}',
1469+
configTemplateDiffVisible: false,
1470+
configTemplateDiffLoading: false,
1471+
configTemplateDiffError: '',
1472+
configTemplateDiffLines: [],
1473+
configTemplateDiffStats: { added: 0, removed: 0, unchanged: 0 },
1474+
configTemplateDiffHasChangesValue: false,
1475+
configTemplateDiffFingerprint: ''
1476+
};
1477+
1478+
await methods.prepareConfigTemplateDiff.call(context);
1479+
1480+
assert.strictEqual(capturedCalls.length, 1);
1481+
assert.strictEqual(capturedCalls[0].action, 'preview-claude-settings-diff');
1482+
assert.strictEqual(capturedCalls[0].params.content, '{"env":{"ANTHROPIC_MODEL":"test"}}');
1483+
assert.strictEqual(context.configTemplateDiffLines.length, 1);
1484+
assert.strictEqual(context.configTemplateDiffHasChangesValue, true);
1485+
});
1486+
1487+
test('prepareConfigTemplateDiff calls preview-config-template-diff for codex context', async () => {
1488+
const capturedCalls = [];
1489+
const methods = createCodexConfigMethods({
1490+
api: async (action, params) => {
1491+
capturedCalls.push({ action, params });
1492+
return {
1493+
diff: {
1494+
lines: [{ type: 'context', value: 'model = "test"' }],
1495+
stats: { added: 0, removed: 0, unchanged: 1 },
1496+
hasChanges: false
1497+
}
1498+
};
1499+
},
1500+
getProviderConfigModeMeta() { return null; }
1501+
});
1502+
const context = {
1503+
...createI18nMethods(),
1504+
...methods,
1505+
lang: 'zh',
1506+
configTemplateContext: 'codex',
1507+
configTemplateContent: 'model = "test"',
1508+
configTemplateDiffVisible: false,
1509+
configTemplateDiffLoading: false,
1510+
configTemplateDiffError: '',
1511+
configTemplateDiffLines: [],
1512+
configTemplateDiffStats: { added: 0, removed: 0, unchanged: 0 },
1513+
configTemplateDiffHasChangesValue: false,
1514+
configTemplateDiffFingerprint: ''
1515+
};
1516+
1517+
await methods.prepareConfigTemplateDiff.call(context);
1518+
1519+
assert.strictEqual(capturedCalls.length, 1);
1520+
assert.strictEqual(capturedCalls[0].action, 'preview-config-template-diff');
1521+
assert.strictEqual(capturedCalls[0].params.template, 'model = "test"');
1522+
});

tests/unit/web-ui-behavior-parity.test.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,10 @@ test('captured bundled app skeleton only exposes expected data key drift versus
840840
'isCurrentLocalProvider',
841841
'localProviderEntry',
842842
'localProxyListenUrl',
843-
'localProxyUpstreamOptions'
843+
'localProxyUpstreamOptions',
844+
'currentClaudeHaikuModel',
845+
'currentClaudeSonnetModel',
846+
'currentClaudeOpusModel'
844847
];
845848
if (parityAgainstHead) {
846849
const allowedExtraComputedKeySet = new Set(allowedExtraCurrentComputedKeys);

web-ui/modules/app.methods.codex-config.mjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,9 +750,13 @@ export function createCodexConfigMethods(options = {}) {
750750
&& this._configTemplateDiffPreviewRequestToken === requestToken
751751
&& this.buildConfigTemplateDiffFingerprint() === requestFingerprint
752752
);
753-
const res = await api('preview-config-template-diff', {
754-
template: this.configTemplateContent
755-
});
753+
const res = this.configTemplateContext === 'claude'
754+
? await api('preview-claude-settings-diff', {
755+
content: this.configTemplateContent
756+
})
757+
: await api('preview-config-template-diff', {
758+
template: this.configTemplateContent
759+
});
756760
if (!shouldApply()) {
757761
return;
758762
}

web-ui/modules/app.methods.startup-claude.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ export function createStartupClaudeMethods(options = {}) {
145145
this.maybeShowStarPrompt();
146146
return true;
147147
} catch (e) {
148-
this.initError = e && e.message === 'timeout'
149-
? '读取配置超时'
150-
: '连接失败: ' + (e && e.message ? e.message : '');
148+
if (e && e.message !== 'timeout') {
149+
this.initError = '连接失败: ' + (e.message || '');
150+
}
151151
return false;
152152
} finally {
153153
if (!preserveLoading) {

web-ui/modules/i18n/locales/en.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ const en = Object.freeze({
486486
'modal.configTemplate.mode.twoStep': 'Two-step confirm: preview diff, then apply.',
487487
'modal.configTemplate.mode.oneStep': 'One-step apply: write immediately.',
488488
'diff.title.configTemplate': 'Diff preview (config.toml)',
489+
'diff.title.claudeSettings': 'Diff preview (settings.json)',
489490
'diff.generating': 'Generating...',
490491
'diff.failed': 'Failed',
491492
'diff.noChanges': 'No changes detected',
@@ -1210,6 +1211,10 @@ const en = Object.freeze({
12101211
'claude.model': 'Model',
12111212
'claude.model.placeholder': 'e.g. claude-3-7-sonnet',
12121213
'claude.model.hint': 'Model changes are saved and applied to the current config automatically.',
1214+
'claude.model.haiku': 'Haiku Model',
1215+
'claude.model.sonnet': 'Sonnet Model',
1216+
'claude.model.opus': 'Opus Model',
1217+
'claude.model.sub.placeholder': 'Defaults to the main model if left empty',
12131218
'claude.targetApi.label': 'Target API',
12141219
'claude.targetApi.responses': 'Anthropic',
12151220
'claude.targetApi.chatCompletions': 'OpenAI Chat Completions (/v1/chat/completions)',

web-ui/modules/i18n/locales/ja.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ const ja = Object.freeze({
488488
'modal.configTemplate.mode.twoStep': '二段階確認:先に差分をプレビューし、その後適用します。',
489489
'modal.configTemplate.mode.oneStep': '一段階適用:「適用」をクリックすると直接書き込みます。',
490490
'diff.title.configTemplate': '差分プレビュー(config.toml)',
491+
'diff.title.claudeSettings': '差分プレビュー(settings.json)',
491492
'diff.generating': '生成中...',
492493
'diff.failed': '生成失敗',
493494
'diff.noChanges': '変更が検出されませんでした',
@@ -1203,6 +1204,10 @@ const ja = Object.freeze({
12031204
'claude.model': 'モデル',
12041205
'claude.model.placeholder': '例: claude-3-7-sonnet',
12051206
'claude.model.hint': 'モデル変更後は自動保存され、現在の設定に適用されます。',
1207+
'claude.model.haiku': 'Haiku モデル',
1208+
'claude.model.sonnet': 'Sonnet モデル',
1209+
'claude.model.opus': 'Opus モデル',
1210+
'claude.model.sub.placeholder': '空欄の場合、メインモデルに従います',
12061211
'claude.targetApi.label': 'ターゲット API',
12071212
'claude.targetApi.responses': 'Anthropic',
12081213
'claude.targetApi.chatCompletions': 'OpenAI Chat Completions (/v1/chat/completions)',

web-ui/modules/i18n/locales/vi.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ const vi = Object.freeze({
362362
'validation.claude.baseUrlRequired': 'Base URL là bắt buộc',
363363
'validation.claude.baseUrlHttpOnly': 'Base URL chỉ hỗ trợ http/https',
364364
'validation.claude.modelRequired': 'Tên mô hình là bắt buộc',
365+
'claude.model.haiku': 'Mô hình Haiku',
366+
'claude.model.sonnet': 'Mô hình Sonnet',
367+
'claude.model.opus': 'Mô hình Opus',
368+
'claude.model.sub.placeholder': 'Để trống sẽ dùng mô hình chính',
365369
'modal.claudeDelete.title': 'Xóa cấu hình Claude',
366370
'modal.claudeDelete.message': 'Xóa cấu hình "{name}"?',
367371
'modal.claudeDelete.confirm': 'Xóa',

0 commit comments

Comments
 (0)