diff --git a/extensions/openrouter/index.test.ts b/extensions/openrouter/index.test.ts index 85dd7ba8174b..6dc5fefb3413 100644 --- a/extensions/openrouter/index.test.ts +++ b/extensions/openrouter/index.test.ts @@ -791,6 +791,57 @@ describe("openrouter provider hooks", () => { } }); + it("falls back to streamed cost estimate when generation metadata response is oversized", async () => { + const provider = await registerSingleProviderPlugin(openrouterPlugin); + // Body exceeds the 16 MiB cap; readProviderJsonResponse must reject it and + // applyOpenRouterBilledCost must fall back to the streamed estimate. + const oversizedBody = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(16 * 1024 * 1024 + 1).fill(0x78)); + controller.close(); + }, + }); + const fetchMock = vi.fn(async (url: string) => { + expect(url).toBe("https://openrouter.ai/api/v1/generation?id=gen-oversized-1"); + return new Response(oversizedBody, { + headers: { "Content-Type": "application/json" }, + status: 200, + }); + }); + vi.stubGlobal("fetch", fetchMock); + const baseStreamFn = vi.fn(() => + createOpenRouterDoneStream({ responseId: "gen-oversized-1", totalCost: 0.001 }), + ); + + try { + const wrapped = provider.wrapStreamFn?.({ + provider: "openrouter", + modelId: "openrouter/auto", + streamFn: baseStreamFn, + } as never); + if (!wrapped) { + throw new Error("expected OpenRouter wrapper"); + } + const stream = await wrapped( + { + provider: "openrouter", + api: "openai-completions", + id: "openrouter/auto", + baseUrl: "https://openrouter.ai/api/v1", + compat: {}, + } as never, + { messages: [] } as never, + { apiKey: "or-test-key" } as never, + ); + const message = await stream.result(); + + expect(fetchMock).toHaveBeenCalledOnce(); + expect(message.usage.cost.total).toBe(0.001); + } finally { + vi.unstubAllGlobals(); + } + }); + it("does not fetch generation metadata for custom OpenRouter-compatible routes", async () => { const provider = await registerSingleProviderPlugin(openrouterPlugin); const fetchMock = vi.fn(); diff --git a/extensions/openrouter/stream.ts b/extensions/openrouter/stream.ts index fbfa8a999075..afc1fecf153a 100644 --- a/extensions/openrouter/stream.ts +++ b/extensions/openrouter/stream.ts @@ -9,6 +9,7 @@ import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-ent import { assertOkOrThrowHttpError, fetchWithTimeoutGuarded, + readProviderJsonResponse, } from "openclaw/plugin-sdk/provider-http"; import { OPENROUTER_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family"; import { @@ -124,7 +125,12 @@ async function fetchOpenRouterGenerationTotalCost(params: { ); try { await assertOkOrThrowHttpError(response, "OpenRouter generation metadata request failed"); - return readOpenRouterTotalCost((await response.json()) as OpenRouterGenerationResponse); + return readOpenRouterTotalCost( + await readProviderJsonResponse( + response, + "openrouter.generation-cost", + ), + ); } finally { await release(); }