feat(usage): opencode transcript source for the usage scorecard - #89
Merged
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):sessionrows with title/directory/parent/aggregate tokens+cost,messagerows with role/tokens/cost/modelID/providerID,partrows with text/tool payloads.src/lib/usage-opencode.mjsmaps 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:
costObservedon usage rows and preferred over the pricing table in aggregate and the transcript header. Rows where no message carried a cost staynullso the pricing table applies — never a fabricated $0 for kimi/openrouter/local rates ak must not invent.providerIDis the inference provider (provenance'observed').byProvidergainsopencode/openrouter/localbuckets; 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.opencodetest seam with the same hermeticity rule as the codex ledger (overridden roots never read the real store);readSessionreturns the meta+turns payload through a factoredsessionPayloadshared with the JSONL parsers;SCHEMA_VERSION7. 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.hostwas already preferred; provider shows observed orunknown).Verification
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 checkexit 0 (1061 kit + cjs suites),pnpm run test:surfaceexit 0 (25).byHost.opencode= 35 sessions / observed cost; provider bucketsopencode,openrouter,localall observed; per-session kimi metering from opencode's own rows; 3 uncosted sessions correctly fell back to the pricing table.Scope notes
/api/session/:id) works through the same parser (meta + turns). The Live tab's streaming adapters are untouched — that's a separate surface.provider: openrouterobserved from the assistant rows, but no OpenRouter-side transcript parsing is claimed here.Refs #59. Independently stackable with #85/#86/#87 (different files except
usage-index.mjs↔ none of them).