From 91d60d2f9ea004eb937727d4d83fd264558366b2 Mon Sep 17 00:00:00 2001 From: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:18:27 +0700 Subject: [PATCH] fix(fallback): re-resolve extra_body when activating fallback provider (#75091) `try_activate_fallback()` re-resolved `reasoning_config` for the new fallback provider (fix for #21256), but never re-resolved `extra_body`. The primary provider's `extra_body` (e.g. `reasoning_effort: "none"`) rode along onto the fallback provider, which is a different API that may reject those fields. Example: primary has `extra_body: {reasoning_effort: "none"}`, fallback is OpenRouter. After failover, every request to OpenRouter carries both the stray top-level `reasoning_effort` AND the nested `reasoning` object, and OpenRouter rejects the pair: HTTP 400: "reasoning_effort" and "reasoning.effort" are both provided The fallback is dead precisely when it is needed. Fix: after swapping provider/model/base_url, clear the primary's extra_body from request_overrides, then re-resolve from the fallback provider's config using the existing _merge_custom_provider_extra_body helper. Same pattern as the reasoning_config re-resolution above. --- agent/chat_completion_helpers.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 033699ebbe..2880bf17a5 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -2842,6 +2842,31 @@ def try_activate_fallback(agent, reason: "FailoverReason | None" = None) -> bool ) # Keep whatever reasoning_config was active — don't break the fallback swap. + # Re-resolve extra_body for the fallback provider (Closes #75091). + # The primary's provider-specific extra_body (e.g. reasoning_effort) + # must not ride along onto the fallback provider, which is a different + # API that may reject those fields. Clear the stale extra_body first, + # then re-resolve from the fallback provider's config. + try: + from agent.agent_init import _merge_custom_provider_extra_body + _custom_providers = getattr(agent, "_custom_providers", None) or [] + # Strip the primary's extra_body so it doesn't contaminate the + # fallback (existing_extra_body in _merge wins on conflict). + _overrides = dict(getattr(agent, "request_overrides", {}) or {}) + _overrides.pop("extra_body", None) + agent.request_overrides = _overrides + _merge_custom_provider_extra_body(agent, _custom_providers) + logger.info( + "Fallback %s: extra_body resolved: %s", + agent.model, + (getattr(agent, "request_overrides", {}) or {}).get("extra_body"), + ) + except Exception as _eb_err: + logger.debug( + "Failed to resolve extra_body for fallback %s; keeping current: %s", + agent.model, _eb_err, + ) + # Keep the prompt's self-identity in sync with the model actually # answering, so "what model are you?" doesn't report the primary. rewrite_prompt_model_identity(agent, fb_model, fb_provider)