Skip to content

feat(usage): opencode transcript source for the usage scorecard - #89

Merged
pacphi merged 1 commit into
pacphi:mainfrom
robertelee78:feat/opencode-usage-scorecard
Jul 29, 2026
Merged

feat(usage): opencode transcript source for the usage scorecard#89
pacphi merged 1 commit into
pacphi:mainfrom
robertelee78:feat/opencode-usage-scorecard

Conversation

@robertelee78

Copy link
Copy Markdown
Contributor

Summary

The usage scorecard read claude/codex transcripts only — opencode sessions didn't exist anywhere in usage/cost attribution, the biggest remaining "second-class" remnant of the third host. opencode persists everything the index needs in ~/.local/share/opencode/opencode.db (SQLite): session rows with title/directory/parent/aggregate tokens+cost, message rows with role/tokens/cost/modelID/providerID, part rows with text/tool payloads.

src/lib/usage-opencode.mjs maps those into the index's per-session record shape (read-only via node:sqlite; malformed rows skipped, never fatal; an absent/corrupt db reads as no source, never a throw).

Two attribution rules, both grounded in the store itself:

  • Cost is observed, never recomputed. Assistant rows carry opencode's own metered cost — used as costObserved on usage rows and preferred over the pricing table in aggregate and the transcript header. Rows where no message carried a cost stay null so the pricing table applies — never a fabricated $0 for kimi/openrouter/local rates ak must not invent.
  • Provider is observed, never the host. The assistant row's providerID is the inference provider (provenance 'observed'). byProvider gains opencode / openrouter / local buckets; a bare host id is never presented as billing identity (ADR-0018's invariant, scorecard side).

Integration: per-session incremental cache keys (latest message time + count — a warm refresh re-parses only sessions that gained messages); carry-forward keyed on row existence; roots.opencode test seam with the same hermeticity rule as the codex ledger (overridden roots never read the real store); readSession returns the meta+turns payload through a factored sessionPayload shared with the JSONL parsers; SCHEMA_VERSION 7. Subagent sessions (parent_id) keep their own metered usage — opencode child sessions are not parent replays, so the codex double-count rule doesn't apply. Dashboard session rows badge opencode correctly with no client change (sx.host was already preferred; provider shows observed or unknown).

Verification

  • 14 new tests: 8 module (usage-opencode.test.mjs) + 6 scan/aggregate/readSession integration (usage-index-opencode.test.mjs), all against fixture dbs built with the production engine; doc citations remapped to the moved lines.
  • pnpm run check exit 0 (1061 kit + cjs suites), pnpm run test:surface exit 0 (25).
  • Live against the real 37-session store: 35 opencode sessions indexed alongside 1547 claude + 745 codex; byHost.opencode = 35 sessions / observed cost; provider buckets opencode, openrouter, local all observed; per-session kimi metering from opencode's own rows; 3 uncosted sessions correctly fell back to the pricing table.

Scope notes

Refs #59. Independently stackable with #85/#86/#87 (different files except usage-index.mjs ↔ none of them).

The scorecard read claude/codex transcripts only; opencode sessions did not
exist anywhere in usage/cost attribution — the biggest remaining
'second-class' remnant of the third host. opencode persists everything the
index needs in ~/.local/share/opencode/opencode.db (SQLite): session rows
with title/directory/parent/tokens/cost, message rows with
role/tokens/cost/modelID/providerID, part rows with text/tool payloads.

usage-opencode.mjs maps those into the index's per-session record shape
(read-only via node:sqlite; malformed rows skipped, never fatal; absent db
reads as no source). Two attribution rules, both grounded in the store:

- COST: assistant rows carry opencode's OWN metered cost — used as observed
  truth (costObserved on usage rows), preferred over the pricing table in
  aggregate and in the transcript header. Rows where NO message carried a
  cost stay null so the table applies — never a fabricated $0 for
  kimi/openrouter/local rates ak must not invent.
- PROVIDER: the assistant row's providerID is the inference provider when
  observed (provenance 'observed'), never the host. byProvider gains
  opencode/openrouter/local buckets alongside the existing ones.

Integration: per-session incremental cache keys (latest message time +
count — a warm refresh re-parses only sessions that gained messages);
carry-forward keyed on row existence; roots.opencode test seam with the
same hermeticity rule as the codex ledger (overridden roots never read the
real store); readSession returns the meta+turns payload through a factored
sessionPayload shared with the JSONL parsers; SCHEMA_VERSION 7. Subagent
sessions (parent_id) keep their own metered usage — opencode child
sessions are not parent replays, so the codex double-count rule does not
apply. The dashboard session rows badge opencode correctly with no client
change (sx.host was already preferred).

Verified: 14 new tests (module + scan/aggregate/readSession integration);
pnpm run check exit 0 (1061 kit + cjs); pnpm run test:surface exit 0 (25);
doc citations remapped. Live against the real store: 35 opencode sessions
indexed next to 1547 claude + 745 codex, byHost and byProvider buckets all
populated, observed kimi metering per session, uncosted rows falling back
to the pricing table.

Refs pacphi#59 (the observability axis this plugs into; opencode-side ingestion
is additive and leaves OpenRouter attribution to that issue).
@pacphi
pacphi merged commit dd2ce20 into pacphi:main Jul 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants