Files
OmniRoute/tests/unit/usage-extractor.test.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

533 lines
14 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const { extractUsageFromResponse } = await import("../../open-sse/handlers/usageExtractor.ts");
const { extractUsage, normalizeUsage } = await import("../../open-sse/utils/usageTracking.ts");
test("normalizeUsage keeps only finite numeric fields for stream cost calculation", () => {
assert.deepEqual(
normalizeUsage({
input_tokens: "12",
output_tokens: 3,
total_tokens: Number.POSITIVE_INFINITY,
input_tokens_details: { cached_tokens: 2 },
}),
{ input_tokens: 12, output_tokens: 3 }
);
});
test("extractUsageFromResponse reads OpenAI chat completion usage", () => {
const usage = extractUsageFromResponse(
{
usage: {
prompt_tokens: 12,
completion_tokens: 8,
prompt_tokens_details: { cached_tokens: 3 },
completion_tokens_details: { reasoning_tokens: 2 },
},
},
"openai"
);
assert.deepEqual(usage, {
prompt_tokens: 12,
completion_tokens: 8,
cached_tokens: 3,
reasoning_tokens: 2,
});
});
test("extractUsageFromResponse reads OpenAI usage when cache/reasoning live under input/output token details", () => {
const usage = extractUsageFromResponse(
{
usage: {
prompt_tokens: 12,
completion_tokens: 8,
input_tokens_details: { cached_tokens: 4 },
output_tokens_details: { reasoning_tokens: 1 },
},
},
"codex"
);
assert.deepEqual(usage, {
prompt_tokens: 12,
completion_tokens: 8,
cached_tokens: 4,
reasoning_tokens: 1,
});
});
test("extractUsageFromResponse defaults missing OpenAI token fields to zero", () => {
const usage = extractUsageFromResponse(
{
usage: {
prompt_tokens: 0,
},
},
"openai"
);
assert.equal(usage.prompt_tokens, 0);
assert.equal(usage.completion_tokens, 0);
assert.equal(usage.cached_tokens, undefined);
assert.equal(usage.reasoning_tokens, undefined);
});
test("extractUsageFromResponse reads Responses API usage from the top-level usage field", () => {
const usage = extractUsageFromResponse(
{
object: "response",
usage: {
input_tokens: 20,
output_tokens: 9,
cache_read_input_tokens: 4,
cache_creation_input_tokens: 5,
reasoning_tokens: 3,
},
},
"github"
);
assert.deepEqual(usage, {
prompt_tokens: 20,
completion_tokens: 9,
cache_read_input_tokens: 4,
cached_tokens: 4,
cache_creation_input_tokens: 5,
reasoning_tokens: 3,
});
});
test("extractUsageFromResponse reads Responses API usage from nested response.usage", () => {
const usage = extractUsageFromResponse(
{
response: {
usage: {
input_tokens: 14,
output_tokens: 6,
input_tokens_details: { cached_tokens: 2 },
output_tokens_details: { reasoning_tokens: 1 },
},
},
},
"codex"
);
assert.deepEqual(usage, {
prompt_tokens: 14,
completion_tokens: 6,
cache_read_input_tokens: undefined,
cached_tokens: 2,
cache_creation_input_tokens: undefined,
reasoning_tokens: 1,
});
});
test("extractUsageFromResponse reads Responses API usage with prompt_tokens_details (OpenAI hybrid format)", () => {
const usage = extractUsageFromResponse(
{
usage: {
input_tokens: 30,
output_tokens: 12,
prompt_tokens_details: { cached_tokens: 10 },
completion_tokens_details: { reasoning_tokens: 5 },
},
},
"codex"
);
assert.deepEqual(usage, {
prompt_tokens: 30,
completion_tokens: 12,
cache_read_input_tokens: undefined,
cached_tokens: 10,
cache_creation_input_tokens: undefined,
reasoning_tokens: 5,
});
});
test("extractUsageFromResponse reads Responses API cache_read_input_tokens as cached_tokens fallback", () => {
const usage = extractUsageFromResponse(
{
usage: {
input_tokens: 50,
output_tokens: 20,
cache_read_input_tokens: 15,
cache_creation_input_tokens: 8,
reasoning_tokens: 3,
},
},
"github"
);
assert.deepEqual(usage, {
prompt_tokens: 50,
completion_tokens: 20,
cache_read_input_tokens: 15,
cached_tokens: 15,
cache_creation_input_tokens: 8,
reasoning_tokens: 3,
});
});
test("extractUsageFromResponse totals Claude prompt tokens with cache read and cache creation", () => {
const usage = extractUsageFromResponse(
{
usage: {
input_tokens: 10,
output_tokens: 7,
cache_read_input_tokens: 4,
cache_creation_input_tokens: 6,
},
},
"claude"
);
assert.deepEqual(usage, {
prompt_tokens: 20,
completion_tokens: 7,
cache_read_input_tokens: 4,
cache_creation_input_tokens: 6,
});
});
test("extractUsageFromResponse surfaces Claude thinking tokens without inflating completion", () => {
const usage = extractUsageFromResponse(
{
usage: {
input_tokens: 22,
output_tokens: 267,
output_tokens_details: { thinking_tokens: 85 },
},
},
"claude"
);
assert.deepEqual(usage, {
prompt_tokens: 22,
completion_tokens: 267,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
reasoning_tokens: 85,
});
});
test("extractUsageFromResponse reads Gemini usageMetadata and thinking tokens", () => {
const usage = extractUsageFromResponse(
{
usageMetadata: {
promptTokenCount: 11,
candidatesTokenCount: 5,
thoughtsTokenCount: 2,
},
},
"gemini"
);
assert.deepEqual(usage, {
prompt_tokens: 11,
completion_tokens: 7,
cached_tokens: 0,
reasoning_tokens: 2,
});
});
test("extractUsageFromResponse reads Gemini usageMetadata from the antigravity response envelope", () => {
// Antigravity / gemini-cli wrap non-streaming payloads in { response: {...} }
// (port of decolua/9router#59d858b — previously logged zero usage).
const usage = extractUsageFromResponse(
{
response: {
usageMetadata: {
promptTokenCount: 42,
candidatesTokenCount: 13,
thoughtsTokenCount: 4,
cachedContentTokenCount: 7,
},
},
},
"antigravity"
);
assert.deepEqual(usage, {
prompt_tokens: 42,
completion_tokens: 17,
cached_tokens: 7,
reasoning_tokens: 4,
});
});
test("extractUsageFromResponse prefers top-level usageMetadata over the envelope", () => {
const usage = extractUsageFromResponse(
{
usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 2 },
response: { usageMetadata: { promptTokenCount: 99, candidatesTokenCount: 99 } },
},
"gemini"
);
assert.deepEqual(usage, {
prompt_tokens: 1,
completion_tokens: 2,
cached_tokens: 0,
reasoning_tokens: 0,
});
});
test("extractUsageFromResponse surfaces Gemini cachedContentTokenCount as cached_tokens", () => {
// Review follow-up on #10430: match the OpenAI/Claude/Responses branches and
// the streaming path (usageTracking.ts) by surfacing Gemini cache-hit tokens.
const usage = extractUsageFromResponse(
{
usageMetadata: {
promptTokenCount: 30,
candidatesTokenCount: 10,
thoughtsTokenCount: 3,
cachedContentTokenCount: 12,
},
},
"gemini"
);
assert.deepEqual(usage, {
prompt_tokens: 30,
completion_tokens: 13,
cached_tokens: 12,
reasoning_tokens: 3,
});
});
test("extractUsageFromResponse returns null when usage is missing", () => {
const usage = extractUsageFromResponse(
{
id: "chatcmpl_no_usage",
choices: [{ message: { role: "assistant", content: "ok" } }],
},
"openai"
);
assert.equal(usage, null);
});
test("extractUsageFromResponse returns null for null and undefined response bodies", () => {
assert.equal(extractUsageFromResponse(null, "openai"), null);
assert.equal(extractUsageFromResponse(undefined, "openai"), null);
});
test("extractUsageFromResponse returns null for non-object response bodies", () => {
assert.equal(extractUsageFromResponse("not-an-object", "openai"), null);
assert.equal(extractUsageFromResponse(42, "openai"), null);
});
// ── extractUsage (streaming) tests ──
test("extractUsage reads response.completed with prompt_tokens_details.cached_tokens", () => {
const usage = extractUsage({
type: "response.completed",
response: {
usage: {
input_tokens: 100,
output_tokens: 50,
prompt_tokens_details: { cached_tokens: 30 },
completion_tokens_details: { reasoning_tokens: 10 },
},
},
});
assert.equal(usage.prompt_tokens, 100);
assert.equal(usage.completion_tokens, 50);
assert.equal(usage.cached_tokens, 30);
assert.equal(usage.reasoning_tokens, 10);
});
test("extractUsage surfaces Claude message_delta thinking tokens", () => {
const usage = extractUsage({
type: "message_delta",
usage: {
input_tokens: 22,
output_tokens: 267,
output_tokens_details: { thinking_tokens: 85 },
},
});
assert.equal(usage.reasoning_tokens, 85);
assert.equal(usage.completion_tokens, 267);
});
test("extractUsage reads response.done with input_tokens_details and output_tokens_details", () => {
const usage = extractUsage({
type: "response.done",
response: {
usage: {
input_tokens: 80,
output_tokens: 40,
input_tokens_details: { cached_tokens: 20 },
output_tokens_details: { reasoning_tokens: 8 },
},
},
});
assert.equal(usage.cached_tokens, 20);
assert.equal(usage.reasoning_tokens, 8);
});
test("extractUsage reads response.completed with cache_read_input_tokens", () => {
const usage = extractUsage({
type: "response.completed",
response: {
usage: {
input_tokens: 60,
output_tokens: 25,
cache_read_input_tokens: 15,
cache_creation_input_tokens: 5,
reasoning_tokens: 3,
},
},
});
assert.equal(usage.cached_tokens, 15);
assert.equal(usage.cache_creation_input_tokens, 5);
assert.equal(usage.reasoning_tokens, 3);
});
test("extractUsage reads OpenAI streaming chunk with prompt_tokens_details", () => {
const usage = extractUsage({
choices: [{ delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: 200,
completion_tokens: 100,
prompt_tokens_details: { cached_tokens: 50 },
completion_tokens_details: { reasoning_tokens: 20 },
},
});
assert.equal(usage.cached_tokens, 50);
assert.equal(usage.reasoning_tokens, 20);
});
// ── Flat field extraction tests (Xiaomi MiMo-style providers) ──
test("extractUsageFromResponse reads flat cached_tokens and reasoning_tokens from OpenAI-compatible usage", () => {
const usage = extractUsageFromResponse(
{
usage: {
prompt_tokens: 258,
completion_tokens: 50,
total_tokens: 308,
cached_tokens: 192,
reasoning_tokens: 49,
},
},
"xiaomi-mimo"
);
assert.deepEqual(usage, {
prompt_tokens: 258,
completion_tokens: 50,
cached_tokens: 192,
reasoning_tokens: 49,
});
});
test("extractUsage reads flat cached_tokens and reasoning_tokens from streaming chunk", () => {
const usage = extractUsage({
choices: [{ delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: 258,
completion_tokens: 50,
total_tokens: 308,
cached_tokens: 192,
reasoning_tokens: 49,
},
});
assert.equal(usage.cached_tokens, 192);
assert.equal(usage.reasoning_tokens, 49);
});
// ── Ollama raw NDJSON streaming usage ──
// Ollama sends a final NDJSON line { done: true, prompt_eval_count, eval_count }
// (raw from the provider, before any OpenAI translation). Without a dedicated
// branch, extractUsage returns null and Ollama streaming usage is dropped.
test("extractUsage reads Ollama raw NDJSON final chunk (done + prompt_eval_count/eval_count)", () => {
const usage = extractUsage({
model: "llama3.1",
done: true,
prompt_eval_count: 26,
eval_count: 298,
});
assert.ok(usage, "expected usage to be extracted from the Ollama final chunk");
assert.equal(usage.prompt_tokens, 26);
assert.equal(usage.completion_tokens, 298);
assert.equal(usage.total_tokens, 324);
});
test("extractUsage defaults missing Ollama eval counts to zero", () => {
const usage = extractUsage({
model: "llama3.1",
done: true,
prompt_eval_count: 12,
});
assert.ok(usage, "expected usage to be extracted even with only prompt_eval_count");
assert.equal(usage.prompt_tokens, 12);
assert.equal(usage.completion_tokens, 0);
assert.equal(usage.total_tokens, 12);
});
test("extractUsage ignores non-final Ollama NDJSON chunks (done=false)", () => {
const usage = extractUsage({
model: "llama3.1",
done: false,
response: "partial",
});
assert.equal(usage, null);
});
// ── Antigravity (Gemini) streaming usageMetadata tests ──
test("extractUsage reads top-level Gemini usageMetadata from a streaming chunk", () => {
const usage = extractUsage({
usageMetadata: {
promptTokenCount: 120,
candidatesTokenCount: 60,
totalTokenCount: 180,
cachedContentTokenCount: 30,
thoughtsTokenCount: 12,
},
});
assert.equal(usage.prompt_tokens, 120);
assert.equal(usage.completion_tokens, 72);
assert.equal(usage.total_tokens, 180);
assert.equal(usage.cached_tokens, 30);
assert.equal(usage.reasoning_tokens, 12);
});
test("extractUsage reads Antigravity usageMetadata wrapped inside a response envelope", () => {
// Antigravity (AG MITM) shapes usage as { response: { usageMetadata: {...} } }.
// Without the response.usageMetadata fallback, token usage is silently dropped.
const usage = extractUsage({
response: {
usageMetadata: {
promptTokenCount: 200,
candidatesTokenCount: 75,
totalTokenCount: 275,
cachedContentTokenCount: 40,
thoughtsTokenCount: 18,
},
},
});
assert.notEqual(usage, null);
assert.equal(usage.prompt_tokens, 200);
assert.equal(usage.completion_tokens, 93);
assert.equal(usage.total_tokens, 275);
assert.equal(usage.cached_tokens, 40);
assert.equal(usage.reasoning_tokens, 18);
});