From 6f9e1c434646501850a43a91c6c7bbc3dcd271e7 Mon Sep 17 00:00:00 2001 From: duguwanglong Date: Fri, 24 Apr 2026 15:39:19 +0800 Subject: [PATCH] fix(session/runner): force MiniMax XML tool-call mode for threatbook-cn-llm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `threatbook-cn-llm` gateway exposes MiniMax models over an OpenAI-compatible endpoint but its streaming chunks omit the `tool_calls` field entirely (observed 2026-04 with `minimax-m2.7`: first `ChoiceDelta` only carries `content` / `role`, `model_extra_keys=[]`). With native function-calling the model can never emit a tool call, so every turn ends with `finish_reason=stop` and `tool_calls=0` — users in IM channels just see a short text reply (e.g. a couple of IPs) and no tool execution. Add `threatbook-cn-llm` to the MiniMax text-call provider whitelist so `_should_use_text_tool_call_mode()` returns True for this provider+model pair. The runner then injects the `` XML instructions and the existing text parser picks the calls up from the content stream, restoring tool execution end-to-end. - Other models routed through the same gateway (qwen, GLM, etc.) remain on the standard OpenAI native function-calling path. - Comment in `_should_use_text_tool_call_mode()` records the root cause for future maintainers. Tests: - New regression cases in `TestMiniMaxTextToolMode`: - threatbook-cn-llm + minimax-m2.7 → XML mode enabled - case-insensitive provider/model id handling - threatbook-cn-llm + qwen3.6-plus → XML mode disabled - `tests/session/test_runner_step.py` (48 cases) all pass. Made-with: Cursor --- flocks/session/runner.py | 8 ++++++++ tests/session/test_runner_step.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/flocks/session/runner.py b/flocks/session/runner.py index 989b4475d..f5731ab00 100644 --- a/flocks/session/runner.py +++ b/flocks/session/runner.py @@ -1338,11 +1338,19 @@ def _build_tool_catalog_prompt(self, agent: AgentInfo) -> Optional[str]: ) def _should_use_text_tool_call_mode(self) -> bool: + # MiniMax models exposed through ThreatBook-managed gateways do not + # forward the OpenAI ``tool_calls`` field on their streaming chunks + # (observed on ``threatbook-cn-llm`` 2026-04: first ``ChoiceDelta`` + # only contains ``content`` / ``role``). Without this opt-in the + # model can never request a tool and ends every turn with + # ``finish_reason=stop`` and zero tool calls. Force the MiniMax XML + # text-call protocol for these provider/model pairs so tools work. model_lower = (self.model_id or "").lower() provider_lower = (self.provider_id or "").lower() minimax_text_tool_call_providers = { "custom-threatbook-internal", "custom-tb-inner", + "threatbook-cn-llm", } return ( "minimax" in model_lower diff --git a/tests/session/test_runner_step.py b/tests/session/test_runner_step.py index bab43b3fd..d3bff1861 100644 --- a/tests/session/test_runner_step.py +++ b/tests/session/test_runner_step.py @@ -622,6 +622,38 @@ def test_enabled_for_custom_tb_inner_minimax(self): ) assert runner._should_use_text_tool_call_mode() is True + def test_enabled_for_threatbook_cn_llm_minimax(self): + # Regression: threatbook-cn-llm gateway strips the OpenAI tool_calls + # field for MiniMax models, so the XML text-call protocol must be + # forced or every turn ends with finish_reason=stop and zero tools. + session = _make_session("ses_minimax_threatbook_cn_llm") + runner = SessionRunner( + session=session, + provider_id="threatbook-cn-llm", + model_id="minimax-m2.7", + ) + assert runner._should_use_text_tool_call_mode() is True + + def test_enabled_for_threatbook_cn_llm_minimax_case_insensitive(self): + session = _make_session("ses_minimax_threatbook_cn_llm_case") + runner = SessionRunner( + session=session, + provider_id="ThreatBook-CN-LLM", + model_id="MiniMax-M2.5", + ) + assert runner._should_use_text_tool_call_mode() is True + + def test_disabled_for_threatbook_cn_llm_non_minimax(self): + # Other models routed through the same gateway (e.g. qwen, GLM) keep + # the standard OpenAI native function-calling path. + session = _make_session("ses_threatbook_cn_llm_qwen") + runner = SessionRunner( + session=session, + provider_id="threatbook-cn-llm", + model_id="qwen3.6-plus", + ) + assert runner._should_use_text_tool_call_mode() is False + def test_disabled_for_other_models(self): session = _make_session("ses_normal_mode") runner = SessionRunner(