Skip to content

Commit 178b33c

Browse files
kevincoltenclaude
andcommitted
fix(error_classifier): keep token-rate throttles out of the context-length rule, re-add token-qualified "exceeds the maximum"
Follow-up to cubic's review of 8b0084a: - "too many tokens per minute" style throttles matched the new "too many tokens" context keyword. The rate-limit rule (which runs first) now also matches "tokens per minute", "tokens per min" and "(tpm)", so token-rate limits stay retryable and never trigger chunking. - The bare "exceeds the maximum" wording from the old token_utils list is back as a compiled pattern that requires "token(s)" within 40 chars, so "9000 tokens exceeds the maximum of 8192" chunks while "file exceeds the maximum upload size" does not. Rules may now hold regex patterns next to substrings; _keyword_matches searches them. - Bedrock's "Input is too long for requested model" is recognised. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011V8PghiyLvfhK5meqkU4QQ
1 parent 8b0084a commit 178b33c

2 files changed

Lines changed: 42 additions & 6 deletions

File tree

open_notebook/utils/error_classifier.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,30 @@
1919
RateLimitError,
2020
)
2121

22-
# Classification rules: (keywords, exception_class, user_message or None to pass through)
23-
_CLASSIFICATION_RULES: list[tuple[list[str], type[OpenNotebookError], str | None]] = [
22+
# Classification rules: (keywords, exception_class, user_message or None to pass through).
23+
# A keyword is a lowercase substring, an HTTP status code (matched as a standalone
24+
# number) or a compiled regex for wordings a substring can't pin down.
25+
_Keyword = str | re.Pattern[str]
26+
_CLASSIFICATION_RULES: list[tuple[list[_Keyword], type[OpenNotebookError], str | None]] = [
2427
# Authentication errors
2528
(
2629
["authentication", "unauthorized", "invalid api key", "invalid_api_key", "401"],
2730
AuthenticationError,
2831
"Authentication failed. Please check your API key in Settings -> Credentials.",
2932
),
30-
# Rate limit errors
33+
# Rate limit errors. Token-rate throttles ("tokens per minute") must be
34+
# caught here, before the context-length rule below sees "tokens".
3135
(
32-
["rate limit", "rate_limit", "429", "too many requests", "quota exceeded"],
36+
[
37+
"rate limit",
38+
"rate_limit",
39+
"429",
40+
"too many requests",
41+
"quota exceeded",
42+
"tokens per minute",
43+
"tokens per min",
44+
"(tpm)",
45+
],
3346
RateLimitError,
3447
"Rate limit exceeded. Please wait a moment and try again.",
3548
),
@@ -68,8 +81,12 @@
6881
"max_tokens",
6982
"too many tokens",
7083
"input too long",
84+
"input is too long", # Bedrock: "Input is too long for requested model"
7185
"prompt is too long",
7286
"input token count",
87+
# "exceeds the maximum" only when tokens are what's being counted —
88+
# the bare phrase also describes upload sizes and request counts.
89+
re.compile(r"tokens?\b.{0,40}\bexceeds? the maximum|exceeds? the maximum\b.{0,40}\btokens?"),
7390
],
7491
ContextLengthExceededError,
7592
"Content too large for the selected model. Try using a smaller selection or a model with a larger context window.",
@@ -116,15 +133,17 @@ def classify_error(exception: BaseException) -> tuple[type[OpenNotebookError], s
116133
return ExternalServiceError, f"AI service error: {_truncate(str(exception))}"
117134

118135

119-
def _keyword_matches(keyword: str, text: str) -> bool:
136+
def _keyword_matches(keyword: _Keyword, text: str) -> bool:
120137
"""Substring match, except HTTP status codes ("401", "429", "500", ...)
121-
must appear as a standalone number.
138+
must appear as a standalone number, and compiled patterns are searched.
122139
123140
Provider messages carry token counts ("142900 tokens > 200000 maximum"),
124141
and a plain substring check would read "429" out of that count and
125142
classify a context-length rejection as a rate limit — which then gets
126143
retried by the worker instead of triggering chunking (cf. #1303 for the
127144
same bug in the connection test)."""
145+
if isinstance(keyword, re.Pattern):
146+
return keyword.search(text) is not None
128147
if keyword.isdigit():
129148
return re.search(rf"(?<!\d){keyword}(?!\d)", text) is not None
130149
return keyword in text

tests/test_context_length_no_retry.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ def test_still_an_external_service_error(self):
9090
"400 The input token count (250012) exceeds the maximum number of tokens allowed (131072).",
9191
# OpenAI
9292
"This model's maximum context length is 8192 tokens. However, your messages resulted in 10500 tokens.",
93+
# Bedrock
94+
"ValidationException: Input is too long for requested model.",
95+
# Generic "exceeds the maximum", qualified by tokens
96+
"Prompt of 9000 tokens exceeds the maximum of 8192.",
97+
"Request exceeds the maximum allowed tokens for this model.",
9398
],
9499
)
95100
def test_provider_wordings_are_context_length(self, message):
@@ -102,6 +107,10 @@ def test_provider_wordings_are_context_length(self, message):
102107
[
103108
("Rate limit exceeded. Please wait a moment.", RateLimitError),
104109
("Error code: 429 - too many requests", RateLimitError),
110+
# Token-rate throttles mention tokens but are transient, not a
111+
# context window: they must stay retryable and must not chunk.
112+
("Too many tokens per minute for this model, slow down.", RateLimitError),
113+
("Request too large for model on tokens per min (TPM): Limit 6000", RateLimitError),
105114
("Error code: 401 - invalid api key", AuthenticationError),
106115
("Error code: 503 - service unavailable", ExternalServiceError),
107116
],
@@ -111,6 +120,14 @@ def test_status_codes_still_match_as_standalone_numbers(self, message, expected)
111120

112121
assert exc_class is expected
113122

123+
def test_bare_exceeds_the_maximum_is_not_context_length(self):
124+
"""Upload/request-size wording shares the phrase but isn't a token window."""
125+
exc_class, _ = classify_error(
126+
Exception("File exceeds the maximum upload size of 100 MB")
127+
)
128+
129+
assert not issubclass(exc_class, ContextLengthExceededError)
130+
114131
def test_status_code_inside_a_larger_number_does_not_match(self):
115132
""""4290 items" must not read as HTTP 429; unknown wording falls through
116133
to the generic external error."""

0 commit comments

Comments
 (0)