fix(agent): classify status-less streaming render errors as format_error

LM Studio / llama.cpp raise a bare status-less APIError when chat-template
Jinja rendering fails mid-stream; classify as format_error with fallback
instead of burning same-provider retries. Stage fires only when status_code
is None. Hand-applied from #64834 (supersedes #62673) onto the staged
classifier pipeline.

Fixes #62662
This commit is contained in:
AlexFucuson9
2026-09-24 16:32:35 +05:30
committed by kshitij
parent 0a661b7c94
commit d0dfea836b
2 changed files with 30 additions and 2 deletions

View File

@@ -944,11 +944,23 @@ def _by_status(c: _Ctx) -> Optional[Verdict]:
return _STATUS_HANDLERS[status](c) if status in _STATUS_HANDLERS else default
# LM Studio / llama.cpp raise a bare status-less ``APIError`` when chat-template Jinja rendering
# fails mid-stream (#62662). Deterministic for the request, so fall back instead of retrying.
_STREAM_RENDER_ERROR_PATTERNS = ("error rendering", "rendering prompt", "jinja template", "jinja render")
def _streaming_render_error(c: _Ctx) -> Optional[Verdict]:
"""Status-less template render failure → format_error with fallback (only when no HTTP status)."""
if c.status_code is None and any(p in c.msg for p in _STREAM_RENDER_ERROR_PATTERNS):
return _v(_R.format_error, retryable=False, should_fallback=True)
return None
# Stage order: plugin hooks → the provider's own profile hook → provider-specific special cases →
# HTTP status → MoA shapes → structured error code → message patterns → SSL → disconnect +
# status-less stream render errors → HTTP status → MoA shapes → structured error code → message patterns → SSL → disconnect +
# large session → transport types → unknown (retryable with backoff).
_STAGES: Sequence[Callable[[_Ctx], Optional[Verdict]]] = (
_plugin_verdict, _profile_verdict, _provider_special_cases, _by_status, _moa_special_cases,
_plugin_verdict, _profile_verdict, _provider_special_cases, _streaming_render_error, _by_status, _moa_special_cases,
_by_error_code, _by_message, _by_transport,
)

View File

@@ -2112,3 +2112,19 @@ class TestAuthErrorNamesOffRouteEndpoint:
for base_url in ("", "https://api.anthropic.com/v1"):
result = classify_api_error(e, provider="anthropic", model="claude", base_url=base_url)
assert result.message == "API keys are not supported by this endpoint.", base_url
class TestStreamingRenderFormatError:
"""Status-less Jinja render failures (LM Studio / llama.cpp) fail over; see #62662."""
def test_error_rendering_no_status_is_format_error(self):
e = MockAPIError("Error rendering prompt with jinja template: ...")
result = classify_api_error(e, provider="lm-studio", model="x")
assert result.reason == FailoverReason.format_error
assert result.retryable is False
assert result.should_fallback is True
def test_render_message_with_status_uses_http_path(self):
e = MockAPIError("Error rendering prompt with jinja template: ...", status_code=500)
result = classify_api_error(e, provider="lm-studio", model="x")
assert result.reason != FailoverReason.format_error