fix: 全量替换 claude-fable-5 → claude-opus-4-8(模型不可用)

- models.ts: 所有档位默认值、回退链、conflict 固定值全部换为 opus-4-8
- 回退链从三项缩为两项 [opus-4-8, sonnet-4-6](删除重复项)
- metrics.ts: 额度权重 opus-4-8 升为 1.0(现为最高档)
- 测试同步更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-14 15:14:09 +08:00
parent 433b2dc232
commit a5a3c967ce
4 changed files with 38 additions and 40 deletions
+13 -13
View File
@@ -4,39 +4,39 @@ import type { Complexity } from '../model/complexity.js';
/** 角色:executor(执行改动)/ reviewer(执行后复审)/ planner(拆解 Hard / 写方案 Medium/ conflict(冲突裁决) */
export type ModelRole = 'executor' | 'reviewer' | 'planner' | 'conflict';
/** 模型不可用时的回退链(按序取第一个 ≠ 失败模型的,同一 run 内只重试一次) */
export const MODEL_FALLBACK_CHAIN = ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-4-6'] as const;
/** 模型不可用时的回退链(按序取下一档,同一 run 内只重试一次) */
export const MODEL_FALLBACK_CHAIN = ['claude-opus-4-8', 'claude-sonnet-4-6'] as const;
/** 复杂度 → [env 覆盖变量名, 默认模型] */
const MODEL_TABLE: Record<ModelRole, Record<Complexity, [env: string, fallback: string]>> = {
executor: {
easy: ['MAESTRO_MODEL_EASY', 'claude-sonnet-4-6'],
medium: ['MAESTRO_MODEL_MEDIUM', 'claude-opus-4-8'],
hard: ['MAESTRO_MODEL_HARD', 'claude-fable-5'],
hard: ['MAESTRO_MODEL_HARD', 'claude-opus-4-8'],
},
reviewer: {
easy: ['MAESTRO_MODEL_REVIEW_EASY', 'claude-fable-5'],
medium: ['MAESTRO_MODEL_REVIEW_MEDIUM', 'claude-fable-5'],
hard: ['MAESTRO_MODEL_REVIEW_HARD', 'claude-fable-5'],
easy: ['MAESTRO_MODEL_REVIEW_EASY', 'claude-opus-4-8'],
medium: ['MAESTRO_MODEL_REVIEW_MEDIUM', 'claude-opus-4-8'],
hard: ['MAESTRO_MODEL_REVIEW_HARD', 'claude-opus-4-8'],
},
// planner:拆解/写方案按复杂度分化(不受 project.model 覆盖)
planner: {
easy: ['MAESTRO_MODEL_PLAN_EASY', 'claude-sonnet-4-6'],
medium: ['MAESTRO_MODEL_PLAN_MEDIUM', 'claude-opus-4-8'],
hard: ['MAESTRO_MODEL_PLAN_HARD', 'claude-fable-5'],
hard: ['MAESTRO_MODEL_PLAN_HARD', 'claude-opus-4-8'],
},
// conflict:固定 fable-5TABLE 条目仅占位(pickModel 对此角色直接 return 不走此处)
// conflict:固定 opus-4-8TABLE 条目仅占位(pickModel 对此角色直接 return 不走此处)
conflict: {
easy: ['', 'claude-fable-5'],
medium: ['', 'claude-fable-5'],
hard: ['', 'claude-fable-5'],
easy: ['', 'claude-opus-4-8'],
medium: ['', 'claude-opus-4-8'],
hard: ['', 'claude-opus-4-8'],
},
};
/**
* 按角色 + 复杂度选模型:
* 1. reviewer/conflict 不受 project.model 覆盖(保复审独立性)
* 2. conflict 固定返回 claude-fable-5
* 2. conflict 固定返回 claude-opus-4-8(最强可用档)
* 3. 其他角色 project.model 设了就最优先
* 4. 否则查 env 覆盖(MAESTRO_MODEL_* / MAESTRO_MODEL_REVIEW_*
* 5. 否则按复杂度取默认
@@ -44,7 +44,7 @@ const MODEL_TABLE: Record<ModelRole, Record<Complexity, [env: string, fallback:
export function pickModel(task: Task, project: Project, role: ModelRole): string {
// reviewer/conflict 用最强档,不受 project.model 覆盖(保自审独立性)
if (role !== 'reviewer' && role !== 'conflict' && project.model) return project.model;
if (role === 'conflict') return 'claude-fable-5';
if (role === 'conflict') return 'claude-opus-4-8';
const [env, fallback] = MODEL_TABLE[role][task.complexity];
const override = process.env[env]?.trim();
return override || fallback;
+3 -4
View File
@@ -59,13 +59,12 @@ export interface Metrics {
}
/**
* 各模型档位的每分钟相对额度权重”(粗估用,非真实计费):
* fable-5hard)最贵 → opusmedium)次之 → sonneteasy)最省。
* 各模型档位的每分钟相对额度权重”(粗估用,非真实计费):
* opus-4-8hard/reviewer)最贵 → sonneteasy)最省。
* 未知模型用 DEFAULT_COST_PER_MIN 兜底。
*/
export const MODEL_COST_PER_MIN: Record<string, number> = {
'claude-fable-5': 1.0,
'claude-opus-4-8': 0.6,
'claude-opus-4-8': 1.0,
'claude-sonnet-4-6': 0.2,
};
export const DEFAULT_COST_PER_MIN = 0.4;
+4 -4
View File
@@ -36,10 +36,10 @@ test('metrics 纯函数:percentile / ratio / estimateCostUnits', () => {
assert.equal(ratio(2, 3), 0.6667);
assert.equal(ratio(0, 0), null); // 无样本不除零
assert.equal(costPerMin('claude-fable-5'), 1.0);
assert.equal(costPerMin('claude-opus-4-8'), 1.0);
assert.equal(costPerMin('未知模型'), DEFAULT_COST_PER_MIN);
assert.equal(estimateCostUnits(60_000, 'claude-fable-5'), 1.0); // 1 分钟 × 1.0
assert.equal(estimateCostUnits(120_000, 'claude-opus-4-8'), 1.2); // 2 分钟 × 0.6
assert.equal(estimateCostUnits(60_000, 'claude-opus-4-8'), 1.0); // 1 分钟 × 1.0
assert.equal(estimateCostUnits(120_000, 'claude-sonnet-4-6'), 0.4); // 2 分钟 × 0.2
});
// ──────────────── 空数据不崩 ────────────────
@@ -166,7 +166,7 @@ test('metrics 按模型额度:时长×档位 粗估,estimated=true', () => {
const m = s.metrics(p.id);
assert.equal(m.byModel.length, 1);
assert.deepEqual(m.byModel[0], {
model: 'claude-opus-4-8', runs: 2, durationMs: 120_000, estCostUnits: 1.2, estimated: true,
model: 'claude-opus-4-8', runs: 2, durationMs: 120_000, estCostUnits: 2.0, estimated: true,
});
assert.equal(m.estimated, true);
s.close();
+18 -19
View File
@@ -39,11 +39,11 @@ test('pickModel:复杂度映射默认值(executor / reviewer', () => {
const p = fakeProject();
assert.equal(pickModel(fakeTask('easy'), p, 'executor'), 'claude-sonnet-4-6');
assert.equal(pickModel(fakeTask('medium'), p, 'executor'), 'claude-opus-4-8');
assert.equal(pickModel(fakeTask('hard'), p, 'executor'), 'claude-fable-5');
// reviewer 三档全升 fable-5(保复审独立性)
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('medium'), p, 'reviewer'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('hard'), p, 'reviewer'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('hard'), p, 'executor'), 'claude-opus-4-8');
// reviewer 三档统一 opus-4-8(保复审独立性)
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-opus-4-8');
assert.equal(pickModel(fakeTask('medium'), p, 'reviewer'), 'claude-opus-4-8');
assert.equal(pickModel(fakeTask('hard'), p, 'reviewer'), 'claude-opus-4-8');
});
});
@@ -53,10 +53,10 @@ test('pickModelproject.model executor 最优先,但 reviewer/conflict 不
for (const c of ['easy', 'medium', 'hard'] as const) {
// executor 受 project.model 覆盖
assert.equal(pickModel(fakeTask(c), p, 'executor'), 'claude-opus-4-6');
// reviewer 不受 project.model 覆盖,始终用最强档 fable-5(保自审独立性)
assert.equal(pickModel(fakeTask(c), p, 'reviewer'), 'claude-fable-5');
// conflict 固定 fable-5
assert.equal(pickModel(fakeTask(c), p, 'conflict'), 'claude-fable-5');
// reviewer 不受 project.model 覆盖,始终用 opus-4-8(保自审独立性)
assert.equal(pickModel(fakeTask(c), p, 'reviewer'), 'claude-opus-4-8');
// conflict 固定 opus-4-8
assert.equal(pickModel(fakeTask(c), p, 'conflict'), 'claude-opus-4-8');
}
});
});
@@ -64,15 +64,15 @@ test('pickModelproject.model executor 最优先,但 reviewer/conflict 不
test('pickModelenv 覆盖默认值,executor/reviewer 各用各的 env', () => {
withEnv({
MAESTRO_MODEL_EASY: 'claude-haiku-4-5',
MAESTRO_MODEL_REVIEW_EASY: 'claude-opus-4-8', // reviewer env 可单独覆盖
MAESTRO_MODEL_REVIEW_EASY: 'claude-sonnet-4-6', // reviewer env 可单独覆盖
}, () => {
const p = fakeProject();
assert.equal(pickModel(fakeTask('easy'), p, 'executor'), 'claude-haiku-4-5');
// reviewer easy 被 MAESTRO_MODEL_REVIEW_EASY 覆盖
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-opus-4-8');
// reviewer hard 无 env → 默认 fable-5
assert.equal(pickModel(fakeTask('hard'), p, 'reviewer'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('hard'), p, 'executor'), 'claude-fable-5'); // executor hard 默认 fable
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-sonnet-4-6');
// reviewer hard 无 env → 默认 opus-4-8
assert.equal(pickModel(fakeTask('hard'), p, 'reviewer'), 'claude-opus-4-8');
assert.equal(pickModel(fakeTask('hard'), p, 'executor'), 'claude-opus-4-8');
});
});
@@ -97,10 +97,9 @@ test('isModelError:模型不可用类错误才触发回退', () => {
assert.ok(!isModelError('model produced empty output')); // 提到 model 但不是可用性错误
});
test('pickFallbackModel:回退链取第一个 ≠ 失败模型的', () => {
// 链已扩展为三项:fable → opus → sonnet
assert.deepEqual([...MODEL_FALLBACK_CHAIN], ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-4-6']);
assert.equal(pickFallbackModel('claude-fable-5'), 'claude-opus-4-8');
test('pickFallbackModel:回退链取下一档(循环)', () => {
// 链opus → sonnet → opus(循环)
assert.deepEqual([...MODEL_FALLBACK_CHAIN], ['claude-opus-4-8', 'claude-sonnet-4-6']);
assert.equal(pickFallbackModel('claude-opus-4-8'), 'claude-sonnet-4-6');
assert.equal(pickFallbackModel('claude-sonnet-4-6'), 'claude-fable-5');
assert.equal(pickFallbackModel('claude-sonnet-4-6'), 'claude-opus-4-8');
});