/** * v2.6 Phase 1-UX (U.6) — pure mapper for opencode's per-step usage event. * * opencode's warm server emits `session.next.step.ended` once per completed LLM * step (so a multi-tool turn fires it several times). Its `properties` carry the * step's token + cost accounting: * * { * timestamp: number; * sessionID: string; * finish: string; * cost: number; // USD for this step * tokens: { * input: number; output: number; reasoning: number; * cache: { read: number; write: number }; * }; * snapshot?: string; * } * * (Verified against @opencode-ai/sdk@1.15.12 — `EventSessionNextStepEnded` in * `dist/v2/gen/types.gen.d.ts`, a member of the `Event` union the SSE loop * switches on.) * * We normalize to the review's target slice `{input, output, cost}` (the * provider-agnostic `AgentUsage` shape lands later). cache read/write tokens are * folded into `input` so the persisted input count reflects the real context the * model billed for; reasoning tokens are folded into `output` since that's what * the provider counts them as for generation. This keeps the persisted totals a * faithful sum of what opencode reported, without inventing extra columns yet. */ /** The `properties` shape of a `session.next.step.ended` event (subset we read). */ export interface StepEndedProps { cost: number; tokens: { input: number; output: number; reasoning: number; cache: { read: number; write: number }; }; } /** Normalized per-step usage delta persisted onto the agent_sessions row. */ export interface StepUsage { input: number; output: number; cost: number; } /** Coerce a possibly-missing/NaN number to a non-negative finite integer (tokens). */ function n(v: unknown): number { const x = typeof v === 'number' ? v : Number(v); return Number.isFinite(x) && x > 0 ? Math.round(x) : 0; } /** Coerce a possibly-missing/NaN number to a non-negative finite float (cost USD). */ function f(v: unknown): number { const x = typeof v === 'number' ? v : Number(v); return Number.isFinite(x) && x > 0 ? x : 0; } /** * Map a `session.next.step.ended` payload → the normalized `{input, output, cost}` * delta. Defensive against missing/partial token blocks (the wire is trusted but * we never want a NaN to poison the accumulated DB total). `input` folds in cache * read+write; `output` folds in reasoning. */ export function stepEndedToUsage(props: Partial | undefined): StepUsage { const t = props?.tokens; const cacheRead = n(t?.cache?.read); const cacheWrite = n(t?.cache?.write); return { input: n(t?.input) + cacheRead + cacheWrite, output: n(t?.output) + n(t?.reasoning), cost: f(props?.cost), }; }