Skip to content

fix: don't route invalid_request_error 400s to fallback, skip persist regardless of size - #2175

Closed
XVVH wants to merge 1 commit into
NousResearch:mainfrom
XVVH:fix/400-fallback-persist-webex-reconnect
Closed

fix: don't route invalid_request_error 400s to fallback, skip persist regardless of size#2175
XVVH wants to merge 1 commit into
NousResearch:mainfrom
XVVH:fix/400-fallback-persist-webex-reconnect

Conversation

@XVVH

@XVVH XVVH commented Mar 20, 2026

Copy link
Copy Markdown

Context

This PR addresses two related issues discovered while investigating the invalid_request_error 400 failures that have been reported with native Anthropic sessions (#2172 fixes the root cause — orphaned tool_result blocks). This PR provides defence-in-depth for that class of error and any other structural 400 that may arise.

Complements #2172 — both should land together.


Problem 1: invalid_request_error 400s silently route to fallback, masking the root cause

When Anthropic returns a 400 invalid_request_error (malformed message structure, orphaned tool_result blocks, unknown fields, etc.), the current code calls _try_activate_fallback() before aborting.

This produces two bad outcomes:

  1. The fallback provider (e.g. OpenRouter) forwards the same malformed request to Anthropic's backend and returns the same 400 — but now attributed to "Provider returned error / provider_name: Azure". The real cause is hidden behind a confusing cross-provider error message.

  2. The user (or maintainer) sees an Azure/OpenRouter error and has no indication the problem originated in the request structure, making it very difficult to diagnose.

invalid_request_error is a structural rejection — it will fail identically on every provider. There is no point routing it to a fallback.

Fix: Detect invalid_request_error by checking error.type in the response body (alongside the existing error.message check for generic 400s). Skip _try_activate_fallback() for this error type and abort immediately with the original error intact.


Problem 2: Mid-sized corrupt sessions persist and reproduce the failure on every restart

The existing #1630 guard skips session persistence when a 400 occurs and the session is large (>50k tokens or >80 messages). This prevents the corrupt session from growing on large conversations.

However, the reported session had ~46 messages and ~29k tokens — below both thresholds. Result: the failed user message was persisted. On the next gateway restart, the session reloaded and the first new message immediately triggered the same 400, creating an unrecoverable loop requiring manual session file deletion.

The size thresholds were a reasonable heuristic for context-overflow 400s, but invalid_request_error indicates a structural message problem that will reproduce at any session size.

Fix: Skip session persistence for any invalid_request_error 400 regardless of session size, in addition to the existing large-session guard. Error message updated to remove the "large" qualifier.


Changes

run_agent.py:

  • Extract _err_type from the error body alongside the existing _err_message
  • Define _is_invalid_request flag for 400 + type == "invalid_request_error"
  • Gate _try_activate_fallback() behind not _is_invalid_request
  • Add _is_invalid_request to the session-persistence skip condition

Test plan

  • Structural 400 (invalid_request_error) from primary provider: aborts immediately, does not activate fallback, does not persist session
  • Transient/ambiguous 400 (empty message, "Error"): existing behaviour preserved — fallback activated if available
  • Auth/rate-limit errors (401, 403, 429): existing behaviour preserved
  • Large session 400 (>50k tokens or >80 messages): existing skip-persist behaviour preserved
  • Well-formed requests: no behavioural change

Related

When Anthropic returns a 400 invalid_request_error (e.g. orphaned
tool_result blocks, unknown fields, malformed message structure), the
current code calls _try_activate_fallback() before aborting. This
produces two bad outcomes:

1. The fallback provider (e.g. OpenRouter) forwards the same malformed
   request to Anthropic's backend and returns the same 400, but now
   attributed to "Provider returned error / provider_name: Azure".
   The root cause is hidden and the error is misleading.

2. The failed user message is persisted to the session file if the
   session is below the size thresholds in NousResearch#1630 (50k tokens / 80
   messages). On the next gateway restart the session reloads and the
   first new message immediately triggers the same 400, creating an
   unrecoverable loop that requires manual session file deletion.

Fix:
- Detect invalid_request_error by checking error.type in the response
  body (alongside the existing error.message check for generic 400s).
- Skip _try_activate_fallback() for invalid_request_error: these are
  structural rejections that will fail identically on every provider.
- Skip session persistence for any invalid_request_error 400 regardless
  of session size, extending the existing NousResearch#1630 behaviour to cover
  mid-sized corrupt sessions (the reported case had ~46 messages /
  ~29k tokens, below both thresholds).

This complements PR NousResearch#2172 (strip orphaned tool_result blocks in
convert_messages_to_anthropic) which prevents the bad message from
being sent in the first place. This PR provides defence-in-depth:
even if a structural 400 occurs for any other reason, it fails fast
with a clear error and does not grow the session.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the thorough analysis. We're going a different direction here — instead of skipping fallback for invalid_request_error, we'd rather:

  1. Fix the root causes of structural 400s upstream (orphaned tool_results already fixed in fix(gateway): strip orphaned tool_results + let /reset bypass running agent #2180)
  2. Surface the original error message clearly before falling back
  3. Keep fallback behavior intact since users expect it

The persist-skip idea has merit but is better handled by preventing broken messages from entering the session in the first place. Appreciate the contribution!

@teknium1 teknium1 closed this Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants