Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions agent/error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ def is_auth(self) -> bool:
"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
Expand Down Expand Up @@ -212,6 +220,12 @@ def is_auth(self) -> bool:
"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
Expand Down Expand Up @@ -298,6 +312,10 @@ def is_auth(self) -> bool:
"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
Expand Down
72 changes: 72 additions & 0 deletions tests/agent/test_error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -2170,3 +2170,75 @@ def test_stale_breaker_runtime_error_triggers_fallback_not_retry(self):
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


Loading