From bb8e75a00da5c14aeaf3f7fa2d288c9896fe0662 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:37:47 +0200 Subject: [PATCH] fix(resilience): surface Responses failed.error.message in 502s (#12472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam. A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente. O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio. Obrigado, @RaviTharuma. --- open-sse/utils/diagnostics.ts | 8 +++++++- tests/unit/diagnostics.test.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/open-sse/utils/diagnostics.ts b/open-sse/utils/diagnostics.ts index 06f7a0a0b6..ebee72e1e8 100644 --- a/open-sse/utils/diagnostics.ts +++ b/open-sse/utils/diagnostics.ts @@ -323,8 +323,14 @@ export function describeMalformedNonStream( ): { message: string; code: string; type: string } { const body = resp && typeof resp === "object" ? (resp as Record) : null; if (body?.object === "response" && body.status === "failed") { + const err = body.error && typeof body.error === "object" ? (body.error as Record) : null; + const rawMessage = + typeof err?.message === "string" && err.message.trim().length > 0 ? err.message.trim() : null; return { - message: "upstream reported a failed response without usable output", + // Trim only here; buildErrorBody (chatCore) does the single sanitization pass. + message: rawMessage + ? `upstream reported a failed response: ${rawMessage}` + : "upstream reported a failed response without usable output", code: "upstream_response_failed", type: "upstream_response_error", }; diff --git a/tests/unit/diagnostics.test.ts b/tests/unit/diagnostics.test.ts index dbcb643480..62da56fc0c 100644 --- a/tests/unit/diagnostics.test.ts +++ b/tests/unit/diagnostics.test.ts @@ -105,6 +105,22 @@ test("failed Responses API body gets a request-scoped machine-readable classific }); }); +test("failed Responses API body surfaces the upstream error message when present", () => { + const failed = { + object: "response", + status: "failed", + output: [], + error: { code: "server_error", message: " Gemini 503: overloaded " }, + }; + const reason = detectMalformedNonStream(failed); + assert.equal(reason, "empty_choices"); + assert.deepEqual(describeMalformedNonStream(failed, reason), { + message: "upstream reported a failed response: Gemini 503: overloaded", + code: "upstream_response_failed", + type: "upstream_response_error", + }); +}); + test("detectMalformedNonStream returns 'empty_choices' when choice message has no content", () => { const body = { choices: [{ message: { content: "", tool_calls: null }, finish_reason: "stop" }],