fix: pass WebUI max_tokens to agents

Read configured max_tokens from config.yaml, pass it into WebUI-created AIAgent instances when supported, and include it in the agent cache signature. Also classify OpenRouter quota phrasing such as more credits, can only afford, and fewer max_tokens.

Adds regression coverage for max_tokens propagation, cache signature isolation, and quota error classification.
This commit is contained in:
Manfred 2026-05-03 11:46:42 +02:00
parent 9e31a2ac65
commit dbb0879956
2 changed files with 68 additions and 0 deletions

View file

@ -1792,6 +1792,25 @@ def _run_agent_streaming(
import inspect as _inspect
_agent_params = set(_inspect.signature(_AIAgent.__init__).parameters)
# CLI-parity max output cap: read config.yaml's max_tokens and pass
# it to AIAgent when supported. Without this WebUI-created agents use
# provider-native output ceilings (e.g. Claude via OpenRouter can
# request 64k), which may turn an otherwise usable fallback into a
# 402 "more credits / fewer max_tokens" failure.
_max_tokens_cfg = None
try:
_raw_max_tokens = _cfg.get('max_tokens')
if _raw_max_tokens is None:
_agent_cfg_for_tokens = _cfg.get('agent', {})
if isinstance(_agent_cfg_for_tokens, dict):
_raw_max_tokens = _agent_cfg_for_tokens.get('max_tokens')
if _raw_max_tokens is not None:
_parsed_max_tokens = int(_raw_max_tokens)
if _parsed_max_tokens > 0:
_max_tokens_cfg = _parsed_max_tokens
except Exception:
_max_tokens_cfg = None
# CLI-parity reasoning effort: read agent.reasoning_effort from the
# active profile's config.yaml (the same key the CLI writes via
# `/reasoning <level>`) and hand the parsed dict to AIAgent. When
@ -1830,6 +1849,8 @@ def _run_agent_streaming(
# but guard defensively to avoid TypeError on an older agent build.
if 'reasoning_config' in _agent_params and _reasoning_config is not None:
_agent_kwargs['reasoning_config'] = _reasoning_config
if 'max_tokens' in _agent_params and _max_tokens_cfg is not None:
_agent_kwargs['max_tokens'] = _max_tokens_cfg
# Params added in newer hermes-agent — skip if not supported
if 'api_mode' in _agent_params:
_agent_kwargs['api_mode'] = _rt.get('api_mode')
@ -1861,6 +1882,8 @@ def _run_agent_streaming(
_hashlib.sha256((resolved_api_key or '').encode()).hexdigest()[:16],
resolved_base_url or '',
resolved_provider or '',
_max_tokens_cfg or '',
_fallback_resolved or {},
sorted(_toolsets) if _toolsets else [],
], sort_keys=True)
_agent_sig = _hashlib.sha256(_sig_blob.encode()).hexdigest()[:16]
@ -2098,6 +2121,9 @@ def _run_agent_streaming(
'insufficient credit' in _err_lower
or 'credit balance' in _err_lower
or 'credits exhausted' in _err_lower
or 'more credits' in _err_lower
or 'can only afford' in _err_lower
or 'fewer max_tokens' in _err_lower
or 'quota_exceeded' in _err_lower
or 'quota exceeded' in _err_lower
or 'exceeded your current quota' in _err_lower
@ -2433,6 +2459,9 @@ def _run_agent_streaming(
'insufficient credit' in _exc_lower
or 'credit balance' in _exc_lower
or 'credits exhausted' in _exc_lower
or 'more credits' in _exc_lower
or 'can only afford' in _exc_lower
or 'fewer max_tokens' in _exc_lower
or 'quota_exceeded' in _exc_lower
or 'quota exceeded' in _exc_lower
or 'exceeded your current quota' in _exc_lower

View file

@ -0,0 +1,39 @@
"""Regression coverage for WebUI streaming provider failure handling.
The incident this guards against: WebUI-created AIAgent instances did not pass
config.yaml's max_tokens, so a fallback Claude model via OpenRouter requested its
native 64k output ceiling and failed with HTTP 402 "more credits / fewer
max_tokens". The stream then looked like a stuck Thinking card instead of a
clear quota error.
"""
from pathlib import Path
STREAMING = Path(__file__).resolve().parents[1] / "api" / "streaming.py"
def _src() -> str:
return STREAMING.read_text(encoding="utf-8")
def test_streaming_passes_configured_max_tokens_to_agent():
src = _src()
assert "_raw_max_tokens = _cfg.get('max_tokens')" in src
assert "_agent_cfg_for_tokens.get('max_tokens')" in src
assert "_agent_kwargs['max_tokens'] = _max_tokens_cfg" in src
def test_streaming_agent_cache_signature_includes_max_tokens_and_fallback():
src = _src()
assert "_max_tokens_cfg or ''" in src
assert "_fallback_resolved or {}" in src
def test_openrouter_more_credits_error_is_classified_as_quota():
src = _src()
assert "'more credits' in _err_lower" in src
assert "'can only afford' in _err_lower" in src
assert "'fewer max_tokens' in _err_lower" in src
assert "'more credits' in _exc_lower" in src
assert "'can only afford' in _exc_lower" in src
assert "'fewer max_tokens' in _exc_lower" in src