Private
Public Access
fix(ai_loop): wrap MiniMax reasoning in <thinking> tags for parse_thinking_trace (FR3, Bug #3)
Adds a new wrap_reasoning_in_text: bool = False keyword argument to
run_with_tool_loop. When True and reasoning_content is non-empty, the
returned text is prepended with <thinking>...</thinking> tags so
thinking_parser.parse_thinking_trace can extract a ThinkingSegment
for the discussion entry.
The wrap is conditional (default False) so it doesn't break providers
that already wrap inline (e.g. DeepSeek, which wraps at line 2117-2118
before run_with_tool_loop sees the response).
_send_minimax now passes wrap_reasoning_in_text=bool(caps.reasoning).
When caps.reasoning is True (M2.5/M2.7), the reasoning is wrapped in
<thinking> tags. When False (M2/M2.1), the parameter is False and
no wrap happens (avoids useless getattr on non-reasoning models).
Also fixes a bug in the test_fr3_minimax_thinking_in_returned_text
test mock: it was returning a raw MagicMock instead of a Result
object, which caused the test to see auto-created MagicMock attributes
instead of the expected text. Now wraps in Result(data=MagicMock(...))
and sets ai_client._model to ensure get_capabilities('minimax', _model)
resolves to the M2.7 capabilities (reasoning=True).
This commit is contained in:
@@ -748,6 +748,7 @@ def run_with_tool_loop(
|
|||||||
reasoning_extractor: Optional[Callable[[Any], str]] = None,
|
reasoning_extractor: Optional[Callable[[Any], str]] = None,
|
||||||
send_func: Optional[Callable[[int], NormalizedResponse]] = None,
|
send_func: Optional[Callable[[int], NormalizedResponse]] = None,
|
||||||
on_pre_dispatch: Optional[Callable[[int, list[dict[str, Any]]], list[dict[str, Any]]]] = None,
|
on_pre_dispatch: Optional[Callable[[int, list[dict[str, Any]]], list[dict[str, Any]]]] = None,
|
||||||
|
wrap_reasoning_in_text: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Orchestrates the LLM conversation loop, executing tool calls and updating history.
|
Orchestrates the LLM conversation loop, executing tool calls and updating history.
|
||||||
@@ -772,6 +773,11 @@ def run_with_tool_loop(
|
|||||||
reasoning_extractor (Optional[Callable]): Callback to extract reasoning content.
|
reasoning_extractor (Optional[Callable]): Callback to extract reasoning content.
|
||||||
send_func (Optional[Callable]): Dispatch sender callback.
|
send_func (Optional[Callable]): Dispatch sender callback.
|
||||||
on_pre_dispatch (Optional[Callable]): Callback to adjust tools.
|
on_pre_dispatch (Optional[Callable]): Callback to adjust tools.
|
||||||
|
wrap_reasoning_in_text (bool): When True and reasoning_content is non-empty, the
|
||||||
|
returned text is prepended with `<thinking>...</thinking>` wrapping the
|
||||||
|
reasoning. This lets thinking_parser.parse_thinking_trace extract a
|
||||||
|
ThinkingSegment for the discussion entry. Default False (callers that
|
||||||
|
already wrap inline, e.g. DeepSeek, pass False).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The final text response returned by the LLM.
|
str: The final text response returned by the LLM.
|
||||||
@@ -833,6 +839,8 @@ def run_with_tool_loop(
|
|||||||
"content": str(out) if out else "",
|
"content": str(out) if out else "",
|
||||||
})
|
})
|
||||||
if trim_func is not None: trim_func(history)
|
if trim_func is not None: trim_func(history)
|
||||||
|
if wrap_reasoning_in_text and reasoning_content:
|
||||||
|
response_text = f"<thinking>\n{reasoning_content}\n</thinking>\n\n{response_text}"
|
||||||
return response_text
|
return response_text
|
||||||
|
|
||||||
async def _execute_single_tool_call_async(
|
async def _execute_single_tool_call_async(
|
||||||
@@ -2438,6 +2446,7 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str,
|
|||||||
history_lock=_minimax_history_lock, history=_minimax_history,
|
history_lock=_minimax_history_lock, history=_minimax_history,
|
||||||
trim_func=lambda h: _trim_minimax_history(_build_minimax_request(0).messages, h),
|
trim_func=lambda h: _trim_minimax_history(_build_minimax_request(0).messages, h),
|
||||||
reasoning_extractor=_extract_minimax_reasoning if caps.reasoning else None,
|
reasoning_extractor=_extract_minimax_reasoning if caps.reasoning else None,
|
||||||
|
wrap_reasoning_in_text=bool(caps.reasoning),
|
||||||
))
|
))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return Result(data="", errors=[_classify_minimax_error(exc, source="ai_client.minimax")])
|
return Result(data="", errors=[_classify_minimax_error(exc, source="ai_client.minimax")])
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ def test_fr3_minimax_thinking_in_returned_text() -> None:
|
|||||||
|
|
||||||
def _fake_send_openai_compatible(client, request, *, capabilities):
|
def _fake_send_openai_compatible(client, request, *, capabilities):
|
||||||
captured_text.append("send_openai_compatible was called")
|
captured_text.append("send_openai_compatible was called")
|
||||||
return MagicMock(
|
return Result(data=MagicMock(
|
||||||
text="The final answer is 42",
|
text="The final answer is 42",
|
||||||
tool_calls=[],
|
tool_calls=[],
|
||||||
usage_input_tokens=0,
|
usage_input_tokens=0,
|
||||||
@@ -212,11 +212,12 @@ def test_fr3_minimax_thinking_in_returned_text() -> None:
|
|||||||
usage_cache_read_tokens=0,
|
usage_cache_read_tokens=0,
|
||||||
usage_cache_creation_tokens=0,
|
usage_cache_creation_tokens=0,
|
||||||
raw_response=fake_raw,
|
raw_response=fake_raw,
|
||||||
)
|
))
|
||||||
|
|
||||||
from src import openai_compatible as oc
|
from src import openai_compatible as oc
|
||||||
from src.vendor_capabilities import register, VendorCapabilities
|
from src.vendor_capabilities import register, VendorCapabilities
|
||||||
register(VendorCapabilities(vendor="minimax", model="MiniMax-M2.7", reasoning=True))
|
register(VendorCapabilities(vendor="minimax", model="MiniMax-M2.7", reasoning=True))
|
||||||
|
ai_client._model = "MiniMax-M2.7"
|
||||||
|
|
||||||
with patch.object(oc, "send_openai_compatible", side_effect=_fake_send_openai_compatible), \
|
with patch.object(oc, "send_openai_compatible", side_effect=_fake_send_openai_compatible), \
|
||||||
patch("src.ai_client._ensure_minimax_client", return_value=MagicMock()), \
|
patch("src.ai_client._ensure_minimax_client", return_value=MagicMock()), \
|
||||||
|
|||||||
Reference in New Issue
Block a user