Skip to content
Closed
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
28 changes: 27 additions & 1 deletion agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,9 +957,18 @@ def parse_available_output_tokens_from_error(error_msg: str) -> Optional[int]:
error_lower = error_msg.lower()

# Must look like an output-cap error, not a prompt-length error.
# OpenRouter/Nous Research format (OpenAI-compatible):
# "5683 of text input, 13410 of tool input, 262000 in the output"
# Note: these errors do NOT contain the word "max_tokens" — they use
# "in the output" to describe the output cap.
is_output_cap_error = (
re.search(r'\d+\s+in\s+the\s+output', error_lower) is not None
) or (
"max_tokens" in error_lower
and ("available_tokens" in error_lower or "available tokens" in error_lower)
and (
"available_tokens" in error_lower
or "available tokens" in error_lower
)
)
if not is_output_cap_error:
return None
Expand All @@ -972,6 +981,23 @@ def parse_available_output_tokens_from_error(error_msg: str) -> Optional[int]:
# fallback: last number after "=" in expressions like "200000 - 190000 = 10000"
r'=\s*(\d+)\s*$',
]

# OpenRouter/Nous format: compute available = context_length - text_input - tool_input
# "maximum context length is 256000 ... 5683 of text input ... 13410 of tool input"
or_match = re.search(
r'maximum\s+context\s+length\s+is\s+(\d+).*?'
r'(\d+)\s+of\s+text\s+input.*?'
r'(\d+)\s+of\s+tool\s+input',
error_lower
)
if or_match:
max_ctx = int(or_match.group(1))
text_input = int(or_match.group(2))
tool_input = int(or_match.group(3))
available = max_ctx - text_input - tool_input
if available >= 1:
return available

for pattern in patterns:
match = re.search(pattern, error_lower)
if match:
Expand Down
58 changes: 58 additions & 0 deletions tests/agent/test_model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,64 @@ def test_number_outside_reasonable_range(self):
assert parse_context_limit_from_error(msg) is None


class TestParseAvailableOutputTokensFromError:
"""Tests for parse_available_output_tokens_from_error() — detects "output cap
too large" errors and returns how many output tokens would fit."""

# ── Anthropic format ──────────────────────────────────────────────

def test_anthropic_available_tokens(self):
msg = (
"max_tokens: 32768 > context_window: 200000 - input_tokens: 190000"
" = available_tokens: 10000"
)
assert parse_available_output_tokens_from_error(msg) == 10000

def test_anthropic_available_tokens_with_space(self):
msg = "max_tokens: 50000 exceeds available tokens: 30000"
assert parse_available_output_tokens_from_error(msg) == 30000

# ── OpenRouter / Nous Research format ─────────────────────────────

def test_openrouter_format_returns_calculated_available(self):
"""Real Nous Research error — compute available = context - text - tool."""
msg = (
"This endpoint's maximum context length is 256000 tokens."
" However, you requested about 281093 tokens"
" (5683 of text input, 13410 of tool input, 262000 in the output)."
" Please reduce the length of either one, or use the"
" context-compression plugin to compress your prompt automatically."
)
# available = 256000 - 5683 - 13410 = 236907
assert parse_available_output_tokens_from_error(msg) == 236907

def test_openrouter_large_tool_input(self):
"""Scenario with 50K tool schemas."""
msg = (
"maximum context length is 131072 tokens."
" However, you requested about 200000 tokens"
" (10000 of text input, 50000 of tool input, 140000 in the output)."
)
# available = 131072 - 10000 - 50000 = 71072
assert parse_available_output_tokens_from_error(msg) == 71072

# ── Negative cases ────────────────────────────────────────────────

def test_plain_context_overflow_not_output_cap(self):
"""A prompt-too-long error without max_tokens mention."""
msg = (
"This model's maximum context length is 32768 tokens."
" However, your messages resulted in 45000 tokens."
)
assert parse_available_output_tokens_from_error(msg) is None

def test_completely_unrelated_error(self):
assert parse_available_output_tokens_from_error("Invalid API key") is None

def test_empty_string(self):
assert parse_available_output_tokens_from_error("") is None


# =========================================================================
# Persistent context length cache
# =========================================================================
Expand Down