fix(agent): preserve Gemini thought_signature in MoA aggregator mode

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
This commit is contained in:
AlexFucuson9 2026-07-17 16:53:08 +07:00 committed by Teknium
parent d0d116be2e
commit f65d105cbb
2 changed files with 21 additions and 2 deletions

View file

@ -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 ""

View file

@ -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)