Skip to content
Open
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
539 changes: 538 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions hooks/session-extract.bundle.mjs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"@clack/prompts": "^1.0.1",
"@mixmark-io/domino": "^2.2.0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@opencode/plugin": "^2.0.4",
"better-sqlite3": "^12.6.2",
"picocolors": "^1.1.1",
"turndown": "^7.2.0",
Expand Down
613 changes: 565 additions & 48 deletions src/adapters/opencode/plugin.ts

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions src/session/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,75 @@ export function parseOpencodeUsage(payload: unknown): AgentUsageCounts | null {
};
}

/**
* Map an OpenCode v2 `session.step.ended` payload into the `buildAgentUsageEvent`
* input shape.
*
* The v2 bus has no `message.updated` event, so the v1 parser is unusable
* here. v2 usage lives under `data` (not `properties.info`); the model is NOT
* carried on the usage event — it is observed on the matching
* `session.step.started` and passed in as `modelId` (the caller correlates
* the two events by `assistantMessageID`); and there is an extra `reasoning`
* token bucket. Field names verified against the published
* @opencode/plugin@2.0.16 + @opencode/schema@2.0.16 types.
*
* Following the gemini-cli convention (thoughts folded into output), the v2
* `reasoning` bucket is folded into `output_tokens` — the provider bills it as
* output. The event ships a native USD cost, so it is taken verbatim as
* `native_cost_usd` and the pricing catalog is bypassed.
*
* Pure, null-safe, struct-only — NO regex. Returns null when every token bucket is
* zero/absent, so an all-zero step emits no event.
*/
export function parseOpencodeV2StepUsage(
data: unknown,
modelId: string,
): AgentUsageCounts | null {
if (!data || typeof data !== "object") return null;
const d = data as Record<string, unknown>;

const tokensRaw = d.tokens;
if (!tokensRaw || typeof tokensRaw !== "object") return null;
const tokens = tokensRaw as Record<string, unknown>;

const num = (v: unknown): number =>
typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;

const cacheRaw = tokens.cache;
const cache =
cacheRaw && typeof cacheRaw === "object"
? (cacheRaw as Record<string, unknown>)
: {};

const input_tokens = num(tokens.input);
// v2 exposes a distinct `reasoning` bucket; fold it into output (billed as output).
const output_tokens = num(tokens.output) + num(tokens.reasoning);
const cache_read_tokens = num(cache.read);
const cache_creation_tokens = num(cache.write);

if (
input_tokens <= 0 &&
output_tokens <= 0 &&
cache_creation_tokens <= 0 &&
cache_read_tokens <= 0
) {
return null;
}

const costRaw = d.cost;
const native_cost_usd =
typeof costRaw === "number" && Number.isFinite(costRaw) ? costRaw : null;

return {
model_id: typeof modelId === "string" ? modelId : "",
input_tokens,
output_tokens,
cache_creation_tokens,
cache_read_tokens,
native_cost_usd,
};
}

/**
* Build a structured `agent_usage` event from summed per-model token counts.
* Emits the colon-string `data` (human/debug + back-compat) AND the structured
Expand Down
Loading