Skip to content

Commit 1cc0a96

Browse files
MichaelZelbelclaude
authored andcommitted
fix(usage-cost): surface unpriced-model spend as missingCostEntries instead of $0
Models that ship an all-zero cost block (e.g. codex gpt-5.5, whose Codex backend exposes no per-token price) made usage-cost report totalCost: 0 with missingCostEntries: 0 -- a confident, complete $0 -- so every budget/spike safeguard keyed off totalCost was silently blind to real pay-per-token spend. scanTranscriptFile now treats a resolved cost config with no positive per-token rate (and no tiered pricing) as "pricing unknown": for turns that burned tokens it drops the transport's fabricated $0 and surfaces the turn as a missing-cost entry, mirroring the existing tiered-pricing override. Models with positive or tiered pricing and zero-token entries are unaffected. Verified on a real OpenClaw 2026.5.20 host (default openai/gpt-5.5, api_key): 1,780,235 tokens that previously reported missingCostEntries 0 now report 32. Related: #85858 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent c4c80ce commit 1cc0a96

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

src/infra/session-cost-usage.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,64 @@ describe("session cost usage", () => {
149149
});
150150
});
151151

152+
it("counts token usage for an unpriced (all-zero cost) model as missing, not a confident $0", async () => {
153+
const root = await makeSessionCostRoot("cost-unknown-pricing");
154+
const sessionsDir = path.join(root, "agents", "main", "sessions");
155+
await fs.mkdir(sessionsDir, { recursive: true });
156+
157+
// A real assistant turn that burned tokens. The transport recorded cost.total: 0,
158+
// derived from the model's all-zero catalog pricing — exactly what codex/gpt-5.x
159+
// models produce, since the Codex backend exposes no per-token price.
160+
const entry = {
161+
type: "message",
162+
timestamp: new Date().toISOString(),
163+
message: {
164+
role: "assistant",
165+
provider: "openai",
166+
model: "gpt-5.5",
167+
usage: {
168+
input: 881,
169+
output: 6,
170+
cacheRead: 22400,
171+
cacheWrite: 0,
172+
totalTokens: 23287,
173+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
174+
},
175+
},
176+
};
177+
178+
await fs.writeFile(
179+
path.join(sessionsDir, "sess-1.jsonl"),
180+
transcriptText("sess-1", entry),
181+
"utf-8",
182+
);
183+
184+
// The model resolves to an all-zero cost config, i.e. its pricing is unknown.
185+
const config = {
186+
models: {
187+
providers: {
188+
openai: {
189+
models: [
190+
{
191+
id: "gpt-5.5",
192+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
193+
},
194+
],
195+
},
196+
},
197+
},
198+
} as unknown as OpenClawConfig;
199+
200+
await withStateDir(root, async () => {
201+
const summary = await loadCostUsageSummary({ days: 30, config });
202+
expect(summary.totals.totalTokens).toBe(23287);
203+
expect(summary.totals.totalCost).toBe(0);
204+
// Unknown pricing must be surfaced as missing rather than reported as a
205+
// confident $0 that would blind budget/spike monitoring to real spend.
206+
expect(summary.totals.missingCostEntries).toBe(1);
207+
});
208+
});
209+
152210
it("ignores compaction checkpoint transcript snapshots in daily totals and discovery", async () => {
153211
const root = await makeSessionCostRoot("cost-checkpoint");
154212
const sessionsDir = path.join(root, "agents", "main", "sessions");

src/infra/session-cost-usage.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,22 @@ const applyCostTotal = (totals: CostUsageTotals, costTotal: number | undefined)
10131013
totals.totalCost += costTotal;
10141014
};
10151015

1016+
// A resolved cost config only counts as "known" pricing when it carries at least one
1017+
// positive per-token rate (or tiered pricing). An all-zero config is indistinguishable
1018+
// from "pricing unknown": e.g. codex models ship cost {input:0,output:0,...} in the
1019+
// generated models.json because the Codex backend exposes no per-token price. Treating
1020+
// such a config as a real $0 makes usage-cost report confident zero spend, which
1021+
// silently blinds every budget/spike safeguard that keys off totalCost.
1022+
const isModelPricingKnown = (cost: ReturnType<typeof resolveModelCostConfig>): boolean => {
1023+
if (!cost) {
1024+
return false;
1025+
}
1026+
if (cost.tieredPricing && cost.tieredPricing.length > 0) {
1027+
return true;
1028+
}
1029+
return cost.input > 0 || cost.output > 0 || cost.cacheRead > 0 || cost.cacheWrite > 0;
1030+
};
1031+
10161032
async function canReadJsonlFromOffset(filePath: string, startOffset: number): Promise<boolean> {
10171033
if (startOffset <= 0) {
10181034
return true;
@@ -1099,6 +1115,17 @@ async function scanTranscriptFile(params: {
10991115
// instead of the stale flat-rate breakdown from the transport layer.
11001116
entry.costTotal = estimateUsageCost({ usage: entry.usage, cost });
11011117
entry.costBreakdown = undefined;
1118+
} else if (
1119+
!isModelPricingKnown(cost) &&
1120+
computeUsageTokenTotals(entry.usage).totalTokens > 0
1121+
) {
1122+
// Pricing for this model is unknown (no positive per-token rate). Any cost the
1123+
// transport recorded is a fabricated $0 derived from an all-zero catalog entry,
1124+
// not a real price. Drop it and surface the turn as a missing-cost entry so the
1125+
// tokens it burned are not reported as confident $0 spend — otherwise every
1126+
// budget/spike safeguard that reads totalCost stays blind to it.
1127+
entry.costTotal = undefined;
1128+
entry.costBreakdown = undefined;
11021129
} else if (entry.costTotal === undefined) {
11031130
// Fill in missing cost estimates.
11041131
entry.costTotal = estimateUsageCost({ usage: entry.usage, cost });

0 commit comments

Comments
 (0)