From f1ad5e27e035461f3cda94342a1da14ecd2a5538 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 23 Apr 2026 14:03:05 -0700 Subject: [PATCH] fix(agents): surface OpenAI model capacity errors --- ...bedded-helpers.formatassistanterrortext.test.ts | 6 ++++++ ...embedded-helpers.sanitizeuserfacingtext.test.ts | 11 +++++++++++ .../pi-embedded-helpers/failover-matches.test.ts | 7 +++++++ src/agents/pi-embedded-helpers/failover-matches.ts | 1 + .../sanitize-user-facing-text.ts | 6 ++++++ .../pi-embedded-runner/run/payloads.errors.test.ts | 14 ++++++++++++++ 6 files changed, 45 insertions(+) diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index 606eff5c516..a3dcc97fb4d 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -64,6 +64,12 @@ describe("formatAssistantErrorText", () => { "The AI service is temporarily overloaded. Please try again in a moment.", ); }); + it("returns a model-switch hint for OpenAI model capacity errors", () => { + const msg = makeAssistantError("Selected model is at capacity. Please try a different model."); + expect(formatAssistantErrorText(msg)).toBe( + "⚠️ Selected model is at capacity. Try a different model, or wait and retry.", + ); + }); it("returns a recovery hint when tool call input is missing", () => { const msg = makeAssistantError("tool_use.input: Field required"); const result = formatAssistantErrorText(msg); diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts index 6e2469caa0c..be73ad35513 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts @@ -141,6 +141,17 @@ describe("sanitizeUserFacingText", () => { ); }); + it("returns a model-switch hint for OpenAI model capacity errors", () => { + expect( + sanitizeUserFacingText( + "OpenAI error: Selected model is at capacity. Please try a different model.", + { + errorContext: true, + }, + ), + ).toBe("⚠️ Selected model is at capacity. Try a different model, or wait and retry."); + }); + it("returns a transport-specific message for prefixed ECONNREFUSED errors", () => { expect( sanitizeUserFacingText("Error: connect ECONNREFUSED 127.0.0.1:443", { diff --git a/src/agents/pi-embedded-helpers/failover-matches.test.ts b/src/agents/pi-embedded-helpers/failover-matches.test.ts index 41ca8460285..a7b7c0928af 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.test.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { isAuthErrorMessage, isBillingErrorMessage, + isOverloadedErrorMessage, isRateLimitErrorMessage, } from "./failover-matches.js"; @@ -68,6 +69,12 @@ describe("Z.ai vendor error codes (#48988)", () => { expect(isRateLimitErrorMessage("rate limit exceeded")).toBe(true); }); + it("OpenAI model-capacity text is classified as overloaded", () => { + expect( + isOverloadedErrorMessage("Selected model is at capacity. Please try a different model."), + ).toBe(true); + }); + it("billing still classified correctly", () => { expect(isBillingErrorMessage("insufficient credits")).toBe(true); }); diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/pi-embedded-helpers/failover-matches.ts index c8b97158ae4..de871aa6809 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -73,6 +73,7 @@ const ERROR_PATTERNS = { overloaded: [ /overloaded_error|"type"\s*:\s*"overloaded_error"/i, "overloaded", + /\b(?:selected\s+)?model\s+(?:is\s+)?at capacity\b/i, // Match "service unavailable" only when combined with an explicit overload // indicator — a generic 503 from a proxy/CDN should not be classified as // provider-overload (#32828). diff --git a/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts b/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts index c45206851e8..228af38e30c 100644 --- a/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts +++ b/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts @@ -34,6 +34,8 @@ export function formatBillingErrorMessage(provider?: string, model?: string): st export const BILLING_ERROR_USER_MESSAGE = formatBillingErrorMessage(); const RATE_LIMIT_ERROR_USER_MESSAGE = "⚠️ API rate limit reached. Please try again later."; +const MODEL_CAPACITY_ERROR_USER_MESSAGE = + "⚠️ Selected model is at capacity. Try a different model, or wait and retry."; const OVERLOADED_ERROR_USER_MESSAGE = "The AI service is temporarily overloaded. Please try again in a moment."; const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/gi; @@ -60,6 +62,7 @@ const HTTP_ERROR_HINTS = [ ]; const RATE_LIMIT_SPECIFIC_HINT_RE = /\bmin(ute)?s?\b|\bhours?\b|\bseconds?\b|\btry again in\b|\breset\b|\bplan\b|\bquota\b/i; +const MODEL_CAPACITY_ERROR_RE = /\b(?:selected\s+)?model\s+(?:is\s+)?at capacity\b/i; const NON_ERROR_PROVIDER_PAYLOAD_MAX_LENGTH = 16_384; const NON_ERROR_PROVIDER_PAYLOAD_PREFIX_RE = /^codex\s*error(?:\s+\d{3})?[:\s-]+/i; @@ -93,6 +96,9 @@ export function formatRateLimitOrOverloadedErrorCopy(raw: string): string | unde if (isRateLimitErrorMessage(raw)) { return extractProviderRateLimitMessage(raw) ?? RATE_LIMIT_ERROR_USER_MESSAGE; } + if (MODEL_CAPACITY_ERROR_RE.test(raw)) { + return MODEL_CAPACITY_ERROR_USER_MESSAGE; + } if (isOverloadedErrorMessage(raw)) { return OVERLOADED_ERROR_USER_MESSAGE; } diff --git a/src/agents/pi-embedded-runner/run/payloads.errors.test.ts b/src/agents/pi-embedded-runner/run/payloads.errors.test.ts index 5aa8dfe7fd6..525d6f94433 100644 --- a/src/agents/pi-embedded-runner/run/payloads.errors.test.ts +++ b/src/agents/pi-embedded-runner/run/payloads.errors.test.ts @@ -101,6 +101,20 @@ describe("buildEmbeddedRunPayloads", () => { expect(payloads.some((payload) => payload.text?.includes("request_id"))).toBe(false); }); + it("surfaces OpenAI model capacity errors instead of generic empty-response copy", () => { + const payloads = buildPayloads({ + lastAssistant: makeAssistant({ + errorMessage: "Selected model is at capacity. Please try a different model.", + content: [], + }), + }); + + expectSinglePayloadSummary(payloads, { + text: "⚠️ Selected model is at capacity. Try a different model, or wait and retry.", + isError: true, + }); + }); + it("includes provider and model context for billing errors", () => { const payloads = buildPayloads({ lastAssistant: makeAssistant({