From bad38150cb2ff63bbeda7c08ecbba61f624179e6 Mon Sep 17 00:00:00 2001 From: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:11:55 +0200 Subject: [PATCH] fix(gateway): fall back to lastCallUsage on /v1/chat/completions --- src/gateway/openai-http.ts | 26 +++++++-- src/gateway/openai-http.usage.test.ts | 78 +++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 src/gateway/openai-http.usage.test.ts diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index bc9a9e7bbff..0f4b502de5f 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -1,7 +1,12 @@ import { randomUUID } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; import type { ImageContent } from "../agents/command/types.js"; -import { normalizeUsage, toOpenAiChatCompletionsUsage } from "../agents/usage.js"; +import { + hasNonzeroUsage, + normalizeUsage, + toOpenAiChatCompletionsUsage, + type NormalizedUsage, +} from "../agents/usage.js"; import { createDefaultDeps } from "../cli/deps.js"; import { agentCommandFromIngress } from "../commands/agent.js"; import type { GatewayHttpChatCompletionsConfig } from "../config/types.gateway.js"; @@ -369,6 +374,7 @@ async function resolveImagesForRequest( export const __testOnlyOpenAiHttp = { resolveImagesForRequest, resolveOpenAiChatCompletionsLimits, + resolveChatCompletionUsage, }; function buildAgentPrompt( @@ -466,16 +472,26 @@ type AgentUsageMeta = { total?: number; }; -function resolveRawAgentUsage(result: unknown): AgentUsageMeta | undefined { - return ( +function resolveAgentRunUsage(result: unknown): NormalizedUsage | undefined { + const agentMeta = ( result as { meta?: { agentMeta?: { usage?: AgentUsageMeta; + lastCallUsage?: AgentUsageMeta; }; }; } | null - )?.meta?.agentMeta?.usage; + )?.meta?.agentMeta; + const primary = normalizeUsage(agentMeta?.usage); + if (hasNonzeroUsage(primary)) { + return primary; + } + const fallback = normalizeUsage(agentMeta?.lastCallUsage); + if (hasNonzeroUsage(fallback)) { + return fallback; + } + return primary ?? fallback; } function resolveChatCompletionUsage(result: unknown): { @@ -483,7 +499,7 @@ function resolveChatCompletionUsage(result: unknown): { completion_tokens: number; total_tokens: number; } { - return toOpenAiChatCompletionsUsage(normalizeUsage(resolveRawAgentUsage(result))); + return toOpenAiChatCompletionsUsage(resolveAgentRunUsage(result)); } function resolveIncludeUsageForStreaming(payload: OpenAiChatCompletionRequest): boolean { diff --git a/src/gateway/openai-http.usage.test.ts b/src/gateway/openai-http.usage.test.ts new file mode 100644 index 00000000000..03524a4c58d --- /dev/null +++ b/src/gateway/openai-http.usage.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { __testOnlyOpenAiHttp } from "./openai-http.js"; + +const { resolveChatCompletionUsage } = __testOnlyOpenAiHttp; + +describe("resolveChatCompletionUsage", () => { + it("maps agentMeta.usage to OpenAI prompt/completion/total fields", () => { + const result = { + meta: { + agentMeta: { + usage: { input: 120, output: 42, cacheRead: 10, total: 172 }, + }, + }, + }; + + expect(resolveChatCompletionUsage(result)).toEqual({ + prompt_tokens: 130, + completion_tokens: 42, + total_tokens: 172, + }); + }); + + it("falls back to agentMeta.lastCallUsage when agentMeta.usage is missing", () => { + const result = { + meta: { + agentMeta: { + lastCallUsage: { input: 80, output: 20, total: 100 }, + }, + }, + }; + + expect(resolveChatCompletionUsage(result)).toEqual({ + prompt_tokens: 80, + completion_tokens: 20, + total_tokens: 100, + }); + }); + + it("falls back to agentMeta.lastCallUsage when agentMeta.usage is all zero", () => { + const result = { + meta: { + agentMeta: { + usage: { input: 0, output: 0, total: 0 }, + lastCallUsage: { input: 55, output: 7, total: 62 }, + }, + }, + }; + + expect(resolveChatCompletionUsage(result)).toEqual({ + prompt_tokens: 55, + completion_tokens: 7, + total_tokens: 62, + }); + }); + + it("returns zeros when both agentMeta.usage and lastCallUsage are absent", () => { + const result = { meta: { agentMeta: {} } }; + + expect(resolveChatCompletionUsage(result)).toEqual({ + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }); + }); + + it("returns zeros when the result has no meta at all", () => { + expect(resolveChatCompletionUsage({})).toEqual({ + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }); + expect(resolveChatCompletionUsage(null)).toEqual({ + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }); + }); +});