Skip to content

Commit 1fe842e

Browse files
authored
Merge pull request #23 from comet-ml/jacques/OPIK-6873-skip-synthetic-calls
fix: skip synthetic zero-usage assistant entries in billing
2 parents 4d06de1 + 9154c1a commit 1fe842e

6 files changed

Lines changed: 59 additions & 2 deletions

File tree

bin/opik-logger-darwin-amd64

802 KB
Binary file not shown.

bin/opik-logger-darwin-arm64

505 KB
Binary file not shown.

bin/opik-logger-linux-amd64

1020 KB
Binary file not shown.

bin/opik-logger-windows-amd64.exe

1.06 MB
Binary file not shown.

src/billing.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ type billingCall struct {
120120
// with usage and the message's contiguous entry span within fullEntries.
121121
// The transcript repeats the same usage on every entry of a multi-block
122122
// message, so usage is taken once from the first entry seen.
123+
//
124+
// All-zero usage means the API never billed the call. Claude Code writes
125+
// such entries locally (`model:"<synthetic>"`, isApiErrorMessage) when a
126+
// request errors or is interrupted; treating one as a real call reconciles
127+
// the full history layout against a zero-token prompt and dumps the
128+
// usage-derived pieces into the fresh-input tier as phantom tokens.
123129
func llmCallsInTurn(fullEntries, turnEntries []TranscriptEntry) []billingCall {
124130
offset := len(fullEntries) - len(turnEntries)
125131
var calls []billingCall
@@ -136,10 +142,10 @@ func llmCallsInTurn(fullEntries, turnEntries []TranscriptEntry) []billingCall {
136142
calls[pos].entryEnd = offset + i + 1
137143
continue
138144
}
139-
if e.Message.Usage == nil {
145+
u := e.Message.Usage
146+
if u == nil || u.InputTokens+u.CacheReadInputTokens+u.CacheCreationInputTokens+u.OutputTokens == 0 {
140147
continue
141148
}
142-
u := e.Message.Usage
143149
index[id] = len(calls)
144150
calls = append(calls, billingCall{
145151
entryIdx: offset + i,

src/billing_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,54 @@ func TestBillingExactOvershootIsClamped(t *testing.T) {
369369
read, write, fresh, output, wantRead, wantFresh, wantOut, rows)
370370
}
371371
}
372+
373+
// Claude Code writes locally fabricated assistant entries (model
374+
// "<synthetic>", isApiErrorMessage) with an all-zero usage object when an
375+
// API call errors. They were never billed: treating one as a real call
376+
// reconciles the whole history against a zero-token prompt and dumps the
377+
// usage-derived pieces into the fresh-input tier.
378+
func TestBillingSkipsSyntheticZeroUsageCalls(t *testing.T) {
379+
u1 := &Usage{InputTokens: 100, CacheCreationInputTokens: 8_000, OutputTokens: 60_000}
380+
entries := []TranscriptEntry{userPromptEntry("do the thing")}
381+
entries = append(entries, assistantCall(t, "m1", u1,
382+
Content{Type: "thinking", Thinking: "redacted"},
383+
Content{Type: "text", Text: "working on it"},
384+
)...)
385+
386+
// The synthetic error entry: zero usage, full history would be "its
387+
// request" — must be ignored entirely.
388+
entries = append(entries, assistantCall(t, "synthetic-1", &Usage{},
389+
Content{Type: "text", Text: "API error: request interrupted"},
390+
)...)
391+
392+
u2 := &Usage{InputTokens: 50, CacheReadInputTokens: 70_000,
393+
CacheCreationInputTokens: 2_000, OutputTokens: 40}
394+
entries = append(entries, assistantCall(t, "m2", u2, Content{Type: "text", Text: "done"})...)
395+
396+
snap := computeBillingSnapshot(entries, entries)
397+
if snap == nil {
398+
t.Fatal("expected billing snapshot")
399+
}
400+
if got := snap["llm_calls"].(int); got != 2 {
401+
t.Fatalf("llm_calls = %d, want 2 (synthetic call must be skipped)", got)
402+
}
403+
404+
wantRead := u1.CacheReadInputTokens + u2.CacheReadInputTokens
405+
wantWrite := u1.CacheCreationInputTokens + u2.CacheCreationInputTokens
406+
wantFresh := u1.InputTokens + u2.InputTokens
407+
wantOut := u1.OutputTokens + u2.OutputTokens
408+
409+
read, write, fresh, output, rows := billingColumnSums(snap)
410+
closeEnough := func(got, want int) bool {
411+
d := got - want
412+
if d < 0 {
413+
d = -d
414+
}
415+
return d <= rows
416+
}
417+
if !closeEnough(read, wantRead) || !closeEnough(write, wantWrite) ||
418+
!closeEnough(fresh, wantFresh) || !closeEnough(output, wantOut) {
419+
t.Errorf("Σ lanes = read %d / write %d / fresh %d / output %d, want %d/%d/%d/%d (±%d)",
420+
read, write, fresh, output, wantRead, wantWrite, wantFresh, wantOut, rows)
421+
}
422+
}

0 commit comments

Comments
 (0)