From 53bfe40a35d41a3dbb6bd2d76b2918ce3c9ff5f4 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:10:32 -0700 Subject: [PATCH] fix(errors): classify throttle messages before token-overflow patterns; add new overflow shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port from anomalyco/opencode#37848 (+ dev-branch twin #37840): expand context-overflow patterns and guard against rate-limit messages that mention tokens. - 'Throttling error: Too many tokens, please wait before trying again.' (AWS Bedrock / proxy shape) classified as context_overflow and routed a healthy session into compression on every throttle. Added 'throttling' to _RATE_LIMIT_PATTERNS, which the message-only path checks BEFORE the overflow list. - 'Input length N exceeds the maximum allowed input length of M tokens.' (Together/Fireworks shape) fell through to unknown — no compression recovery. Added 'maximum allowed input length' to overflow patterns. - 'request_too_large' / 'Request exceeds the maximum size' (Anthropic 413 type re-wrapped without a status code by aggregators/proxies) fell through to unknown. Added to _PAYLOAD_TOO_LARGE_PATTERNS. All three shapes proven live on main before the fix; 265 classifier + bedrock tests and 238 sibling rate-guard/compression tests pass. --- agent/error_classifier.py | 18 +++++++ tests/agent/test_error_classifier.py | 72 ++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 33c2f5458560..e629e7b7af9e 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -159,6 +159,14 @@ _RATE_LIMIT_PATTERNS = [ "throttlingexception", "too many concurrent requests", "servicequotaexceededexception", + # Generic throttle prefix — Bedrock (and some proxies) surface throttling + # as "Throttling error: Too many tokens, please wait before trying + # again." Without this entry the message falls through to the + # context-overflow list (which contains "too many tokens") and the retry + # loop compresses a healthy session instead of backing off. Matched + # BEFORE _CONTEXT_OVERFLOW_PATTERNS in the message-only path, so the + # throttle wins. (port of anomalyco/opencode#37848's exclusion guard) + "throttling", ] # Patterns that indicate provider-side overload, NOT a per-credential rate @@ -212,6 +220,12 @@ _PAYLOAD_TOO_LARGE_PATTERNS = [ "request entity too large", "payload too large", "error code: 413", + # Anthropic's structured 413 error type. Normally arrives with an HTTP + # 413 status (handled by the status path), but aggregators/proxies can + # re-wrap it into a plain message with no status attribute — route it to + # the same compression recovery. (port of anomalyco/opencode#37848) + "request_too_large", + "request exceeds the maximum size", ] # Image-size patterns. Matched against 400 bodies (not 413) because most @@ -298,6 +312,10 @@ _CONTEXT_OVERFLOW_PATTERNS = [ "max input token", "input token", "exceeds the maximum number of input tokens", + # Together/Fireworks-style: "Input length 131393 exceeds the maximum + # allowed input length of 131040 tokens." No other pattern in this list + # matches that wording. (port of anomalyco/opencode#37848) + "maximum allowed input length", ] # Model not found patterns diff --git a/tests/agent/test_error_classifier.py b/tests/agent/test_error_classifier.py index 8ef407f50fb2..363462ee1d2c 100644 --- a/tests/agent/test_error_classifier.py +++ b/tests/agent/test_error_classifier.py @@ -2170,3 +2170,75 @@ class Test408RequestTimeout: assert result.should_fallback is True assert result.should_compress is False + +# ── Test: throttle vs overflow disambiguation + new overflow shapes ───── +# Port of anomalyco/opencode#37848 (expand context overflow patterns + +# rate-limit exclusion guard). + +class TestThrottleVsOverflowDisambiguation: + """Throttle messages that mention tokens must NOT route to compression.""" + + def test_bedrock_throttling_too_many_tokens_is_rate_limit(self): + # AWS Bedrock (and some proxies) surface throttling as + # "Throttling error: Too many tokens, please wait before trying + # again." — the "too many tokens" fragment sits in + # _CONTEXT_OVERFLOW_PATTERNS, so before the "throttling" rate-limit + # pattern this compressed a healthy session on every throttle. + e = Exception( + "Throttling error: Too many tokens, please wait before trying again." + ) + result = classify_api_error(e, provider="bedrock", model="claude") + assert result.reason == FailoverReason.rate_limit + assert result.should_compress is False + + def test_plain_too_many_tokens_still_overflow(self): + # Without any throttle wording, "Too many tokens" remains a + # context-overflow signal (Z.AI / GLM family wording). + e = Exception("Too many tokens") + result = classify_api_error(e, provider="zai", model="glm-5") + assert result.reason == FailoverReason.context_overflow + assert result.should_compress is True + + +class TestExpandedOverflowPatterns: + """New provider overflow wordings route into compression recovery.""" + + def test_maximum_allowed_input_length_is_overflow(self): + # Together/Fireworks-style wording — matched no pattern before. + e = Exception( + "Input length 131393 exceeds the maximum allowed input length " + "of 131040 tokens." + ) + result = classify_api_error(e, provider="together", model="m") + assert result.reason == FailoverReason.context_overflow + assert result.should_compress is True + + def test_request_too_large_message_only_is_payload_too_large(self): + # Anthropic's structured 413 type re-wrapped by a proxy with no + # status attribute — was falling through to `unknown`. + e = Exception( + '{"error":{"type":"request_too_large",' + '"message":"Request exceeds the maximum size"}}' + ) + result = classify_api_error(e, provider="anthropic", model="m") + assert result.reason == FailoverReason.payload_too_large + assert result.should_compress is True + + def test_longer_than_context_length_still_overflow(self): + # Regression guard for wordings that already matched. + e = Exception( + "The input (516368 tokens) is longer than the model's context " + "length (262144 tokens)." + ) + result = classify_api_error(e, provider="openrouter", model="m") + assert result.reason == FailoverReason.context_overflow + + def test_configured_context_size_still_overflow(self): + e = Exception( + "Prompt has 5,958,968 tokens, but the configured context size " + "is 256,000 tokens" + ) + result = classify_api_error(e, provider="ollama", model="m") + assert result.reason == FailoverReason.context_overflow + +