fix(providers): preserve streaming error bodies · openclaw/openclaw@6f4272b
steipete
·
2026-05-08
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -154,6 +154,7 @@ Docs: https://docs.openclaw.ai
|
154 | 154 | |
155 | 155 | ### Fixes |
156 | 156 | |
| 157 | +- Providers: preserve non-OK `text/event-stream` response bodies so provider HTTP errors keep their JSON detail instead of collapsing to generic streaming failures. Fixes #78180. |
157 | 158 | - Chat commands: make `/model default` reset the session model override instead of treating it as a literal model name. Fixes #78182. |
158 | 159 | - Cron: make rejected `payload.model` errors show the configured `agents.defaults.models` allowlist instead of echoing the rejected model twice. Fixes #79058. |
159 | 160 | - Agents/subagents: retry parent wake announces when the announce-summary model run fails with fallback cooldown exhaustion instead of dropping the wake on the first transient provider overload. Refs #78581. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -341,6 +341,43 @@ describe("buildGuardedModelFetch", () => {
|
341 | 341 | expect(items).toEqual([{ ok: true }]); |
342 | 342 | }); |
343 | 343 | |
| 344 | +it("preserves non-OK SSE bodies for provider HTTP error parsing", async () => { |
| 345 | +fetchWithSsrFGuardMock.mockResolvedValue({ |
| 346 | +response: new Response( |
| 347 | +JSON.stringify({ |
| 348 | +error: { |
| 349 | +message: "API key expired", |
| 350 | +}, |
| 351 | +}), |
| 352 | +{ |
| 353 | +status: 400, |
| 354 | +headers: { "content-type": "text/event-stream" }, |
| 355 | +}, |
| 356 | +), |
| 357 | +finalUrl: |
| 358 | +"https://generativelanguage.googleapis.com/v1beta/models/gemini:streamGenerateContent", |
| 359 | +release: vi.fn(async () => undefined), |
| 360 | +}); |
| 361 | + |
| 362 | +const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js"); |
| 363 | +const model = { |
| 364 | +id: "gemini-3.1-pro-preview", |
| 365 | +provider: "google", |
| 366 | +api: "openai-completions", |
| 367 | +baseUrl: "https://generativelanguage.googleapis.com/v1beta", |
| 368 | +} as unknown as Model<"openai-completions">; |
| 369 | + |
| 370 | +const response = await buildGuardedModelFetch(model)( |
| 371 | +"https://generativelanguage.googleapis.com/v1beta/models/gemini:streamGenerateContent", |
| 372 | +{ method: "POST" }, |
| 373 | +); |
| 374 | + |
| 375 | +expect(response.status).toBe(400); |
| 376 | +await expect(response.json()).resolves.toEqual({ |
| 377 | +error: { message: "API key expired" }, |
| 378 | +}); |
| 379 | +}); |
| 380 | + |
344 | 381 | it("refreshes the guarded timeout while consuming streaming response chunks", async () => { |
345 | 382 | const encoder = new TextEncoder(); |
346 | 383 | const refreshTimeout = vi.fn(); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -48,7 +48,7 @@ function findSseEventBoundary(buffer: string): { index: number; length: number }
|
48 | 48 | |
49 | 49 | function sanitizeOpenAISdkSseResponse(response: Response): Response { |
50 | 50 | const contentType = response.headers.get("content-type") ?? ""; |
51 | | -if (!response.body || !/\btext\/event-stream\b/i.test(contentType)) { |
| 51 | +if (!response.ok || !response.body || !/\btext\/event-stream\b/i.test(contentType)) { |
52 | 52 | return response; |
53 | 53 | } |
54 | 54 | |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。