From 1e1dc56a0942d2a2dff09e8c1b2354a4ff367a88 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 2 Jun 2026 12:48:06 +0000 Subject: [PATCH 1/2] fix(console): parse provider SSE error bodies --- .../app/src/routes/zen/util/handler.ts | 3 +- .../app/src/routes/zen/util/providerError.ts | 33 +++++++++++++++++++ .../console/app/test/providerError.test.ts | 26 +++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 packages/console/app/src/routes/zen/util/providerError.ts create mode 100644 packages/console/app/test/providerError.test.ts diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 4438688c22..dc8649c6d1 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -48,6 +48,7 @@ import { localeFromRequest } from "~/lib/language" import { createModelTpmLimiter } from "./modelTpmLimiter" import { createModelTpsLimiter } from "./modelTpsLimiter" import { accumulateUsage, HOT_WORKSPACES } from "./usageBatcher" +import { readProviderError } from "./providerError" type ZenData = Awaited> type RetryOptions = { @@ -248,7 +249,7 @@ export async function handler( // Handle non-streaming response if (!isStream || [400, 404, 429].includes(res.status)) { - const json = await res.json() + const json = res.status === 200 ? await res.json() : await readProviderError(res) await rateLimiter?.track() const usage = providerInfo.extractUsage(json) if (usage) { diff --git a/packages/console/app/src/routes/zen/util/providerError.ts b/packages/console/app/src/routes/zen/util/providerError.ts new file mode 100644 index 0000000000..2a793930f5 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/providerError.ts @@ -0,0 +1,33 @@ +export async function readProviderError(res: Response) { + const body = await res.text() + const json = parseJson(body) + if (json && typeof json === "object") return json as Record + return parseProviderErrorBody(body, res.statusText) +} + +export function parseProviderErrorBody(body: string, statusText: string) { + const text = body.trim() + const sseData = text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.startsWith("data:")) + .map((line) => line.slice(5).trim()) + .filter((line) => line && line !== "[DONE]") + + const parsed = sseData.map(parseJson).find((item) => item && typeof item === "object") + if (parsed) return parsed as Record + + return { + error: { + message: sseData[0] || text || statusText, + }, + } +} + +function parseJson(value: string) { + try { + return JSON.parse(value) as unknown + } catch { + return undefined + } +} diff --git a/packages/console/app/test/providerError.test.ts b/packages/console/app/test/providerError.test.ts new file mode 100644 index 0000000000..38d4f3c208 --- /dev/null +++ b/packages/console/app/test/providerError.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { parseProviderErrorBody } from "../src/routes/zen/util/providerError" + +describe("provider error parsing", () => { + test("parses SSE error bodies from upstream rate limits", () => { + expect( + parseProviderErrorBody( + 'event:error\ndata: {"error":{"type":"rate_limit_error","message":"Too many requests"}}\n\n', + "Too Many Requests", + ), + ).toEqual({ + error: { + type: "rate_limit_error", + message: "Too many requests", + }, + }) + }) + + test("wraps plain text errors in provider error shape", () => { + expect(parseProviderErrorBody("overloaded", "Too Many Requests")).toEqual({ + error: { + message: "overloaded", + }, + }) + }) +}) From 8378856a905a5e0ff426d0c9787876bbd7c0f7af Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 2 Jun 2026 13:54:04 +0000 Subject: [PATCH 2/2] refactor(console): simplify provider error parsing --- .../app/src/routes/zen/util/handler.ts | 12 ++++++++-- .../app/src/routes/zen/util/providerError.ts | 24 +++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index dc8649c6d1..48514e5865 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -48,7 +48,7 @@ import { localeFromRequest } from "~/lib/language" import { createModelTpmLimiter } from "./modelTpmLimiter" import { createModelTpsLimiter } from "./modelTpsLimiter" import { accumulateUsage, HOT_WORKSPACES } from "./usageBatcher" -import { readProviderError } from "./providerError" +import { parseProviderErrorBody } from "./providerError" type ZenData = Awaited> type RetryOptions = { @@ -249,7 +249,15 @@ export async function handler( // Handle non-streaming response if (!isStream || [400, 404, 429].includes(res.status)) { - const json = res.status === 200 ? await res.json() : await readProviderError(res) + const json = await (async () => { + if (res.status === 200) return res.json() + const body = await res.text() + try { + const parsed = JSON.parse(body) + if (parsed && typeof parsed === "object") return parsed as Record + } catch {} + return parseProviderErrorBody(body, res.statusText) + })() await rateLimiter?.track() const usage = providerInfo.extractUsage(json) if (usage) { diff --git a/packages/console/app/src/routes/zen/util/providerError.ts b/packages/console/app/src/routes/zen/util/providerError.ts index 2a793930f5..f52f605252 100644 --- a/packages/console/app/src/routes/zen/util/providerError.ts +++ b/packages/console/app/src/routes/zen/util/providerError.ts @@ -1,10 +1,3 @@ -export async function readProviderError(res: Response) { - const body = await res.text() - const json = parseJson(body) - if (json && typeof json === "object") return json as Record - return parseProviderErrorBody(body, res.statusText) -} - export function parseProviderErrorBody(body: string, statusText: string) { const text = body.trim() const sseData = text @@ -14,7 +7,14 @@ export function parseProviderErrorBody(body: string, statusText: string) { .map((line) => line.slice(5).trim()) .filter((line) => line && line !== "[DONE]") - const parsed = sseData.map(parseJson).find((item) => item && typeof item === "object") + const parsed = (() => { + for (const data of sseData) { + try { + const json = JSON.parse(data) + if (json && typeof json === "object") return json as Record + } catch {} + } + })() if (parsed) return parsed as Record return { @@ -23,11 +23,3 @@ export function parseProviderErrorBody(body: string, statusText: string) { }, } } - -function parseJson(value: string) { - try { - return JSON.parse(value) as unknown - } catch { - return undefined - } -}