Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/driver-usage-omit-cost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gemstack/framework': patch
---

Fix the Claude Code driver reporting `costUsd: 0` when a result line carries token usage but no price. `DriverUsage.costUsd` is documented as omitted (never `0`) when there is no price, because the budget cap reads `0` as "this turn was free" rather than "the price is unknown" (#540). The driver now omits the field in that case, matching the Codex driver and the type's own contract. Claude runs that do report a price are unchanged.
18 changes: 18 additions & 0 deletions packages/framework/src/driver/claude-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ test('StreamJsonParser leaves usage off when the result line reports none (#322)
assert.deepEqual(p.result(), { text: 'done', sessionId: 's' })
})

test('StreamJsonParser omits costUsd (never 0) when tokens report but no price (#540)', () => {
const p = new StreamJsonParser()
p.push(
JSON.stringify({
type: 'result',
subtype: 'success',
result: 'done',
session_id: 's',
usage: { input_tokens: 100, output_tokens: 40, cache_read_input_tokens: 900, cache_creation_input_tokens: 50 },
}),
)
const { usage } = p.result()
// Tokens surface; costUsd is absent (unknown), not 0 (which the budget gate reads as free).
assert.equal(usage?.costUsd, undefined)
assert.equal(Object.prototype.hasOwnProperty.call(usage, 'costUsd'), false)
assert.deepEqual(usage, { inputTokens: 100, outputTokens: 40, cacheReadTokens: 900, cacheCreationTokens: 50 })
})

test('StreamJsonParser ignores non-JSON noise and falls back to assistant text', () => {
const p = new StreamJsonParser()
assert.deepEqual(p.push('some banner line'), [])
Expand Down
4 changes: 3 additions & 1 deletion packages/framework/src/driver/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,10 @@ function parseUsage(obj: Record<string, unknown>): DriverUsage | undefined {
if (typeof cost !== 'number' && !hasUsage) return undefined
const usage = (hasUsage ? raw : {}) as Record<string, unknown>
const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
// Omit costUsd when there is no price, never 0: the budget gate reads 0 as "free"
// and undefined as "unknown" (#540), and Codex reports tokens without a price.
return {
costUsd: typeof cost === 'number' && Number.isFinite(cost) ? cost : 0,
...(typeof cost === 'number' && Number.isFinite(cost) ? { costUsd: cost } : {}),
inputTokens: num(usage['input_tokens']),
outputTokens: num(usage['output_tokens']),
cacheReadTokens: num(usage['cache_read_input_tokens']),
Expand Down
Loading