fix(runs): count a model run once when the terminal node carries its result on
A terminal node's result is the record it received, so after post_upgrade the run listed the same post-upgrade check twice and counted its cost twice (run view, run list and model costs). Identical calls now count once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d05b6c27f3
commit
bcd144fbdc
2 files changed
+46
-23
No files matched your search
@@ -27,4 +27,18 @@ describe('workflowModelCalls', () => {
|
||||
expect(calls[2]).toMatchObject({ instanceId: 'run-1', workflowId: 'update-radarr', summary: 'Clean start.', actions: ['hl ps'], at: '2026-09-22T21:35:00.000Z' });
|
||||
expect(totalCostUsd(calls)).toBeCloseTo(0.82);
|
||||
});
|
||||
|
||||
it('counts a model run once when a later node carries its result on, as the terminal node does', () => {
|
||||
const postUpgrade = { action: 'model.postUpgrade', state: 'completed', report: { outcome: 'nothing_to_do', summary: 'Clean start.', actions: ['hl ps'], model: 'sonnet', costUsd: 0.2 } };
|
||||
const notes = { verdict: 'attention', summary: 'A migration runs.', model: 'opus', costUsd: 0.12 };
|
||||
const calls = workflowModelCalls({ id: 'run-1', workflowId: 'update-radarr' }, [
|
||||
attempt(1, 'release_notes', notes),
|
||||
attempt(2, 'gate', notes),
|
||||
attempt(3, 'post_upgrade', postUpgrade),
|
||||
attempt(4, 'terminal_updated', postUpgrade),
|
||||
]);
|
||||
|
||||
expect(calls.map((call) => call.nodeId)).toEqual(['release_notes', 'post_upgrade']);
|
||||
expect(totalCostUsd(calls)).toBeCloseTo(0.32);
|
||||
});
|
||||
});
|
||||
@@ -22,34 +22,43 @@ const cost = (value: unknown): number => (typeof value === 'number' && Number.is
|
||||
/**
|
||||
* The model runs a workflow run made, read from its node attempts: a model action's result carries `report`
|
||||
* (app/privileged-action-executor-adapter), the release notes operation's result carries `verdict` and `model`.
|
||||
* A node that passes the record on (a terminal node's result is its input) repeats that result; the same call counts once.
|
||||
*/
|
||||
export function workflowModelCalls(instance: { readonly id: string; readonly workflowId: string }, attempts: readonly NodeAttempt[]): WorkflowModelCall[] {
|
||||
return attempts.flatMap((attempt): WorkflowModelCall[] => {
|
||||
const result = record(attempt.result);
|
||||
const at = attempt.finishedAt ?? attempt.startedAt;
|
||||
const base = { instanceId: instance.id, workflowId: instance.workflowId, nodeId: attempt.nodeId, at };
|
||||
const action = text(result['action']);
|
||||
if (action === 'model.postUpgrade' || action === 'model.repair') {
|
||||
const report = record(result['report']);
|
||||
if (Object.keys(report).length === 0) return [];
|
||||
return [{
|
||||
...base, kind: action === 'model.repair' ? 'repair' : 'postUpgrade', model: text(report['model']),
|
||||
outcome: text(report['outcome']) ?? 'unknown', summary: text(report['summary']) ?? '',
|
||||
actions: Array.isArray(report['actions']) ? report['actions'].filter((item): item is string => typeof item === 'string') : [],
|
||||
costUsd: cost(report['costUsd']),
|
||||
}];
|
||||
}
|
||||
// Only a release notes result that reached a model names one; "no source" and unreadable notes cost nothing.
|
||||
if ('verdict' in result && text(result['model']) !== null) {
|
||||
return [{
|
||||
...base, kind: 'releaseNotes', model: text(result['model']), outcome: text(result['verdict']) ?? 'unknown',
|
||||
summary: text(result['summary']) ?? '', actions: [], costUsd: cost(result['costUsd']),
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
const seen = new Set<string>();
|
||||
return attempts.flatMap((attempt) => modelCallOf(instance, attempt)).filter(({ nodeId: _nodeId, at: _at, ...call }) => {
|
||||
const key = JSON.stringify(call);
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function modelCallOf(instance: { readonly id: string; readonly workflowId: string }, attempt: NodeAttempt): WorkflowModelCall[] {
|
||||
const result = record(attempt.result);
|
||||
const at = attempt.finishedAt ?? attempt.startedAt;
|
||||
const base = { instanceId: instance.id, workflowId: instance.workflowId, nodeId: attempt.nodeId, at };
|
||||
const action = text(result['action']);
|
||||
if (action === 'model.postUpgrade' || action === 'model.repair') {
|
||||
const report = record(result['report']);
|
||||
if (Object.keys(report).length === 0) return [];
|
||||
return [{
|
||||
...base, kind: action === 'model.repair' ? 'repair' : 'postUpgrade', model: text(report['model']),
|
||||
outcome: text(report['outcome']) ?? 'unknown', summary: text(report['summary']) ?? '',
|
||||
actions: Array.isArray(report['actions']) ? report['actions'].filter((item): item is string => typeof item === 'string') : [],
|
||||
costUsd: cost(report['costUsd']),
|
||||
}];
|
||||
}
|
||||
// Only a release notes result that reached a model names one; "no source" and unreadable notes cost nothing.
|
||||
if ('verdict' in result && text(result['model']) !== null) {
|
||||
return [{
|
||||
...base, kind: 'releaseNotes', model: text(result['model']), outcome: text(result['verdict']) ?? 'unknown',
|
||||
summary: text(result['summary']) ?? '', actions: [], costUsd: cost(result['costUsd']),
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function totalCostUsd(calls: readonly WorkflowModelCall[]): number {
|
||||
return calls.reduce((sum, call) => sum + call.costUsd, 0);
|
||||
}
|
||||
Reference in new issue
Block a user