From f65d105cbb533a241f152165e4ea6b85ed8d3264 Mon Sep 17 00:00:00 2001 From: AlexFucuson9 Date: Fri, 17 Jul 2026 16:53:08 +0700 Subject: [PATCH] fix(agent): preserve Gemini thought_signature in MoA aggregator mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When MoA mode is active with a Gemini model as the aggregator, agent.model holds the virtual preset name (e.g. "closed"), not the actual aggregator model name. The _sanitize_tool_calls_for_strict_api call uses agent.model to decide whether to keep extra_content (thought_signature) on tool_calls — since "closed" doesn't contain "gemini", the thought_signature is stripped and the Gemini aggregator rejects the next request with HTTP 400 (INVALID_ARGUMENT): "Function call is missing a thought_signature in functionCall parts." Fix: resolve the actual aggregator model name from moa_config (conversation_loop) or last_aggregator_slot (chat_completion_helpers) and pass it to _sanitize_tool_calls_for_strict_api so the _model_consumes_thought_signature check sees the real Gemini model. Closes #65092 --- agent/chat_completion_helpers.py | 12 +++++++++++- agent/conversation_loop.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 8dc764c07532..34098cdc5e21 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -1946,7 +1946,17 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str: for internal_key in [k for k in api_msg if isinstance(k, str) and k.startswith("_")]: api_msg.pop(internal_key, None) if _needs_sanitize: - agent._sanitize_tool_calls_for_strict_api(api_msg, model=agent.model) + # In MoA mode, agent.model is the virtual preset name, + # not the actual aggregator model. Resolve the real + # aggregator model so Gemini preserves thought_signature. + _sanitize_model = agent.model + if agent.provider == "moa": + _moa_client = getattr(agent, "client", None) + if _moa_client is not None: + _agg_slot = getattr(_moa_client, "last_aggregator_slot", None) + if _agg_slot and _agg_slot.get("model"): + _sanitize_model = _agg_slot["model"] + agent._sanitize_tool_calls_for_strict_api(api_msg, model=_sanitize_model) api_messages.append(api_msg) effective_system = agent._cached_system_prompt or "" diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 62b675aa33b4..0a7c36ddbd0a 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -1103,7 +1103,16 @@ def run_conversation( # Uses new dicts so the internal messages list retains the fields # for Codex Responses compatibility. if agent._should_sanitize_tool_calls(): - agent._sanitize_tool_calls_for_strict_api(api_msg, model=agent.model) + # In MoA mode, agent.model is the virtual preset name + # (e.g. "closed"), not the actual aggregator model. Use + # the resolved aggregator model so Gemini aggregators + # correctly preserve thought_signature (extra_content). + _sanitize_model = agent.model + if agent.provider == "moa" and moa_config: + _agg = moa_config.get("aggregator") or {} + if _agg.get("model"): + _sanitize_model = _agg["model"] + agent._sanitize_tool_calls_for_strict_api(api_msg, model=_sanitize_model) # Keep 'reasoning_details' - OpenRouter uses this for multi-turn reasoning context # The signature field helps maintain reasoning continuity api_messages.append(api_msg)