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', ''))
- <thinking> 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).
This commit is contained in:
ed
2026-06-11 08:48:07 -04:00
parent fadb4c329b
commit c9ed734d9d
+51 -28
View File
@@ -2233,40 +2233,63 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str,
_ensure_minimax_client() _ensure_minimax_client()
from src.openai_compatible import OpenAICompatibleRequest, send_openai_compatible from src.openai_compatible import OpenAICompatibleRequest, send_openai_compatible
from src.vendor_capabilities import get_capabilities from src.vendor_capabilities import get_capabilities
tools: list[dict[str, Any]] | None = _get_deepseek_tools() or None
with _minimax_history_lock: with _minimax_history_lock:
_repair_minimax_history(_minimax_history) _repair_minimax_history(_minimax_history)
if discussion_history and not _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}"}) _minimax_history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"})
else: else:
_minimax_history.append({"role": "user", "content": user_message}) _minimax_history.append({"role": "user", "content": user_message})
messages = [{"role": "system", "content": f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>"}] response_text: str = ""
messages.extend(_minimax_history) reasoning_content: str = ""
request = OpenAICompatibleRequest( for round_idx in range(MAX_TOOL_ROUNDS + 2):
messages=messages, with _minimax_history_lock:
model=_model, messages = [{"role": "system", "content": f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>"}]
temperature=_temperature, messages.extend(_minimax_history)
top_p=_top_p, request = OpenAICompatibleRequest(
max_tokens=min(_max_tokens, 8192), messages=messages,
stream=stream, model=_model,
stream_callback=stream_callback, temperature=_temperature,
) top_p=_top_p,
caps = get_capabilities("minimax", _model) max_tokens=min(_max_tokens, 8192),
response = send_openai_compatible(_minimax_client, request, capabilities=caps) stream=stream,
reasoning_content = "" stream_callback=stream_callback,
if response.raw_response and hasattr(response.raw_response, "choices"): tools=tools,
choice = response.raw_response.choices[0] tool_choice="auto" if tools else "auto",
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 "" caps = get_capabilities("minimax", _model)
thinking_tags = "" response = send_openai_compatible(_minimax_client, request, capabilities=caps)
if reasoning_content: reasoning_content = ""
thinking_tags = f"<thinking>\n{reasoning_content}\n</thinking>\n" if response.raw_response and hasattr(response.raw_response, "choices"):
full_text = thinking_tags + response.text choice = response.raw_response.choices[0]
with _minimax_history_lock: if hasattr(choice.message, "reasoning_details") and choice.message.reasoning_details:
msg_to_store: dict[str, Any] = {"role": "assistant", "content": response.text or None} reasoning_content = choice.message.reasoning_details[0].get("text", "") if choice.message.reasoning_details else ""
if reasoning_content: with _minimax_history_lock:
msg_to_store["reasoning_content"] = reasoning_content msg_to_store: dict[str, Any] = {"role": "assistant", "content": response.text or None}
_minimax_history.append(msg_to_store) if reasoning_content:
return full_text 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"<thinking>\n{reasoning_content}\n</thinking>\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 #endregion: MiniMax Provider