fix(agent): malformed tool-call-argument 400s no longer walk the fallback chain
When a proxy (Ollama, OpenRouter) rejects the MODEL's own unparseable tool-call JSON with `400 invalid tool call arguments`, the classifier returned the generic format_error verdict (`should_fallback=True`) and the non-retryable client-error path cascaded through every fallback provider: 4-5 sequential calls, 20-60s per occurrence, ending on a model that produced the same broken JSON (#12770). - error_classifier: explicit `_MALFORMED_TOOL_ARGS_PATTERNS` checked before the request-validation and overflow heuristics, returning format_error with `retryable=False, should_fallback=False`. - turn_api_error: the client-error settlement honours `should_fallback`; the verdicts that legitimately reach that branch (policy block, TLS chain, MoA shape/preset errors) now state `should_fallback=True` explicitly, so the gate changes behaviour only for the new verdict. Local validation errors keep their historical fallback. Fixes #12770. Pattern list and gating approach from #16022 by @cuyua9 (stale base); tests trimmed to two invariants. Co-authored-by: cuyua9 <2114364329@qq.com>
This commit is contained in:
@@ -272,8 +272,9 @@ def settle_unrecovered_error(
|
||||
# ``FailoverReason.billing`` (402) is deliberately NOT excluded: pool rotation and
|
||||
# eager fallback already gave up, so retrying only burns paid requests on a depleted
|
||||
# balance. Mirrors 401/403.
|
||||
is_local_validation_error = _is_local_validation_error(api_error)
|
||||
is_client_error = (
|
||||
_is_local_validation_error(api_error)
|
||||
is_local_validation_error
|
||||
or (
|
||||
not classified.retryable
|
||||
and not classified.should_compress
|
||||
@@ -304,12 +305,16 @@ def settle_unrecovered_error(
|
||||
)
|
||||
retry_count = 0
|
||||
return _verdict("continue")
|
||||
# ``should_fallback=False`` marks a deterministic failure no other provider can fix (the
|
||||
# model's own malformed tool-call JSON, #12770): skip the cascade. Local validation errors
|
||||
# carry no classifier verdict and keep their historical fallback.
|
||||
fallback_allowed = classified.should_fallback or is_local_validation_error
|
||||
# Announce the fallback only when a chain exists, else "trying fallback..." lies
|
||||
# before a silent abort.
|
||||
if agent._has_pending_fallback():
|
||||
if fallback_allowed and agent._has_pending_fallback():
|
||||
_label = _NONRETRYABLE_LABELS.get(classified.reason, f"Non-retryable error (HTTP {status_code})")
|
||||
agent._buffer_status(f"⚠️ {_label} — trying fallback...")
|
||||
if agent._try_activate_fallback():
|
||||
if fallback_allowed and agent._try_activate_fallback():
|
||||
# Direct ``return _verdict("break")`` is load-bearing: the restart handler
|
||||
# re-runs the pre-API preflight against the fallback's context window.
|
||||
active_system_prompt = _arm_fallback_restart(agent, api_messages, active_system_prompt, _retry)
|
||||
|
||||
Reference in New Issue
Block a user