diff --git a/run_agent.py b/run_agent.py index cce83f6b6..21ffbd9f3 100644 --- a/run_agent.py +++ b/run_agent.py @@ -2530,6 +2530,31 @@ class AIAgent: return msg + @staticmethod + def _sanitize_tool_calls_for_strict_api(api_msg: dict) -> dict: + """Strip Codex Responses API fields from tool_calls for strict providers. + + Providers like Mistral strictly validate the Chat Completions schema + and reject unknown fields (call_id, response_item_id) with 422. + These fields are preserved in the internal message history — this + method only modifies the outgoing API copy. + + 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). + """ + tool_calls = api_msg.get("tool_calls") + if not isinstance(tool_calls, list): + return api_msg + _STRIP_KEYS = {"call_id", "response_item_id"} + api_msg["tool_calls"] = [ + {k: v for k, v in tc.items() if k not in _STRIP_KEYS} + if isinstance(tc, dict) else tc + for tc in tool_calls + ] + return api_msg + def flush_memories(self, messages: list = None, min_turns: int = None): """Give the model one turn to persist memories before context is lost. @@ -2567,6 +2592,7 @@ class AIAgent: try: # Build API messages for the flush call + _is_strict_api = "api.mistral.ai" in self.base_url.lower() api_messages = [] for msg in messages: api_msg = msg.copy() @@ -2577,6 +2603,8 @@ class AIAgent: api_msg.pop("reasoning", None) api_msg.pop("finish_reason", None) api_msg.pop("_flush_sentinel", None) + if _is_strict_api: + self._sanitize_tool_calls_for_strict_api(api_msg) api_messages.append(api_msg) if self._cached_system_prompt: @@ -3042,11 +3070,14 @@ class AIAgent: try: # Build API messages, stripping internal-only fields # (finish_reason, reasoning) that strict APIs like Mistral reject with 422 + _is_strict_api = "api.mistral.ai" in self.base_url.lower() api_messages = [] for msg in messages: api_msg = msg.copy() for internal_field in ("reasoning", "finish_reason"): api_msg.pop(internal_field, None) + if _is_strict_api: + self._sanitize_tool_calls_for_strict_api(api_msg) api_messages.append(api_msg) effective_system = self._cached_system_prompt or "" @@ -3425,6 +3456,12 @@ class AIAgent: # Remove finish_reason - not accepted by strict APIs (e.g. Mistral) if "finish_reason" in api_msg: api_msg.pop("finish_reason") + # Strip Codex Responses API fields (call_id, response_item_id) for + # strict providers like Mistral that reject unknown fields with 422. + # Uses new dicts so the internal messages list retains the fields + # for Codex Responses compatibility. + if "api.mistral.ai" in self.base_url.lower(): + self._sanitize_tool_calls_for_strict_api(api_msg) # Keep 'reasoning_details' - OpenRouter uses this for multi-turn reasoning context # The signature field helps maintain reasoning continuity api_messages.append(api_msg)