@@ -493,6 +493,46 @@ describe("model-pricing-cache", () => {
|
493 | 493 | expect(health.sources[0]?.detail).toContain("OpenRouter pricing response is malformed JSON"); |
494 | 494 | }); |
495 | 495 | |
| 496 | +it("records malformed pricing content-length headers as source failures", async () => { |
| 497 | +const config = { |
| 498 | +agents: { |
| 499 | +defaults: { |
| 500 | +model: { primary: "custom/gpt-remote" }, |
| 501 | +}, |
| 502 | +}, |
| 503 | +models: { |
| 504 | +providers: { |
| 505 | +custom: { |
| 506 | +baseUrl: "https://models.example/v1", |
| 507 | +api: "openai-completions", |
| 508 | +models: [{ id: "gpt-remote" }], |
| 509 | +}, |
| 510 | +}, |
| 511 | +}, |
| 512 | +} as unknown as OpenClawConfig; |
| 513 | +const fetchImpl = withFetchPreconnect(async (input: RequestInfo | URL) => { |
| 514 | +const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; |
| 515 | +if (url.includes("openrouter.ai")) { |
| 516 | +return new Response(JSON.stringify({ data: [] }), { |
| 517 | +status: 200, |
| 518 | +headers: { "Content-Type": "application/json", "Content-Length": "1e3" }, |
| 519 | +}); |
| 520 | +} |
| 521 | +return new Response(JSON.stringify({}), { |
| 522 | +status: 200, |
| 523 | +headers: { "Content-Type": "application/json" }, |
| 524 | +}); |
| 525 | +}); |
| 526 | + |
| 527 | +await refreshGatewayModelPricingCache({ config, fetchImpl }); |
| 528 | + |
| 529 | +const health = getGatewayModelPricingHealth(); |
| 530 | +expect(health.state).toBe("degraded"); |
| 531 | +expect(health.sources).toHaveLength(1); |
| 532 | +expect(health.sources[0]?.source).toBe("openrouter"); |
| 533 | +expect(health.sources[0]?.detail).toContain("invalid content-length header: 1e3"); |
| 534 | +}); |
| 535 | + |
496 | 536 | it("records and clears scheduled refresh rejections for health surfaces", async () => { |
497 | 537 | vi.useFakeTimers(); |
498 | 538 | try { |
|