fix(errors): classify throttle messages before token-overflow patterns; add new overflow shapes

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.
This commit is contained in:
Teknium 2026-07-23 17:10:32 -07:00
parent c4d1913294
commit 53bfe40a35
2 changed files with 90 additions and 0 deletions

View file

@ -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

View file

@ -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