Skip to content

opencode stats over-reports Total Cost by double-counting forked sessions #36944

Description

@evatths

Summary

opencode stats computes Total Cost as SUM(session.cost). Forking a session copies the parent's prior messages into the new session (new message IDs, but the originaltime.created / cost / tokens). Those turns were never re-sent to the provider, so they were never re-billed — yet each fork's session.cost rollup includes them. The result: shared history is counted once per fork, inflating the reported total.

Version

opencode 1.17.20

Impact

opencode statsTotal Cost is over-reported roughly in proportion to how much a user forks. In one dataset the over-count was ~11%. Deduplicating on the billed event brings the total down accordingly.

The figures below are illustrative/synthetic (not real usage), included only to show the relationship between the values. The ~11% over-count is the real observed magnitude.

sessions : 1200
opencode stats (SUM session.cost): $4,000.00
SUM(message.cost) raw : $4,060.00
deduplicated billed events : $3,556.00
OVER-COUNT vs reality : $444.00 (11.1%)

opencode stats matches SUM(session.cost)exactly, confirming that is the aggregate used.

Root cause

Cost is a property of a billed event, but the aggregate keys on the row/session, which forking duplicates. Note session.parent_id is often NULL on the copies, so lineage can't be relied on to detect them, and "different message IDs" does not imply independent spend — copies always get new IDs.

Concrete pattern (illustrative numbers): groups of sessions are byte-identical copies of one another (same message count, same per-message timestamps and cost) — i.e. forks that never diverged. Each copy's session.cost is added to the total again:

Identical-copy session clusters (same billed events, counted N times):
cluster #1: x3 @ $30.00 -> $60.00 phantom
cluster #2: x2 @ $12.00 -> $12.00 phantom
cluster #3: x3 @ $8.00 -> $16.00 phantom
cluster #4: x2 @ $2.50 -> $2.50 phantom

(These are only the forks that never diverged; continued forks also duplicate their inherited prefix, which typically accounts for the bulk of the over-count.)

Repro

cp ~/.local/share/opencode/opencode.db /tmp/repro.db # DB is live; snapshot it
python3 fork_cost_double_count_repro.py /tmp/repro.db
fork_cost_double_count_repro.py
#!/usr/bin/env python3"""Repro: `opencode stats` (== SUM(session.cost)) double-counts forked sessions."""importsqlite3, json, sys, collections, hashlibdb=sqlite3.connect(sys.argv[1] iflen(sys.argv) >1else"/tmp/repro.db")
db.row_factory=sqlite3.Rowstats_total=db.execute("SELECT COALESCE(SUM(cost),0) FROM session").fetchone()[0]
n_sessions=db.execute("SELECT COUNT(*) FROM session").fetchone()[0]
# De-duplicate on the *billed event* (immutable properties of one generation).raw=dedup=0.0seen=set()
forrindb.execute("SELECT data FROM message"):
d=json.loads(r["data"]); c=d.get("cost") or0.0ifc<=0: continuet=d.get("time") or {}; tk=d.get("tokens") or {}; ca=tk.get("cache") or {}
fp= (t.get("created"), d.get("providerID"), d.get("modelID"),
tk.get("input"), tk.get("output"), tk.get("reasoning"),
ca.get("read"), ca.get("write"), round(c, 10))
raw+=ciffpnotinseen:
seen.add(fp); dedup+=cprint(f"sessions : {n_sessions}")
print(f"opencode stats (SUM session.cost): ${stats_total:,.2f}")
print(f"SUM(message.cost) raw : ${raw:,.2f}")
print(f"deduplicated billed events : ${dedup:,.2f}")
print(f"OVER-COUNT vs reality : ${stats_total-dedup:,.2f} "f"({100*(stats_total-dedup)/stats_total:.1f}%)")
# Concrete example: byte-identical copy clusters (forks that never diverged).ev=collections.defaultdict(list)
forrindb.execute("SELECT session_id, data FROM message"):
d=json.loads(r["data"]); c=d.get("cost") or0.0ifc<=0: continueev[r["session_id"]].append(((d.get("time") or {}).get("created"), round(c, 10)))
cost_of= {r["id"]: (r["cost"] or0.0) forrindb.execute("SELECT id,cost FROM session")}
clusters=collections.defaultdict(list)
forsid, einev.items():
e.sort(); clusters[hashlib.md5(json.dumps(e).encode()).hexdigest()].append(sid)
print("\nIdentical-copy session clusters (same billed events, counted N times):")
rows= [((len(ss)-1)*cost_of[ss[0]], len(ss), cost_of[ss[0]])
forssinclusters.values() iflen(ss) >1]
fori, (extra, n, cost) inenumerate(sorted(rows, reverse=True)[:8], 1):
print(f" cluster #{i}: x{n} @ ${cost:.2f} -> ${extra:.2f} phantom")

Suggested fix

Deduplicate on a billed-event fingerprint before summing — e.g. (time.created, providerID, modelID, tokens.input, tokens.output, tokens.reasoning, tokens.cache.read, tokens.cache.write, cost) — or give inherited (copied) messages a zero rollup cost in forks so each event is counted only in its originating session.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions