Skip to content

Commit c0883a5

Browse files
authored
fix(openrouter): bound generation-cost JSON response reads (#97490)
1 parent 0d59280 commit c0883a5

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

extensions/openrouter/index.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,57 @@ describe("openrouter provider hooks", () => {
791791
}
792792
});
793793

794+
it("falls back to streamed cost estimate when generation metadata response is oversized", async () => {
795+
const provider = await registerSingleProviderPlugin(openrouterPlugin);
796+
// Body exceeds the 16 MiB cap; readProviderJsonResponse must reject it and
797+
// applyOpenRouterBilledCost must fall back to the streamed estimate.
798+
const oversizedBody = new ReadableStream<Uint8Array>({
799+
start(controller) {
800+
controller.enqueue(new Uint8Array(16 * 1024 * 1024 + 1).fill(0x78));
801+
controller.close();
802+
},
803+
});
804+
const fetchMock = vi.fn(async (url: string) => {
805+
expect(url).toBe("https://openrouter.ai/api/v1/generation?id=gen-oversized-1");
806+
return new Response(oversizedBody, {
807+
headers: { "Content-Type": "application/json" },
808+
status: 200,
809+
});
810+
});
811+
vi.stubGlobal("fetch", fetchMock);
812+
const baseStreamFn = vi.fn(() =>
813+
createOpenRouterDoneStream({ responseId: "gen-oversized-1", totalCost: 0.001 }),
814+
);
815+
816+
try {
817+
const wrapped = provider.wrapStreamFn?.({
818+
provider: "openrouter",
819+
modelId: "openrouter/auto",
820+
streamFn: baseStreamFn,
821+
} as never);
822+
if (!wrapped) {
823+
throw new Error("expected OpenRouter wrapper");
824+
}
825+
const stream = await wrapped(
826+
{
827+
provider: "openrouter",
828+
api: "openai-completions",
829+
id: "openrouter/auto",
830+
baseUrl: "https://openrouter.ai/api/v1",
831+
compat: {},
832+
} as never,
833+
{ messages: [] } as never,
834+
{ apiKey: "or-test-key" } as never,
835+
);
836+
const message = await stream.result();
837+
838+
expect(fetchMock).toHaveBeenCalledOnce();
839+
expect(message.usage.cost.total).toBe(0.001);
840+
} finally {
841+
vi.unstubAllGlobals();
842+
}
843+
});
844+
794845
it("does not fetch generation metadata for custom OpenRouter-compatible routes", async () => {
795846
const provider = await registerSingleProviderPlugin(openrouterPlugin);
796847
const fetchMock = vi.fn();

extensions/openrouter/stream.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-ent
99
import {
1010
assertOkOrThrowHttpError,
1111
fetchWithTimeoutGuarded,
12+
readProviderJsonResponse,
1213
} from "openclaw/plugin-sdk/provider-http";
1314
import { OPENROUTER_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family";
1415
import {
@@ -124,7 +125,12 @@ async function fetchOpenRouterGenerationTotalCost(params: {
124125
);
125126
try {
126127
await assertOkOrThrowHttpError(response, "OpenRouter generation metadata request failed");
127-
return readOpenRouterTotalCost((await response.json()) as OpenRouterGenerationResponse);
128+
return readOpenRouterTotalCost(
129+
await readProviderJsonResponse<OpenRouterGenerationResponse>(
130+
response,
131+
"openrouter.generation-cost",
132+
),
133+
);
128134
} finally {
129135
await release();
130136
}

0 commit comments

Comments
 (0)