Files
workinf_Blender_Wasm/web/protocol/provider-usage.ts
mes123456 6a0b980d75
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Complete M16 action select circle parity
2026-08-20 22:57:59 -04:00

94 lines
4.0 KiB
TypeScript

export type ProviderUsageStatus = "AVAILABLE" | "PARTIAL" | "MISSING" | "INCONSISTENT";
export type ProviderUsageField = "inputTokens" | "outputTokens" | "totalTokens";
export interface ProviderUsage {
status: ProviderUsageStatus;
inputTokens: number | null;
outputTokens: number | null;
totalTokens: number | null;
missing: ProviderUsageField[];
inconsistent: ProviderUsageField[];
derivedTotal: boolean;
sources: Partial<Record<ProviderUsageField, string[]>>;
}
const ALIASES: Record<ProviderUsageField, Set<string>> = {
inputTokens: new Set(["input_tokens", "inputtokens", "prompt_tokens", "prompttokens", "prompt_token_count", "prompttokencount", "input_token_count", "inputtokencount"]),
outputTokens: new Set(["output_tokens", "outputtokens", "completion_tokens", "completiontokens", "candidates_token_count", "candidatestokencount", "output_token_count", "outputtokencount"]),
totalTokens: new Set(["total_tokens", "totaltokens", "total_token_count", "totaltokencount"]),
};
function keyName(key: string): string {
return key.replace(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`).replace(/-/gu, "_").toLowerCase();
}
function tokenCount(value: unknown): number | undefined {
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
if (typeof value === "string" && /^\d+$/u.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
return undefined;
}
interface Observation { value: number; path: string; }
/** Normalize OpenAI, Responses, Anthropic, Gemini and nested gateway usage shapes. */
export function parseProviderUsage(input: unknown): ProviderUsage {
const observations: Record<ProviderUsageField, Observation[]> = { inputTokens: [], outputTokens: [], totalTokens: [] };
const seen = new Set<object>();
const visit = (value: unknown, path: string): void => {
if (typeof value !== "object" || value === null) return;
if (seen.has(value)) return;
seen.add(value);
if (Array.isArray(value)) {
value.forEach((item, index) => visit(item, `${path}[${index}]`));
return;
}
for (const [key, child] of Object.entries(value)) {
const normalized = keyName(key);
for (const field of Object.keys(ALIASES) as ProviderUsageField[]) {
if (ALIASES[field].has(normalized)) {
const count = tokenCount(child);
if (count !== undefined) observations[field].push({ value: count, path: `${path}.${key}` });
}
}
visit(child, `${path}.${key}`);
}
};
visit(input, "$");
const values = {} as Record<ProviderUsageField, number | null>;
const inconsistent: ProviderUsageField[] = [];
const sources: Partial<Record<ProviderUsageField, string[]>> = {};
for (const field of Object.keys(observations) as ProviderUsageField[]) {
const items = observations[field];
const unique = [...new Set(items.map((item) => item.value))];
values[field] = unique.length === 0 ? null : unique[0];
sources[field] = items.map((item) => item.path);
if (unique.length > 1) inconsistent.push(field);
}
let derivedTotal = false;
if (values.totalTokens === null && values.inputTokens !== null && values.outputTokens !== null && inconsistent.length === 0) {
values.totalTokens = values.inputTokens + values.outputTokens;
derivedTotal = true;
}
if (values.totalTokens !== null && values.inputTokens !== null && values.outputTokens !== null
&& values.totalTokens !== values.inputTokens + values.outputTokens && !inconsistent.includes("totalTokens")) {
inconsistent.push("totalTokens");
}
const missing = (Object.keys(values) as ProviderUsageField[]).filter((field) => values[field] === null);
const status: ProviderUsageStatus = inconsistent.length > 0
? "INCONSISTENT"
: missing.length === 3
? "MISSING"
: missing.length > 0
? "PARTIAL"
: "AVAILABLE";
return { status, ...values, missing, inconsistent, derivedTotal, sources };
}
export const normalizeProviderUsage = parseProviderUsage;
export const parseUsage = parseProviderUsage;