import type { TimelineItem } from "./streaming-v2";
export interface Metrics {
context: { consumed: number; budget: number } | null;
cached: { cached: number; prompt: number; ratio: number } | null;
tokPerSec: number | null;
reported: boolean;
route: string | null;
}
export function readMetrics(items: TimelineItem[]): Metrics {
if (!Array.isArray(items) || items.length === 0) {
return {
context: null,
cached: null,
tokPerSec: null,
reported: true,
route: null,
};
}
let newestReceipt: { consumed: number; budget: number } | null = null;
const usageRecords: Array<{
tokensReported: boolean;
routeModel: string | null;
promptTokens?: number;
cachedPromptTokens?: number;
completionTokens?: number;
generationMs?: number;
}> = [];
for (const item of items) {
if (item.lane !== "lifecycle") continue;
const entry = item.data as any;
if (!entry || typeof entry !== "object") continue;
const kind = entry.kind || entry.type;
const d = (entry.data && typeof entry.data === "object" ? entry.data : entry) as Record<string, any>;
const innerKind = d.kind || d.type;
// 1. Context receipt: check data.consumed_tokens, data.context_receipt.consumed_tokens, data.first_change.consumed_tokens
if (
kind === "context.receipt" ||
innerKind === "context_receipt" ||
innerKind === "context.receipt" ||
d.context_receipt !== undefined
) {
const cr = d.context_receipt;
const fc = d.first_change;
let consumed: number | undefined;
if (typeof d.consumed_tokens === "number") consumed = d.consumed_tokens;
else if (typeof cr?.consumed_tokens === "number") consumed = cr.consumed_tokens;
else if (typeof fc?.consumed_tokens === "number") consumed = fc.consumed_tokens;
let budget: number | undefined;
if (typeof d.total_budget_tokens === "number") budget = d.total_budget_tokens;
else if (typeof cr?.total_budget_tokens === "number") budget = cr.total_budget_tokens;
else if (typeof fc?.total_budget_tokens === "number") budget = fc.total_budget_tokens;
else if (typeof d.budget_tokens === "number") budget = d.budget_tokens;
else if (typeof cr?.budget_tokens === "number") budget = cr.budget_tokens;
if (
typeof consumed === "number" &&
typeof budget === "number" &&
!Number.isNaN(consumed) &&
!Number.isNaN(budget)
) {
newestReceipt = { consumed, budget };
}
}
// 2. Model usage: request_usage or model.usage payload
if (
kind === "model.usage" ||
innerKind === "request_usage" ||
innerKind === "model.usage" ||
d.request_sha256 !== undefined ||
d.usage !== undefined
) {
const usage = (d.usage && typeof d.usage === "object" ? d.usage : d) as Record<string, any>;
let tokensReported = true;
if (
usage.tokens_reported === false ||
d.tokens_reported === false ||
d.usage_reported === false
) {
tokensReported = false;
}
let routeModel: string | null = null;
const routeObj = d.route ?? usage.route;
if (routeObj && typeof routeObj === "object" && typeof routeObj.model === "string" && routeObj.model.trim()) {
routeModel = routeObj.model.trim();
} else if (typeof routeObj === "string" && routeObj.trim()) {
routeModel = routeObj.trim();
}
const promptTokens = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : undefined;
const cachedPromptTokens =
typeof usage.cached_prompt_tokens === "number"
? usage.cached_prompt_tokens
: typeof usage.prompt_cache_hit_tokens === "number"
? usage.prompt_cache_hit_tokens
: undefined;
const completionTokens = typeof usage.completion_tokens === "number" ? usage.completion_tokens : undefined;
const generationMs = typeof usage.generation_ms === "number" ? usage.generation_ms : undefined;
usageRecords.push({
tokensReported,
routeModel,
promptTokens,
cachedPromptTokens,
completionTokens,
generationMs,
});
}
}
// Session-scoped: sum the usage records of the session for the cache figures
let sumPrompt = 0;
let sumCached = 0;
let anyPromptReported = false;
let anyCacheReported = false;
for (const rec of usageRecords) {
if (!rec.tokensReported) continue;
if (typeof rec.promptTokens === "number") {
sumPrompt += rec.promptTokens;
anyPromptReported = true;
}
if (typeof rec.cachedPromptTokens === "number") {
sumCached += rec.cachedPromptTokens;
anyCacheReported = true;
}
}
const cached =
anyCacheReported && anyPromptReported && sumPrompt > 0
? { cached: sumCached, prompt: sumPrompt, ratio: sumCached / sumPrompt }
: null;
// Newest usage for tok/s and route; reported: false when newest usage tokens_reported === false
let tokPerSec: number | null = null;
let reported = true;
let route: string | null = null;
if (usageRecords.length > 0) {
const newest = usageRecords[usageRecords.length - 1];
reported = newest.tokensReported;
route = newest.routeModel;
if (
reported &&
typeof newest.completionTokens === "number" &&
typeof newest.generationMs === "number" &&
newest.generationMs > 0
) {
tokPerSec = (newest.completionTokens / newest.generationMs) * 1000;
}
}
return {
context: newestReceipt,
cached,
tokPerSec,
reported,
route,
};
}