fix: strip extra_content from tool_calls for strict APIs (Fireworks, Mistral)
Fireworks/Mistral reject HTTP 400 'Extra inputs are not permitted, field: messages[N].tool_calls[M].extra_content' on any session whose history contains prior Gemini tool calls. Gemini 3 thinking models attach extra_content (thought_signature) to tool_calls; it survived to the wire because the sanitize paths only stripped call_id/response_item_id. Strip extra_content from the outgoing wire copy in both sanitize paths (ChatCompletionsTransport.convert_messages + _sanitize_tool_calls_for_strict_api), but gate it on the target model: keep extra_content for Gemini-family targets (the thought_signature MUST be replayed or Gemini 400s), strip it for everyone else — including non-Gemini models that inherit a stale Gemini signature earlier in a mixed-provider session. Native Gemini is unaffected (GeminiNativeClient bypasses these paths). Original stored history is never mutated (only the per-call copy). Fixes #17986.
This commit is contained in:
+11
-2
@@ -4627,7 +4627,7 @@ class AIAgent:
|
||||
return reapply_reasoning_echo_for_provider(self, api_messages)
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_tool_calls_for_strict_api(api_msg: dict) -> dict:
|
||||
def _sanitize_tool_calls_for_strict_api(api_msg: dict, model: "str | None" = None) -> dict:
|
||||
"""Strip Codex Responses API fields from tool_calls for strict providers.
|
||||
|
||||
Providers like Mistral, Fireworks, and other strict OpenAI-compatible APIs
|
||||
@@ -4636,17 +4636,26 @@ class AIAgent:
|
||||
the internal message history — this method only modifies the outgoing
|
||||
API copy.
|
||||
|
||||
``extra_content`` (Gemini thought_signature) is also stripped — strict
|
||||
providers reject it with "Extra inputs are not permitted" — UNLESS the
|
||||
outgoing ``model`` is itself Gemini-family, in which case it must be
|
||||
replayed (Gemini 3 thinking models 400 without it). Defaults to
|
||||
stripping when no model is supplied.
|
||||
|
||||
Creates new tool_call dicts rather than mutating in-place, so the
|
||||
original messages list retains call_id/response_item_id for Codex
|
||||
Responses API compatibility (e.g. if the session falls back to a
|
||||
Codex provider later).
|
||||
|
||||
Fields stripped: call_id, response_item_id
|
||||
Fields stripped: call_id, response_item_id, extra_content (model-gated)
|
||||
"""
|
||||
tool_calls = api_msg.get("tool_calls")
|
||||
if not isinstance(tool_calls, list):
|
||||
return api_msg
|
||||
from agent.transports.chat_completions import _model_consumes_thought_signature
|
||||
_STRIP_KEYS = {"call_id", "response_item_id"}
|
||||
if not _model_consumes_thought_signature(model):
|
||||
_STRIP_KEYS = _STRIP_KEYS | {"extra_content"}
|
||||
api_msg["tool_calls"] = [
|
||||
{k: v for k, v in tc.items() if k not in _STRIP_KEYS}
|
||||
if isinstance(tc, dict) else tc
|
||||
|
||||
Reference in New Issue
Block a user