From 8b10bb23e1b7c35a717463dd26caaa3fa63b54de Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 11 Jun 2026 09:25:37 -0600 Subject: [PATCH 1/4] fix(gateway): surface in-stream errors from Zoo and Vercel AI gateways Once a gateway response starts streaming the HTTP status is already 200, so upstream failures (e.g. provider rate limits) arrive as an in-band error chunk rather than a thrown HTTP error. Both handlers ignored these chunks, so the extension showed a generic "no response" instead of the real reason. - zoo-gateway: detect the error chunk and rebuild it into an Error carrying status/code so the existing classify/surface logic handles it (sign-in, add-credits, budget, etc.), and the upstream message reaches the user. - vercel-ai-gateway: detect the error chunk and throw the upstream message. - Add unit tests covering both handlers and the toGatewayStreamError mapping. Co-authored-by: Cursor --- .../__tests__/vercel-ai-gateway.spec.ts | 22 ++++++ .../providers/__tests__/zoo-gateway.spec.ts | 70 ++++++++++++++++++- src/api/providers/vercel-ai-gateway.ts | 11 +++ src/api/providers/zoo-gateway.ts | 21 ++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts index 2fe4390fb5..8ac051b8fa 100644 --- a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts @@ -190,6 +190,28 @@ describe("VercelAiGatewayHandler", () => { }) }) + it("throws the upstream reason when an in-stream error chunk is received", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + error: { + message: "Too many requests, please wait before trying again", + code: 429, + }, + } + }, + })) + + const handler = new VercelAiGatewayHandler(mockOptions) + const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello" }]) + + await expect(async () => { + for await (const _chunk of stream) { + // drain + } + }).rejects.toThrow("Too many requests, please wait before trying again") + }) + it("uses correct temperature from options", async () => { const customTemp = 0.5 const handler = new VercelAiGatewayHandler({ diff --git a/src/api/providers/__tests__/zoo-gateway.spec.ts b/src/api/providers/__tests__/zoo-gateway.spec.ts index 0d82b14f1a..318518e63d 100644 --- a/src/api/providers/__tests__/zoo-gateway.spec.ts +++ b/src/api/providers/__tests__/zoo-gateway.spec.ts @@ -19,7 +19,7 @@ import OpenAI from "openai" import { zooGatewayDefaultModelId, ZOO_GATEWAY_DEFAULT_TEMPERATURE } from "@roo-code/types" -import { ZooGatewayHandler, classifyGatewayApiError } from "../zoo-gateway" +import { ZooGatewayHandler, classifyGatewayApiError, toGatewayStreamError } from "../zoo-gateway" import { ApiHandlerOptions } from "../../../shared/api" import { Package } from "../../../shared/package" import { clearZooCodeToken } from "../../../services/zoo-code-auth" @@ -363,6 +363,48 @@ describe("ZooGatewayHandler", () => { }, ]) }) + + it("throws the upstream reason when the gateway sends an in-stream error chunk", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + error: { + message: "Too many requests, please wait before trying again", + status: 429, + code: "rate_limited", + }, + } + }, + })) + + const handler = new ZooGatewayHandler(mockOptions) + + await expect(drainCreateMessage(handler)).rejects.toThrow( + "Too many requests, please wait before trying again", + ) + }) + + it("surfaces the add-credits prompt when an in-stream error carries a budget code", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + error: { + message: "Monthly budget exceeded", + status: 429, + code: "monthly_budget_exceeded", + }, + } + }, + })) + + const handler = new ZooGatewayHandler(mockOptions) + + await expect(drainCreateMessage(handler)).rejects.toThrow() + expect(showErrorMessage).toHaveBeenCalledWith( + "common:zooAuth.errors.budget_exceeded", + "common:zooAuth.buttons.add_credits", + ) + }) }) describe("completePrompt", () => { @@ -443,6 +485,32 @@ describe("ZooGatewayHandler", () => { }) }) + describe("toGatewayStreamError", () => { + it("preserves the message, status, and code from the chunk", () => { + const error = toGatewayStreamError({ + message: "rate limited", + status: 429, + code: "rate_limited", + }) as Error & { + status?: number + code?: string + } + + expect(error).toBeInstanceOf(Error) + expect(error.message).toBe("rate limited") + expect(error.status).toBe(429) + expect(error.code).toBe("rate_limited") + }) + + it("falls back to a default message and leaves status/code undefined", () => { + const error = toGatewayStreamError({}) as Error & { status?: number; code?: string } + + expect(error.message).toBe("Zoo Gateway stream error") + expect(error.status).toBeUndefined() + expect(error.code).toBeUndefined() + }) + }) + describe("surfaceGatewayApiError", () => { it("clears the cached token and offers re-sign-in on 401", async () => { const handler = new ZooGatewayHandler(mockOptions) diff --git a/src/api/providers/vercel-ai-gateway.ts b/src/api/providers/vercel-ai-gateway.ts index 51b0eb5f51..35d5a67bfe 100644 --- a/src/api/providers/vercel-ai-gateway.ts +++ b/src/api/providers/vercel-ai-gateway.ts @@ -69,6 +69,17 @@ export class VercelAiGatewayHandler extends RouterProvider implements SingleComp const completion = await this.client.chat.completions.create(body) for await (const chunk of completion) { + // Vercel AI Gateway reports mid-stream failures as an in-band error chunk + // rather than throwing, so surface it instead of returning an empty response. + if ("error" in chunk && chunk.error) { + const raw = chunk.error as { message?: unknown } + const message = + typeof raw.message === "string" && raw.message.length > 0 + ? raw.message + : "Vercel AI Gateway stream error" + throw new Error(message) + } + const delta = chunk.choices[0]?.delta if (delta?.content) { yield { diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index 5c0df04887..4724464ff3 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -37,6 +37,20 @@ function getApiErrorCode(error: unknown): string | undefined { return undefined } +// The gateway sends in-band stream errors as `{ message, status?, code? }`. Rebuild +// them into an Error carrying status/code so the same classify/surface logic that +// handles thrown HTTP errors applies to mid-stream failures too. +// Exported for unit tests. +export function toGatewayStreamError(raw: unknown): Error { + const err = raw as { message?: unknown; status?: unknown; code?: unknown } | null + const message = + typeof err?.message === "string" && err.message.length > 0 ? err.message : "Zoo Gateway stream error" + return Object.assign(new Error(message), { + status: typeof err?.status === "number" ? err.status : undefined, + code: typeof err?.code === "string" ? err.code : undefined, + }) +} + function buildZooCodeSignInUrl(): string { const callbackUri = encodeURIComponent( `${vscode.env.uriScheme}://${Package.publisher}.${Package.name}/auth-callback`, @@ -209,6 +223,13 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio }) for await (const chunk of completion) { + // Once the gateway starts streaming the HTTP status is already 200, so it + // reports upstream failures (e.g. provider rate limits) as an in-band error + // chunk. Surface it so the user sees the real reason instead of an empty reply. + if ("error" in chunk && chunk.error) { + throw toGatewayStreamError(chunk.error) + } + const delta = chunk.choices[0]?.delta if (delta?.content) { yield { From a2a41ec084802f5aa5d40da7570d4b071edb39c4 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 11 Jun 2026 09:28:08 -0600 Subject: [PATCH 2/4] chore: add changeset for gateway in-stream error propagation Co-authored-by: Cursor --- .changeset/gateway-stream-error-propagation.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gateway-stream-error-propagation.md diff --git a/.changeset/gateway-stream-error-propagation.md b/.changeset/gateway-stream-error-propagation.md new file mode 100644 index 0000000000..d0fe7c6f9f --- /dev/null +++ b/.changeset/gateway-stream-error-propagation.md @@ -0,0 +1,5 @@ +--- +"zoo-code": patch +--- + +Surface upstream errors that arrive mid-stream from the Zoo and Vercel AI gateways. Provider failures such as rate limits are sent as an in-band error chunk once the response is already streaming, so the extension previously showed a generic "no response" instead of the real reason. Both handlers now detect these chunks and surface the upstream message (and, for the Zoo gateway, the existing sign-in / add-credits / budget prompts). From 55bbcfd6a9c78ca96f571bc2210aba3bd42d9ab3 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 11 Jun 2026 10:04:50 -0600 Subject: [PATCH 3/4] test(gateway): cover vercel-ai-gateway in-stream error fallback message Adds a case where the in-band error chunk has no message, exercising the default "Vercel AI Gateway stream error" fallback branch flagged as uncovered. Co-authored-by: Cursor --- .../__tests__/vercel-ai-gateway.spec.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts index 8ac051b8fa..1d82901bfd 100644 --- a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts @@ -212,6 +212,23 @@ describe("VercelAiGatewayHandler", () => { }).rejects.toThrow("Too many requests, please wait before trying again") }) + it("throws a default message when an in-stream error chunk has no message", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { error: {} } + }, + })) + + const handler = new VercelAiGatewayHandler(mockOptions) + const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello" }]) + + await expect(async () => { + for await (const _chunk of stream) { + // drain + } + }).rejects.toThrow("Vercel AI Gateway stream error") + }) + it("uses correct temperature from options", async () => { const customTemp = 0.5 const handler = new VercelAiGatewayHandler({ From c496bcbe86b5a9ecec36f87534e101d440226390 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 11 Jun 2026 21:31:59 -0600 Subject: [PATCH 4/4] chore: remove changeset for gateway in-stream error propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer review — this change does not require a changeset. Co-authored-by: Cursor --- .changeset/gateway-stream-error-propagation.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/gateway-stream-error-propagation.md diff --git a/.changeset/gateway-stream-error-propagation.md b/.changeset/gateway-stream-error-propagation.md deleted file mode 100644 index d0fe7c6f9f..0000000000 --- a/.changeset/gateway-stream-error-propagation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"zoo-code": patch ---- - -Surface upstream errors that arrive mid-stream from the Zoo and Vercel AI gateways. Provider failures such as rate limits are sent as an in-band error chunk once the response is already streaming, so the extension previously showed a generic "no response" instead of the real reason. Both handlers now detect these chunks and surface the upstream message (and, for the Zoo gateway, the existing sign-in / add-credits / budget prompts).