Archived
fix(app): 配方的 analyze 走 traced 网关并回账用量 —— 一条问模型不花钱的路
装配层从前传的是裸 `deps.modelGateway`:`createModelGateway` 自己不做 `addConsumption`,计量全靠调用方给的 `onUsage` —— agent 那两条路都接了, 只有 analyze 漏了。后果两条:配方里 analyze 烧的 token 一个数都不计入活动预算 (预算这道闸对它形同虚设);时间轴上也不产生模型 span,「这一单问了几次模型」 看不见它。而 spec §5.2 白纸黑字写着「用量计入预算」。 现 `createAnalyzeGateway(tracedGateway, onAgentUsage)`,网关内**两次** generate (含重问那一次)的用量各记一笔 —— 失败的调用不是免费的。记账不吞异常: 记不上账等于预算失效,宁可当场停下由配方留痕交人。 debt.md 里那半句「它烧 token、计预算」当时是假的,改老实。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -50,4 +50,53 @@ describe('analyze 网关', () => {
|
||||
await expect(gateway.run(call)).rejects.toThrow(/出参不成形/);
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
* **两次调用的用量都要交回去记账**(2026-08-29 终审 C1)。
|
||||
* `createModelGateway` 自己不做 `addConsumption` —— 不给 `onUsage`,配方里的 analyze
|
||||
* 就是一条问模型不花钱的路,活动预算对它形同虚设。重问那一次照样烧真钱,一并记。
|
||||
*/
|
||||
it('用量交回 onUsage:第一次成的记一次,重问了记两次', async () => {
|
||||
const billed: Array<{ taskId: string; tokens: number }> = [];
|
||||
const once = createAnalyzeGateway(
|
||||
{
|
||||
generate: () =>
|
||||
Promise.resolve({ text: '', object: { items: [] }, usage: { inputTokens: 30, outputTokens: 12, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' }),
|
||||
} as never,
|
||||
(taskId, tokens) => { billed.push({ taskId, tokens }); return Promise.resolve(); },
|
||||
);
|
||||
await once.run(call);
|
||||
expect(billed).toEqual([{ taskId: TASK, tokens: 42 }]);
|
||||
|
||||
billed.length = 0;
|
||||
let n = 0;
|
||||
const twice = createAnalyzeGateway(
|
||||
{
|
||||
generate: () => {
|
||||
n += 1;
|
||||
return Promise.resolve(
|
||||
n === 1
|
||||
? { text: 'not json', usage: { inputTokens: 5, outputTokens: 5, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' }
|
||||
: { text: '', object: { items: [] }, usage: { inputTokens: 7, outputTokens: 3, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' },
|
||||
);
|
||||
},
|
||||
} as never,
|
||||
(taskId, tokens) => { billed.push({ taskId, tokens }); return Promise.resolve(); },
|
||||
);
|
||||
await twice.run(call);
|
||||
expect(billed.map((b) => b.tokens)).toEqual([10, 10]);
|
||||
});
|
||||
|
||||
it('两次都不成形也照样记两笔 —— 失败的调用不是免费的', async () => {
|
||||
const billed: number[] = [];
|
||||
const gateway = createAnalyzeGateway(
|
||||
{
|
||||
generate: () =>
|
||||
Promise.resolve({ text: 'nope', usage: { inputTokens: 4, outputTokens: 6, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' }),
|
||||
} as never,
|
||||
(_taskId, tokens) => { billed.push(tokens); return Promise.resolve(); },
|
||||
);
|
||||
await expect(gateway.run(call)).rejects.toThrow(/出参不成形/);
|
||||
expect(billed).toEqual([10, 10]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user