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:
Nate George
2026-06-03 16:42:52 -07:00
committed by Teknium
parent ec69c767ff
commit e8c3ac2f5c
6 changed files with 173 additions and 9 deletions
@@ -46,6 +46,44 @@ class TestChatCompletionsBasic:
assert "codex_reasoning_items" in msgs[0]
assert "codex_message_items" in msgs[0]
def _msg_with_extra_content(self):
return [
{"role": "assistant", "content": "ok",
"tool_calls": [{"id": "call_1", "type": "function",
"extra_content": {"google": {"thought_signature": "SIG_123"}},
"function": {"name": "t", "arguments": "{}"}}]},
]
def test_convert_messages_strips_extra_content_for_strict_provider(self, transport):
"""Strict providers (Fireworks, Mistral) reject extra_content on
tool_calls with HTTP 400. When the outgoing model is NOT Gemini-family,
the Gemini thought_signature must be stripped — including stale
signatures inherited from earlier in a mixed-provider session.
"""
msgs = self._msg_with_extra_content()
result = transport.convert_messages(msgs, model="accounts/fireworks/models/llama-v3p1-70b")
assert "extra_content" not in result[0]["tool_calls"][0]
# Original list untouched (deepcopy-on-demand)
assert "extra_content" in msgs[0]["tool_calls"][0]
def test_convert_messages_strips_extra_content_when_model_unknown(self, transport):
"""Default (no model supplied) is to strip — safe for strict providers."""
msgs = self._msg_with_extra_content()
result = transport.convert_messages(msgs)
assert "extra_content" not in result[0]["tool_calls"][0]
def test_convert_messages_keeps_extra_content_for_gemini(self, transport):
"""Gemini 3 thinking models require the thought_signature replayed on
every turn — stripping it would 400. Keep extra_content for Gemini
targets (including aggregator slugs like google/gemini-3-pro).
"""
for model in ("gemini-3-pro", "google/gemini-3-pro-preview", "gemma-3-27b"):
msgs = self._msg_with_extra_content()
result = transport.convert_messages(msgs, model=model)
assert result[0]["tool_calls"][0]["extra_content"] == {
"google": {"thought_signature": "SIG_123"}
}, model
def test_convert_messages_strips_tool_name(self, transport):
"""Internal `tool_name` (used for FTS indexing in the SQLite store) is
not part of the OpenAI Chat Completions schema. Strict providers like