From c9ed734d9d666fd0f5150ea4d750a7803514739e Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 11 Jun 2026 08:48:07 -0400 Subject: [PATCH] refactor(minimax): restore tool-call loop in _send_minimax The previous refactor (commit 344a66fc) dropped the tool-call loop in _send_minimax. The original function executed tool calls when the response had tool_calls; the refactor was single-shot. This is a real behavior regression (tools stop working) even though the existing tests don't catch it. Restore the tool loop: - For each round (up to MAX_TOOL_ROUNDS + 2), call send_openai_compatible with tools=_get_deepseek_tools() and tool_choice='auto' - If response has tool_calls: dispatch each via _execute_tool_calls_concurrently (handles both async context and sync via run_coroutine_threadsafe / asyncio.run), append each result to _minimax_history with role='tool' and tool_call_id - If no tool_calls: return the response text (with thinking tags for reasoning models) - The lock is acquired/released per iteration to avoid holding it during the API call (which can take seconds) Preserved: - 10-arg signature - _minimax_history_lock (now acquired per iteration) - _repair_minimax_history - discussion_history handling - System + context message wrapping - Reasoning content extraction (response.raw_response.choices[0].message .reasoning_details[0].get('text', '')) - tags wrap on the final response Dropped (still): - extra_body={reasoning_split: True} (not supported by send_openai_compatible; would be a Phase 5 adapter addition if minimax-reasoner models need it) New line count: 75 lines (vs 41 single-shot, vs 231 pre-refactor). Net effect: 231 -> 75 = 68% reduction; tool loop preserved. Verification: 38/38 tests pass (no regressions). --- src/ai_client.py | 79 +++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/ai_client.py b/src/ai_client.py index 47fcec94..00fe1da3 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -2233,40 +2233,63 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str, _ensure_minimax_client() from src.openai_compatible import OpenAICompatibleRequest, send_openai_compatible from src.vendor_capabilities import get_capabilities + tools: list[dict[str, Any]] | None = _get_deepseek_tools() or None with _minimax_history_lock: _repair_minimax_history(_minimax_history) if discussion_history and not _minimax_history: _minimax_history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"}) else: _minimax_history.append({"role": "user", "content": user_message}) - messages = [{"role": "system", "content": f"{_get_combined_system_prompt()}\n\n\n{md_content}\n"}] - messages.extend(_minimax_history) - request = OpenAICompatibleRequest( - messages=messages, - model=_model, - temperature=_temperature, - top_p=_top_p, - max_tokens=min(_max_tokens, 8192), - stream=stream, - stream_callback=stream_callback, - ) - caps = get_capabilities("minimax", _model) - response = send_openai_compatible(_minimax_client, request, capabilities=caps) - reasoning_content = "" - if response.raw_response and hasattr(response.raw_response, "choices"): - choice = response.raw_response.choices[0] - if hasattr(choice.message, "reasoning_details") and choice.message.reasoning_details: - reasoning_content = choice.message.reasoning_details[0].get("text", "") if choice.message.reasoning_details else "" - thinking_tags = "" - if reasoning_content: - thinking_tags = f"\n{reasoning_content}\n\n" - full_text = thinking_tags + response.text - with _minimax_history_lock: - msg_to_store: dict[str, Any] = {"role": "assistant", "content": response.text or None} - if reasoning_content: - msg_to_store["reasoning_content"] = reasoning_content - _minimax_history.append(msg_to_store) - return full_text + response_text: str = "" + reasoning_content: str = "" + for round_idx in range(MAX_TOOL_ROUNDS + 2): + with _minimax_history_lock: + messages = [{"role": "system", "content": f"{_get_combined_system_prompt()}\n\n\n{md_content}\n"}] + messages.extend(_minimax_history) + request = OpenAICompatibleRequest( + messages=messages, + model=_model, + temperature=_temperature, + top_p=_top_p, + max_tokens=min(_max_tokens, 8192), + stream=stream, + stream_callback=stream_callback, + tools=tools, + tool_choice="auto" if tools else "auto", + ) + caps = get_capabilities("minimax", _model) + response = send_openai_compatible(_minimax_client, request, capabilities=caps) + reasoning_content = "" + if response.raw_response and hasattr(response.raw_response, "choices"): + choice = response.raw_response.choices[0] + if hasattr(choice.message, "reasoning_details") and choice.message.reasoning_details: + reasoning_content = choice.message.reasoning_details[0].get("text", "") if choice.message.reasoning_details else "" + with _minimax_history_lock: + msg_to_store: dict[str, Any] = {"role": "assistant", "content": response.text or None} + if reasoning_content: + msg_to_store["reasoning_content"] = reasoning_content + if response.tool_calls: + msg_to_store["tool_calls"] = response.tool_calls + _minimax_history.append(msg_to_store) + if not response.tool_calls: + response_text = (f"\n{reasoning_content}\n\n" if reasoning_content else "") + response.text + break + try: + loop = asyncio.get_running_loop() + results = asyncio.run_coroutine_threadsafe( + _execute_tool_calls_concurrently(response.tool_calls, base_dir, pre_tool_callback, qa_callback, round_idx, "minimax", patch_callback), + loop, + ).result() + except RuntimeError: + results = asyncio.run(_execute_tool_calls_concurrently(response.tool_calls, base_dir, pre_tool_callback, qa_callback, round_idx, "minimax", patch_callback)) + with _minimax_history_lock: + for _i, (name, call_id, out, _) in enumerate(results): + _minimax_history.append({ + "role": "tool", + "tool_call_id": call_id, + "content": str(out) if out else "", + }) + return response_text #endregion: MiniMax Provider