From 4e7a41fada8616341ece22610a49c3a0fe545a14 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 21 Sep 2026 13:16:35 -0400 Subject: [PATCH 1/2] fix(llm): prevent Bedrock throttling errors from being misclassified as context overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bedrock ThrottlingException messages (e.g. 'Token throughput limit exceeded', 'Too many tokens per minute') match the broad regex patterns in isContextOverflow (specifically /token limit exceeded/i, /too many tokens/i, /exceeds the limit of/i) and are misclassified as context-window overflow. This triggers the compaction recovery path, which also fails (same throttle), producing a misleading 'Conversation history too large to compact' error. The misclassification is amplified by the Bedrock AI SDK adapter's known quirk of coercing undefined to the string 'undefined' — the message() function in provider/error.ts correctly extracts the real error from responseBody, but strips the SDK's '429 Too Many Requests' prefix that would have triggered the 'too many requests' exclusion. Three fixes: 1. Status code guard in parseAPICallError: HTTP 429 and 5xx are never context overflow regardless of message text. This is the highest-leverage fix — every Bedrock throttling error arriving as HTTP 429 is now immune. 2. Tighter isContextOverflow patterns + broader exclusions: - /exceeds the limit of/ now requires a context/input/prompt noun prefix - New exclusions for 'throughput', 'per minute/second/hour/day', 'invocation.*limit/quota', and 'quota' 3. Tests for Bedrock throttling messages that must NOT be classified as context overflow, covering both the pattern-level check and the full APICallError parsing path (including the 'undefined' message workaround). --- packages/app-bundle/manifest.json | 14 +++-- .../packages/llm/src/provider-error.ts | 55 ++++++++++++++++++ .../packages/llm/test/provider-error.test.ts | 56 +++++++++++++++++++ .../packages/opencode/src/provider/error.ts | 10 +++- .../opencode/test/session/message-v2.test.ts | 50 +++++++++++++++++ 5 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 packages/app-bundle/overlay/packages/llm/src/provider-error.ts create mode 100644 packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 75364999f..6ae978643 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -43,7 +43,9 @@ "packages/core/test/location-mutation.test.ts": "2c4c8faa220cc8fabfe12d57bd74ec8dd8aac8485d0ecafd2761cdee1806d6b0", "packages/core/test/tool-question.test.ts": "4df41393f15104a1d3a9c3d06e30cef515108490b41fcf4d9a7d9399fd4ed945", "packages/llm/src/cache-policy.ts": "04c1fb5d3d061cfa95d04e54df0a1c2cdaafac921224242446eaa2968bff7857", + "packages/llm/src/provider-error.ts": "b623fc2106b3a1749e2411f77ec4f632964bed12b99ae4d4e77f6c655e57c505", "packages/llm/test/cache-policy.test.ts": "e921095c4301440c786105063439e49b2fedbaf6d46f30457e0a96388d268f4b", + "packages/llm/test/provider-error.test.ts": "7354a4115e0fad9beeb4d824b34a4d659fd776539c0569a20aea4aca3cb32372", "packages/opencode/script/build.ts": "876e7d569aa71ce060c1d4b24c96e9bf13245cbf4ca743828d645560a95aa641", "packages/opencode/script/httpapi-exercise.ts": "772bd2cf5748ca90c5d5790c2d044237518cc8ede0411fc4ba28b9d7440ba28c", "packages/opencode/test/amicode-release-workflow.test.ts": "f6dfcb3ed9c572ca8e8b21ff82e038872b635b25c68f9580f6d6f51ee2813189", @@ -274,7 +276,7 @@ "packages/opencode/src/project/instance-context.ts": "65a35792e50e96a50ea14cd9f928045c0e2fc15eb49506f5bbceee38c4af1457", "packages/opencode/src/project/instance-store.ts": "c22d3e2499e63dd8d9395d58189df75444d306774df833e746f22e327edcf2ab", "packages/opencode/src/provider/auth.ts": "b0ab1322c1039e27176bf9e3500603eae27506065ac141a69710220f3361f6b8", - "packages/opencode/src/provider/error.ts": "7f3c8210c775e2ce98140bf015dc97e61192912af062c75e18ecb09ee2736a0f", + "packages/opencode/src/provider/error.ts": "d72310be26cfae5c2943f9939a6461e848532a5646ff405d628e9a50fc1ebe91", "packages/opencode/src/provider/provider.ts": "671f6d8de83f919ddaa69b8240cacd82e2b8202ab505d4334175f2858bbd8c32", "packages/opencode/src/provider/transform.ts": "d35a95a6092b5dd9cc8e37adb4d52f9ad9880d021cb3119c1d4abbc4b4fb0772", "packages/opencode/src/server/server.ts": "56b3c2047917aa80d1dfb29bfa9839bcc2ad7f5a6f4db12103cd962a98fba866", @@ -344,7 +346,7 @@ "packages/opencode/test/session/business-record.test.ts": "7857094479408bcfd6ea8d9f5b425c6384bcf8490f930674bc1c203e82782d75", "packages/opencode/test/session/instruction.test.ts": "8b4d954eb10afa852a671e6c0a94d19604deacc9a62f9c3651c7456cc32736b7", "packages/opencode/test/session/llm-native-recorded.test.ts": "7139b58569af0172862af521e8f8d7a78132f40f06dcceb4baf8ca3a2f07d3bd", - "packages/opencode/test/session/message-v2.test.ts": "9edc16ae57b7f2d34aaed7378d82fcf9d07a82075a05730bfb3caae39d70ac7f", + "packages/opencode/test/session/message-v2.test.ts": "d1b930b4b57243387920b76fb68680e507e3a288d50d06c357d83eb7931da735", "packages/opencode/test/session/mutation.test.ts": "ccb82b6858ac097d2af09a94497d05f6a0ff843b928a69e763fdb510905a10cd", "packages/opencode/test/session/prompt.test.ts": "e3d0748edb833640ef284857ae725e4b974843480d4128f56239e3fd1e6d481f", "packages/opencode/test/session/prose-guard.test.ts": "7239a758cbf27df4c3879fa9ee0374964559c1461773c2f5a85196f64632e6c4", @@ -849,7 +851,7 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-09-21T02:29:31.939Z", + "extracted_at": "2026-09-21T17:09:47.901Z", "per_package": { "packages/app": { "A": 171, @@ -863,7 +865,7 @@ }, "packages/llm": { "A": 0, - "M": 4, + "M": 6, "D": 0 }, "packages/opencode": { @@ -893,7 +895,7 @@ } }, "counts": { - "overlay_total": 827, + "overlay_total": 829, "deletions": 7, "server_coupled": 0 }, @@ -933,7 +935,9 @@ "packages/core/test/location-mutation.test.ts": "M", "packages/core/test/tool-question.test.ts": "M", "packages/llm/src/cache-policy.ts": "M", + "packages/llm/src/provider-error.ts": "M", "packages/llm/test/cache-policy.test.ts": "M", + "packages/llm/test/provider-error.test.ts": "M", "packages/opencode/script/build.ts": "M", "packages/opencode/script/httpapi-exercise.ts": "M", "packages/opencode/test/amicode-release-workflow.test.ts": "A", diff --git a/packages/app-bundle/overlay/packages/llm/src/provider-error.ts b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts new file mode 100644 index 000000000..f20017834 --- /dev/null +++ b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts @@ -0,0 +1,55 @@ +import { Schema } from "effect" +import { LLMError, ProviderErrorEvent } from "./schema" + +const patterns = [ + /prompt is too long/i, + /request_too_large/i, + /input is too long for requested model/i, + /exceeds the context window/i, + /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i, + /input token count.*exceeds the maximum/i, + /tokens in request more than max tokens allowed/i, + /maximum prompt length is \d+/i, + /reduce the length of the messages/i, + /maximum context length is \d+ tokens/i, + /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i, + /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i, + // Tightened: require a context/input/prompt noun so throughput/quota limits + // like "exceeds the limit of 100000 tokens per minute" don't false-positive. + /(?:input|context|prompt|message).*exceeds the limit of \d+/i, + /exceeds the available context size/i, + /greater than the context length/i, + /context window exceeds limit/i, + /exceeded model token limit/i, + /context[_ ]length[_ ]exceeded/i, + /request entity too large/i, + /context length is only \d+ tokens/i, + /input length.*exceeds.*context length/i, + /prompt too long; exceeded (?:max )?context length/i, + /too large for model with \d+ maximum context length/i, + /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, + /model_context_window_exceeded/i, + /too many tokens/i, + /token limit exceeded/i, +] + +const exclusions = [ + /^(throttling error|service unavailable):/i, + /rate limit/i, + /too many requests/i, + // Bedrock ThrottlingException messages that match broad patterns above but + // are throughput/quota limits, not context-window overflow. + /throughput/i, + /per\s+(minute|second|hour|day)/i, + /invocation.*(limit|quota|exceeded)/i, + /quota/i, +] + +export const isContextOverflow = (message: string) => + !exclusions.some((pattern) => pattern.test(message)) && + (patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)) + +export const isContextOverflowFailure = (failure: unknown) => + failure instanceof LLMError + ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow" + : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow" diff --git a/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts new file mode 100644 index 000000000..123bfaeb9 --- /dev/null +++ b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test" +import { isContextOverflow } from "../src" + +describe("provider error classification", () => { + test("classifies provider token limit messages as context overflow", () => { + const messages = [ + "tokens in request more than max tokens allowed", + '{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}', + "Requested token count exceeds the model's maximum context length of 131072 tokens.", + "Input length (265330) exceeds model's maximum context length (262144).", + "Input length 131393 exceeds the maximum allowed input length of 131040 tokens.", + "The input (516368 tokens) is longer than the model's context length (262144 tokens).", + "Prompt has 5,958,968 tokens, but the configured context size is 256,000 tokens", + "Too many tokens", + "Token limit exceeded", + ] + + expect(messages.every(isContextOverflow)).toBe(true) + }) + + test("does not classify rate limits as context overflow", () => { + const messages = [ + "Throttling error: Too many tokens, please wait before trying again.", + "Rate limit exceeded, please retry after 30 seconds.", + "Too many requests. Please slow down.", + ] + + expect(messages.some(isContextOverflow)).toBe(false) + }) + + test("does not classify Bedrock throttling/quota messages as context overflow", () => { + const messages = [ + // ThrottlingException messages that match broad patterns (too many tokens, + // token limit exceeded, exceeds the limit) but are throughput limits. + "Token throughput limit exceeded", + "Too many tokens per minute for model anthropic.claude-opus-4-8-20250808-v1:0", + "You have exceeded the model invocation throughput limit of 100000 tokens per minute", + "Model invocation throughput limit exceeded", + "Insufficient quota for this request", + "Request quota exceeded, please try again later", + "Token throughput quota exceeded: too many tokens per second", + ] + + expect(messages.some(isContextOverflow)).toBe(false) + }) + + test("still classifies real Bedrock context overflow as context overflow", () => { + const messages = [ + "Input is too long for requested model. Max input token count: 200000, actual token count: 250000", + "Input is too long for requested model", + "Input length 265330 exceeds the context window of 200000 tokens", + ] + + expect(messages.every(isContextOverflow)).toBe(true) + }) +}) diff --git a/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts b/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts index 4c72e3750..592590277 100644 --- a/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts +++ b/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts @@ -176,7 +176,15 @@ 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") { + // Status-code guard: 429 (rate limit) and 5xx (server error) are never + // context overflow, even when the error message text happens to match the + // heuristic patterns (e.g. Bedrock ThrottlingException "token throughput + // limit exceeded" matches /token limit exceeded/i but is a rate limit). + // The `undefined` status case (stream errors without HTTP metadata) still + // runs the heuristic — only known non-overflow statuses are excluded. + const status = input.error.statusCode + const overflowEligible = status === undefined || (status >= 400 && status < 500 && status !== 429) + if (overflowEligible && (isContextOverflow(m) || status === 413 || body?.error?.code === "context_length_exceeded")) { return { type: "context_overflow", message: m, diff --git a/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts b/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts index cb0e47230..db8e390ca 100644 --- a/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts +++ b/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts @@ -1538,6 +1538,56 @@ describe("session.message-v2.fromError", () => { expect(SessionV1.APIError.isInstance(result)).toBe(true) }) + test("does not classify Bedrock 429 throttling with overflow-matching body as context overflow", () => { + // Bedrock's @ai-sdk/amazon-bedrock adapter coerces undefined to "undefined", + // stripping the SDK's "429 Too Many Requests" prefix that would otherwise + // trigger the "too many requests" exclusion in isContextOverflow. The status + // code guard in parseAPICallError must prevent the raw Bedrock message + // (which matches /token limit exceeded/i) from being misclassified. + const bedrockMessages = [ + "Token throughput limit exceeded", + "Too many tokens per minute for model anthropic.claude-opus-4-8-20250808-v1:0", + "You have exceeded the model invocation throughput limit of 100000 tokens per minute", + ] + + for (const bodyMessage of bedrockMessages) { + const result = MessageV2.fromError( + new APICallError({ + message: "undefined", + url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-opus-4-8-20250808-v1%3A0/converse-stream", + requestBodyValues: {}, + statusCode: 429, + responseHeaders: { "content-type": "application/json" }, + responseBody: JSON.stringify({ message: bodyMessage }), + isRetryable: true, + }), + { providerID: ProviderV2.ID.make("amazon-bedrock") }, + ) + expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false) + expect(SessionV1.APIError.isInstance(result)).toBe(true) + } + }) + + test("does not classify Bedrock 400 throttling with overflow-matching body as context overflow", () => { + // Bedrock ValidationException (HTTP 400) can carry throughput messages that + // match context-overflow patterns. The broadened exclusions in + // isContextOverflow must prevent these from being misclassified. + const result = MessageV2.fromError( + new APICallError({ + message: "undefined", + url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-opus-4-8-20250808-v1%3A0/converse-stream", + requestBodyValues: {}, + statusCode: 400, + responseHeaders: { "content-type": "application/json" }, + responseBody: JSON.stringify({ message: "Token throughput limit exceeded" }), + isRetryable: false, + }), + { providerID: ProviderV2.ID.make("amazon-bedrock") }, + ) + expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false) + expect(SessionV1.APIError.isInstance(result)).toBe(true) + }) + test("serializes unknown inputs", () => { const result = MessageV2.fromError(123, { providerID }) From 34e910a09272d18927ddcf5764f3113030081fd5 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:33:26 +0000 Subject: [PATCH 2/2] fix(llm): require exact matches for generic token overflow errors to avoid misclassifying quota limits --- packages/app-bundle/manifest.json | 6 +++--- .../app-bundle/overlay/packages/llm/src/provider-error.ts | 4 ++-- .../overlay/packages/llm/test/provider-error.test.ts | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 6ae978643..0fa26ca33 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -43,9 +43,9 @@ "packages/core/test/location-mutation.test.ts": "2c4c8faa220cc8fabfe12d57bd74ec8dd8aac8485d0ecafd2761cdee1806d6b0", "packages/core/test/tool-question.test.ts": "4df41393f15104a1d3a9c3d06e30cef515108490b41fcf4d9a7d9399fd4ed945", "packages/llm/src/cache-policy.ts": "04c1fb5d3d061cfa95d04e54df0a1c2cdaafac921224242446eaa2968bff7857", - "packages/llm/src/provider-error.ts": "b623fc2106b3a1749e2411f77ec4f632964bed12b99ae4d4e77f6c655e57c505", + "packages/llm/src/provider-error.ts": "65b9124905f0755c1e368df5150aa211bea3cc29afb66845b5ceaccba10042dd", "packages/llm/test/cache-policy.test.ts": "e921095c4301440c786105063439e49b2fedbaf6d46f30457e0a96388d268f4b", - "packages/llm/test/provider-error.test.ts": "7354a4115e0fad9beeb4d824b34a4d659fd776539c0569a20aea4aca3cb32372", + "packages/llm/test/provider-error.test.ts": "ac3bf099319acdc27ff47ea79fd74357d55e12ab95131a07a72a5a2a9bc0c08f", "packages/opencode/script/build.ts": "876e7d569aa71ce060c1d4b24c96e9bf13245cbf4ca743828d645560a95aa641", "packages/opencode/script/httpapi-exercise.ts": "772bd2cf5748ca90c5d5790c2d044237518cc8ede0411fc4ba28b9d7440ba28c", "packages/opencode/test/amicode-release-workflow.test.ts": "f6dfcb3ed9c572ca8e8b21ff82e038872b635b25c68f9580f6d6f51ee2813189", @@ -851,7 +851,7 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-09-21T17:09:47.901Z", + "extracted_at": "2026-09-21T17:30:46.454Z", "per_package": { "packages/app": { "A": 171, diff --git a/packages/app-bundle/overlay/packages/llm/src/provider-error.ts b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts index f20017834..29c81cbc2 100644 --- a/packages/app-bundle/overlay/packages/llm/src/provider-error.ts +++ b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts @@ -29,8 +29,8 @@ const patterns = [ /too large for model with \d+ maximum context length/i, /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, /model_context_window_exceeded/i, - /too many tokens/i, - /token limit exceeded/i, + /^too many tokens$/i, + /^token limit exceeded$/i, ] const exclusions = [ diff --git a/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts index 123bfaeb9..13066525a 100644 --- a/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts +++ b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts @@ -23,6 +23,7 @@ describe("provider error classification", () => { "Throttling error: Too many tokens, please wait before trying again.", "Rate limit exceeded, please retry after 30 seconds.", "Too many requests. Please slow down.", + "Monthly token limit exceeded", ] expect(messages.some(isContextOverflow)).toBe(false)