From 8809590841d6f32d5c886e334ecd8f8eaf89a8ed Mon Sep 17 00:00:00 2001 From: skogaby Date: Thu, 30 Jul 2026 00:35:19 -0500 Subject: [PATCH] fix(llm): stop classifying Bedrock throttling as context overflow Bedrock's ThrottlingException message "Too many tokens, please wait before trying again." matched the /too many tokens/i overflow pattern while the throttling exclusion only matched a literal "throttling error:" prefix the AI SDK never emits. The 429 was converted into a ContextOverflowError, which skipped retry and triggered a spurious auto-compaction instead of backing off. Broaden the exclusion list to match throttle vocabulary and transient retry language anywhere in the message, and gate parseAPICallError so a 429 status is never classified as context overflow. --- packages/llm/src/provider-error.ts | 11 ++++++++++- packages/llm/test/provider-error.test.ts | 5 +++++ packages/opencode/src/provider/error.ts | 7 ++++++- packages/opencode/test/session/retry.test.ts | 18 ++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/llm/src/provider-error.ts b/packages/llm/src/provider-error.ts index f8b8a5c013e1..da743018658b 100644 --- a/packages/llm/src/provider-error.ts +++ b/packages/llm/src/provider-error.ts @@ -31,7 +31,16 @@ const patterns = [ /token limit exceeded/i, ] -const exclusions = [/^(throttling error|service unavailable):/i, /rate limit/i, /too many requests/i] +// Throttling messages can mention tokens (e.g. Bedrock's ThrottlingException +// "Too many tokens, please wait before trying again.") without any prefix, so +// exclude throttle vocabulary and transient retry language anywhere in the message. +const exclusions = [ + /^service unavailable:/i, + /throttl/i, + /rate limit/i, + /too many requests/i, + /please (wait|try again)/i, +] export const isContextOverflow = (message: string) => !exclusions.some((pattern) => pattern.test(message)) && diff --git a/packages/llm/test/provider-error.test.ts b/packages/llm/test/provider-error.test.ts index c4185969640a..7fd937349a89 100644 --- a/packages/llm/test/provider-error.test.ts +++ b/packages/llm/test/provider-error.test.ts @@ -21,6 +21,11 @@ describe("provider error classification", () => { test("does not classify rate limits as context overflow", () => { const messages = [ "Throttling error: Too many tokens, please wait before trying again.", + // Bedrock ThrottlingException via @ai-sdk/amazon-bedrock (no prefix) + "Too many tokens, please wait before trying again.", + "ThrottlingException: Too many tokens, please wait before trying again.", + "undefined: Too many tokens, please wait before trying again.", + "Too many tokens, please try again later.", "Rate limit exceeded, please retry after 30 seconds.", "Too many requests. Please slow down.", ] diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 21149a2cf389..71540b1cf1fb 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -165,7 +165,12 @@ export type ParsedAPICallError = export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError { const m = message(input.providerID, input.error) const body = json(input.error.responseBody) - if (isContextOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") { + // 429 is throttling, never a context overflow, even when the message mentions + // tokens (e.g. Bedrock's "Too many tokens, please wait before trying again.") + const overflow = + input.error.statusCode !== 429 && + (isContextOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") + if (overflow) { return { type: "context_overflow", message: m, diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 30ac879a6a9d..07f28fdc648e 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -413,6 +413,24 @@ describe("session.message-v2.fromError", () => { expect(result.data.isRetryable).toBe(true) }) + test("classifies Bedrock throttling as retryable APIError, not context overflow", () => { + const error = new APICallError({ + message: "Too many tokens, please wait before trying again.", + url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-sonnet/converse-stream", + requestBodyValues: {}, + statusCode: 429, + responseHeaders: { "content-type": "application/json" }, + responseBody: '{"message":"Too many tokens, please wait before trying again."}', + isRetryable: true, + }) + const result = MessageV2.fromError(error, { providerID: ProviderV2.ID.make("amazon-bedrock") }) + expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false) + if (!SessionV1.APIError.isInstance(result)) throw new Error("expected APIError") + expect(result.data.isRetryable).toBe(true) + expect(result.data.statusCode).toBe(429) + expect(SessionRetry.retryable(result, retryProvider)).toBeDefined() + }) + test("converts OpenAI server_error stream chunks to retryable APIError", () => { const result = MessageV2.fromError( {