mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import type { ThreadTokenUsage, TokenUsageBreakdown } from "../../domain/runtime/metrics";
|
|
|
|
const ROLLOUT_TOKEN_USAGE_READ_TIMEOUT_MS = 2_000;
|
|
const ROLLOUT_TOKEN_USAGE_MAX_BASE64_BYTES = 12 * 1024 * 1024;
|
|
|
|
export type RolloutReadFileBase64 = (path: string, options: { timeoutMs: number }) => Promise<string | null>;
|
|
|
|
export async function recoverRolloutTokenUsage(
|
|
path: string | null,
|
|
readFileBase64: RolloutReadFileBase64,
|
|
): Promise<ThreadTokenUsage | null> {
|
|
if (!path || !isAbsolutePath(path)) return null;
|
|
|
|
let dataBase64: string | null;
|
|
try {
|
|
dataBase64 = await readFileBase64(path, { timeoutMs: ROLLOUT_TOKEN_USAGE_READ_TIMEOUT_MS });
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (dataBase64 === null) return null;
|
|
if (dataBase64.length > ROLLOUT_TOKEN_USAGE_MAX_BASE64_BYTES) return null;
|
|
|
|
const text = decodeBase64Text(dataBase64);
|
|
return text ? parseRolloutTokenUsageJsonl(text) : null;
|
|
}
|
|
|
|
function parseRolloutTokenUsageJsonl(text: string): ThreadTokenUsage | null {
|
|
const lines = text.split(/\r?\n/);
|
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
const line = lines[index]?.trim();
|
|
if (!line) continue;
|
|
|
|
let value: unknown;
|
|
try {
|
|
value = JSON.parse(line);
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
const usage = tokenUsageFromRolloutRecord(value);
|
|
if (usage) return usage;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function tokenUsageFromRolloutRecord(value: unknown): ThreadTokenUsage | null {
|
|
const record = objectRecord(value);
|
|
if (record?.["type"] !== "event_msg") return null;
|
|
const payload = objectRecord(record["payload"]);
|
|
if (payload?.["type"] !== "token_count") return null;
|
|
const info = objectRecord(payload["info"]);
|
|
if (!info) return null;
|
|
|
|
const last = tokenUsageBreakdownFromRecord(info["last_token_usage"]);
|
|
const total = tokenUsageBreakdownFromRecord(info["total_token_usage"]);
|
|
const modelContextWindow = nullableNonNegativeNumber(info["model_context_window"]);
|
|
if (!last || !total || modelContextWindow === undefined) return null;
|
|
|
|
return { last, total, modelContextWindow };
|
|
}
|
|
|
|
function tokenUsageBreakdownFromRecord(value: unknown): TokenUsageBreakdown | null {
|
|
const record = objectRecord(value);
|
|
if (!record) return null;
|
|
const totalTokens = nonNegativeNumber(record["total_tokens"]);
|
|
const inputTokens = nonNegativeNumber(record["input_tokens"]);
|
|
const cachedInputTokens = nonNegativeNumber(record["cached_input_tokens"]);
|
|
const outputTokens = nonNegativeNumber(record["output_tokens"]);
|
|
const reasoningOutputTokens = nonNegativeNumber(record["reasoning_output_tokens"]);
|
|
if (
|
|
totalTokens === null ||
|
|
inputTokens === null ||
|
|
cachedInputTokens === null ||
|
|
outputTokens === null ||
|
|
reasoningOutputTokens === null
|
|
) {
|
|
return null;
|
|
}
|
|
return { totalTokens, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
|
|
}
|
|
|
|
function decodeBase64Text(dataBase64: string): string | null {
|
|
try {
|
|
const binary = atob(dataBase64);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let index = 0; index < binary.length; index += 1) {
|
|
bytes[index] = binary.charCodeAt(index);
|
|
}
|
|
return new TextDecoder().decode(bytes);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isAbsolutePath(path: string): boolean {
|
|
return path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path);
|
|
}
|
|
|
|
function objectRecord(value: unknown): Record<string, unknown> | null {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
|
}
|
|
|
|
function nonNegativeNumber(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
}
|
|
|
|
function nullableNonNegativeNumber(value: unknown): number | null | undefined {
|
|
if (value === null) return null;
|
|
return nonNegativeNumber(value) ?? undefined;
|
|
}
|