Files
orca/src/main/opencode-usage/opencode-usage-row-parsing.ts
T
Neilandgriffinmartin f1463683ee fix(usage): include OpenCode cached input in totals
Normalize OpenCode cache reads and writes consistently across usage views, persistence and telemetry.

Continues #20187.

Co-authored-by: griffinmartin <griffinmartin@users.noreply.github.com>
2026-10-02 20:41:18 -07:00

115 lines
3.7 KiB
TypeScript

import { ensureNumber, extractString } from '../usage/usage-record-coercion'
import type { OpenCodeUsageRow } from './opencode-usage-row-queries'
import type { OpenCodeUsageParsedEvent } from './types'
function parseJsonObject(value: unknown): Record<string, unknown> | null {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return value as Record<string, unknown>
}
if (typeof value !== 'string') {
return null
}
try {
const parsed = JSON.parse(value) as unknown
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: null
} catch {
return null
}
}
function extractModelLabel(data: Record<string, unknown>, sessionModel: unknown): string | null {
const directModel = extractString(data.modelID) ?? extractString(data.modelId)
const directProvider = extractString(data.providerID) ?? extractString(data.providerId)
if (directModel) {
return directProvider ? `${directProvider}/${directModel}` : directModel
}
const modelObject = parseJsonObject(data.model) ?? parseJsonObject(sessionModel)
if (!modelObject) {
return null
}
const modelID = extractString(modelObject.modelID) ?? extractString(modelObject.id)
const providerID = extractString(modelObject.providerID)
if (!modelID) {
return null
}
return providerID ? `${providerID}/${modelID}` : modelID
}
function extractCwd(data: Record<string, unknown>, row: OpenCodeUsageRow): string | null {
const pathData = parseJsonObject(data.path)
return (
extractString(pathData?.cwd) ??
extractString(row.directory) ??
extractString(row.worktree) ??
null
)
}
function normalizeMillis(value: unknown): number | null {
const numeric = ensureNumber(value)
if (numeric <= 0) {
return null
}
return numeric < 10_000_000_000 ? numeric * 1000 : numeric
}
function extractTimestamp(data: Record<string, unknown>, row: OpenCodeUsageRow): string | null {
const timeData = parseJsonObject(data.time)
const millis =
normalizeMillis(timeData?.completed) ??
normalizeMillis(timeData?.created) ??
normalizeMillis(row.time_updated) ??
normalizeMillis(row.time_created)
return millis ? new Date(millis).toISOString() : null
}
export function parseOpenCodeUsageRow(row: OpenCodeUsageRow): OpenCodeUsageParsedEvent | null {
const data = parseJsonObject(row.data)
if (!data) {
return null
}
const tokens = parseJsonObject(data.tokens)
if (!tokens) {
return null
}
const cache = parseJsonObject(tokens.cache)
const uncachedInputTokens = ensureNumber(tokens.input)
const outputTokens = ensureNumber(tokens.output)
const reasoningOutputTokens = ensureNumber(tokens.reasoning)
// OpenCode reports uncached input separately; Orca includes both cache buckets in input.
const cacheWriteInputTokens = ensureNumber(cache?.write)
const cachedInputTokens = ensureNumber(cache?.read) + cacheWriteInputTokens
const inputTokens = uncachedInputTokens + cachedInputTokens
const totalTokens = Math.max(
ensureNumber(tokens.total),
inputTokens + outputTokens + reasoningOutputTokens
)
if (inputTokens + outputTokens + reasoningOutputTokens + cachedInputTokens + totalTokens <= 0) {
return null
}
const timestamp = extractTimestamp(data, row)
if (!timestamp) {
return null
}
return {
sessionId: row.session_id,
timestamp,
cwd: extractCwd(data, row),
model: extractModelLabel(data, row.session_model),
estimatedCostUsd: ensureNumber(data.cost) > 0 ? ensureNumber(data.cost) : null,
inputTokens,
cachedInputTokens,
cacheWriteInputTokens,
outputTokens,
reasoningOutputTokens,
totalTokens
}
}