mirror of
https://github.com/ikawrakow/ik_llama.cpp.git
synced 2026-07-20 09:45:35 +00:00
fix: MiniMax-M3 streaming parser when tool calls start before </mm:think> (#2085)
* fix: improve MiniMax-M3 streaming parser * fix: guard MiniMax-M3 tool args against marker leaks --------- Co-authored-by: Smart <smart@augmented-special.services>
This commit is contained in:
@@ -75,6 +75,29 @@ static std::string escape_json_string_inner(const std::string & s) {
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static bool truncate_json_strings_at_marker(ordered_json & value, const std::string & marker) {
|
||||
bool changed = false;
|
||||
|
||||
if (value.is_string()) {
|
||||
std::string s = value.get<std::string>();
|
||||
size_t p = s.find(marker);
|
||||
if (p != std::string::npos) {
|
||||
value = s.substr(0, p);
|
||||
changed = true;
|
||||
}
|
||||
} else if (value.is_array()) {
|
||||
for (auto & item : value) {
|
||||
changed = truncate_json_strings_at_marker(item, marker) || changed;
|
||||
}
|
||||
} else if (value.is_object()) {
|
||||
for (auto & item : value.items()) {
|
||||
changed = truncate_json_strings_at_marker(item.value(), marker) || changed;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Convert Python-style single-quoted strings to JSON double-quoted strings
|
||||
// Only converts outer string delimiters, properly handling escape sequences:
|
||||
// - {'key': 'value'} -> {"key": "value"}
|
||||
@@ -249,6 +272,35 @@ void common_chat_peg_mapper::from_ast(const common_peg_ast_arena & arena,
|
||||
}
|
||||
}
|
||||
|
||||
void common_chat_peg_minimax_m3_mapper::from_ast(const common_peg_ast_arena & arena,
|
||||
const common_peg_parse_result & parse_result) {
|
||||
common_chat_peg_mapper::from_ast(arena, parse_result);
|
||||
|
||||
// MiniMax-M3 tool calls are emitted from the thinking/action phase.
|
||||
if (!result.tool_calls.empty() && !result.content.empty()) {
|
||||
result.reasoning_content += result.content;
|
||||
result.content.clear();
|
||||
}
|
||||
|
||||
// MiniMax-M3 sometimes starts emitting a new namespaced tool marker before
|
||||
// closing a string argument. Keep that marker out of executable tool text.
|
||||
static const std::string ns_marker = "]<]minimax[>[";
|
||||
for (auto & tool_call : result.tool_calls) {
|
||||
if (tool_call.arguments.find(ns_marker) == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
ordered_json args = ordered_json::parse(tool_call.arguments);
|
||||
if (truncate_json_strings_at_marker(args, ns_marker)) {
|
||||
tool_call.arguments = args.dump();
|
||||
}
|
||||
} catch (const ordered_json::exception &) {
|
||||
// Leave malformed JSON to existing downstream validation.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void common_chat_peg_mapper::map(const common_peg_ast_node & node) {
|
||||
// Handle reasoning/content tags
|
||||
bool is_reasoning = node.tag == common_chat_peg_builder::REASONING;
|
||||
|
||||
@@ -40,6 +40,12 @@ class common_chat_peg_gemma4_mapper : public common_chat_peg_mapper {
|
||||
void visit(const common_peg_ast_arena & arena, common_peg_ast_id id);
|
||||
};
|
||||
|
||||
class common_chat_peg_minimax_m3_mapper : public common_chat_peg_mapper {
|
||||
public:
|
||||
common_chat_peg_minimax_m3_mapper(common_chat_msg & msg) : common_chat_peg_mapper(msg) {}
|
||||
virtual void from_ast(const common_peg_ast_arena & arena, const common_peg_parse_result & result);
|
||||
};
|
||||
|
||||
struct content_structure;
|
||||
struct tool_call_structure;
|
||||
|
||||
@@ -195,4 +201,3 @@ struct tagged_peg_parser {
|
||||
|
||||
tagged_peg_parser build_tagged_peg_parser(
|
||||
const std::function<common_peg_parser(common_peg_parser_builder & builder)> & fn);
|
||||
|
||||
|
||||
+62
-12
@@ -162,12 +162,31 @@ std::vector<common_chat_msg_diff> common_chat_msg_diff::compute_diffs(const comm
|
||||
|
||||
// TODO: these can become expensive for long messages - how to optimize?
|
||||
if (msg_prv.reasoning_content != msg_new.reasoning_content) {
|
||||
auto & diff = diffs.emplace_back();
|
||||
diff.reasoning_content_delta = string_diff(msg_prv.reasoning_content, msg_new.reasoning_content);
|
||||
std::string reasoning_content_delta = string_diff(msg_prv.reasoning_content, msg_new.reasoning_content);
|
||||
// Partial parsing can reclassify bytes that were previously streamed as
|
||||
// content into reasoning_content once more context arrives. Streaming
|
||||
// deltas are append-only, so remove that already-streamed content prefix
|
||||
// from the reasoning delta instead of emitting the same bytes twice.
|
||||
if (!msg_prv.content.empty() &&
|
||||
string_starts_with(reasoning_content_delta, msg_prv.content) &&
|
||||
string_starts_with(msg_prv.content, msg_new.content)) {
|
||||
reasoning_content_delta = reasoning_content_delta.substr(msg_prv.content.size());
|
||||
}
|
||||
// string_diff() can return an empty delta for shrink/reclassification
|
||||
// cases. Do not emit empty append-only chunks.
|
||||
if (!reasoning_content_delta.empty()) {
|
||||
auto & diff = diffs.emplace_back();
|
||||
diff.reasoning_content_delta = std::move(reasoning_content_delta);
|
||||
}
|
||||
}
|
||||
if (msg_prv.content != msg_new.content) {
|
||||
auto & diff = diffs.emplace_back();
|
||||
diff.content_delta = string_diff(msg_prv.content, msg_new.content);
|
||||
std::string content_delta = string_diff(msg_prv.content, msg_new.content);
|
||||
// Same empty-delta rule as above, without the reasoning/content overlap
|
||||
// trim: there may be no new content bytes to append.
|
||||
if (!content_delta.empty()) {
|
||||
auto & diff = diffs.emplace_back();
|
||||
diff.content_delta = std::move(content_delta);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg_new.tool_calls.size() < msg_prv.tool_calls.size()) {
|
||||
@@ -680,6 +699,8 @@ const char * common_chat_format_name(common_chat_format format) {
|
||||
return "peg-native";
|
||||
case COMMON_CHAT_FORMAT_PEG_GEMMA4:
|
||||
return "peg-gemma4";
|
||||
case COMMON_CHAT_FORMAT_PEG_MINIMAX_M3:
|
||||
return "peg-minimax-m3";
|
||||
default:
|
||||
throw std::runtime_error("Unknown chat format");
|
||||
}
|
||||
@@ -1938,7 +1959,7 @@ static common_chat_params common_chat_params_init_minimax_m3(const common_chat_t
|
||||
common_chat_params data;
|
||||
|
||||
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
|
||||
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
|
||||
data.format = COMMON_CHAT_FORMAT_PEG_MINIMAX_M3;
|
||||
data.supports_thinking = true;
|
||||
data.thinking_start_tag = "<mm:think>";
|
||||
data.thinking_end_tag = "</mm:think>";
|
||||
@@ -1967,11 +1988,35 @@ static common_chat_params common_chat_params_init_minimax_m3(const common_chat_t
|
||||
auto generation_prompt = p.prefix(inputs.generation_prompt, THINK_START);
|
||||
auto end = p.end();
|
||||
|
||||
auto thinking_body = [&]() {
|
||||
return has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE
|
||||
? p.until_one_of({ THINK_END, FC_START })
|
||||
: p.until(THINK_END);
|
||||
};
|
||||
|
||||
auto reasoning = p.eps();
|
||||
if (extract_reasoning && inputs.enable_thinking) {
|
||||
reasoning = p.optional(p.optional(p.literal(THINK_START)) + p.reasoning(p.until(THINK_END)) + THINK_END);
|
||||
} else if (extract_reasoning) {
|
||||
reasoning = p.optional(p.optional(p.literal(THINK_START)) + p.until(THINK_END) + p.literal(THINK_END));
|
||||
if (extract_reasoning) {
|
||||
// The generation prompt usually opens <mm:think>, but MiniMax-M3 may
|
||||
// also emit an unopened block ending in </mm:think>. Keep both forms
|
||||
// quarantined as reasoning instead of leaking them into visible text.
|
||||
auto opened = p.literal(THINK_START) +
|
||||
p.reasoning(thinking_body()) +
|
||||
p.optional(p.literal(THINK_END));
|
||||
auto unopened = p.reasoning(thinking_body()) + p.literal(THINK_END);
|
||||
reasoning = p.optional(p.choice({ opened, unopened }));
|
||||
|
||||
if (inputs.enable_thinking) {
|
||||
// During streaming, the closing tag may not have arrived yet; if
|
||||
// a tool call starts first, stop reasoning at the tool marker.
|
||||
auto partial = p.reasoning(thinking_body());
|
||||
reasoning = p.optional(p.choice({ opened, unopened, partial }));
|
||||
}
|
||||
} else if (inputs.enable_thinking) {
|
||||
auto opened = p.content(p.literal(THINK_START) +
|
||||
thinking_body() +
|
||||
p.optional(p.literal(THINK_END)));
|
||||
auto unopened = p.content(thinking_body()) + p.literal(THINK_END);
|
||||
reasoning = p.optional(p.choice({ opened, unopened }));
|
||||
}
|
||||
|
||||
if (has_response_format) {
|
||||
@@ -2056,16 +2101,17 @@ static common_chat_params common_chat_params_init_minimax_m3(const common_chat_t
|
||||
|
||||
auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
||||
|
||||
common_peg_parser tool_calls = p.eps();
|
||||
common_peg_parser tool_call_envelope = p.eps();
|
||||
if (inputs.parallel_tool_calls) {
|
||||
tool_calls = p.trigger_rule("tool-call",
|
||||
tool_call_envelope = p.trigger_rule("tool-call",
|
||||
p.literal(FC_START) + p.space() + tool_choice +
|
||||
p.zero_or_more(p.space() + tool_choice) + p.space() + p.literal(FC_END));
|
||||
} else {
|
||||
tool_calls = p.trigger_rule("tool-call",
|
||||
tool_call_envelope = p.trigger_rule("tool-call",
|
||||
p.literal(FC_START) + p.space() + tool_choice + p.space() + p.literal(FC_END));
|
||||
}
|
||||
|
||||
auto tool_calls = tool_call_envelope;
|
||||
if (!require_tools) {
|
||||
tool_calls = p.optional(tool_calls);
|
||||
}
|
||||
@@ -2780,6 +2826,8 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & src_pars
|
||||
std::unique_ptr<common_chat_peg_mapper> mapper;
|
||||
if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) {
|
||||
mapper = std::make_unique<common_chat_peg_gemma4_mapper>(msg);
|
||||
} else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) {
|
||||
mapper = std::make_unique<common_chat_peg_minimax_m3_mapper>(msg);
|
||||
} else {
|
||||
mapper = std::make_unique<common_chat_peg_mapper>(msg);
|
||||
}
|
||||
@@ -2801,6 +2849,8 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & src_pars
|
||||
std::unique_ptr<common_chat_peg_mapper> mapper;
|
||||
if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) {
|
||||
mapper = std::make_unique<common_chat_peg_gemma4_mapper>(msg);
|
||||
} else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) {
|
||||
mapper = std::make_unique<common_chat_peg_minimax_m3_mapper>(msg);
|
||||
} else {
|
||||
mapper = std::make_unique<common_chat_peg_mapper>(msg);
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ enum common_chat_format {
|
||||
COMMON_CHAT_FORMAT_PEG_SIMPLE,
|
||||
COMMON_CHAT_FORMAT_PEG_NATIVE,
|
||||
COMMON_CHAT_FORMAT_PEG_GEMMA4,
|
||||
COMMON_CHAT_FORMAT_PEG_MINIMAX_M3,
|
||||
|
||||
COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats
|
||||
};
|
||||
|
||||
@@ -65,6 +65,9 @@ static void test_cohere_analysis(testing & t);
|
||||
// End-to-end Cohere2MoE (North Code) dedicated PEG parser coverage.
|
||||
static void test_cohere2moe_parser(testing & t);
|
||||
|
||||
// End-to-end MiniMax-M3 dedicated PEG parser coverage.
|
||||
static void test_minimax_m3_partial_reasoning_parser(testing & t);
|
||||
|
||||
// SmolLM3 template analysis tests
|
||||
static void test_smollm3_analysis(testing & t);
|
||||
|
||||
@@ -102,6 +105,7 @@ int main(int argc, char * argv[]) {
|
||||
t.test("seed_oss_diffs", test_seed_oss_tool_analysis);
|
||||
t.test("cohere", test_cohere_analysis);
|
||||
t.test("cohere2moe_parser", test_cohere2moe_parser);
|
||||
t.test("minimax_m3_partial_reasoning_parser", test_minimax_m3_partial_reasoning_parser);
|
||||
t.test("nemotron", test_nemotron_analysis);
|
||||
t.test("smollm3", test_smollm3_analysis);
|
||||
t.test("standard_json_tools", test_standard_json_tools_formats);
|
||||
@@ -1971,6 +1975,191 @@ static void test_tagged_args_with_embedded_quotes(testing & t) {
|
||||
}
|
||||
}
|
||||
|
||||
// End-to-end coverage for MiniMax-M3 partial reasoning streams:
|
||||
// template apply -> PEG parse -> assert message and streaming diff. The partial
|
||||
// case mirrors a real leaked <mm:think> stream where </mm:think> had not arrived.
|
||||
static void test_minimax_m3_partial_reasoning_parser(testing & t) {
|
||||
const std::string template_str = R"(
|
||||
{%- set ns_token = ']<]minimax[>[' -%}
|
||||
{%- set toolcall_begin_token = ns_token ~ '<tool_call>' -%}
|
||||
{%- set toolcall_end_token = ns_token ~ '</tool_call>' -%}
|
||||
{%- for message in messages -%}
|
||||
{{- message.role ~ ': ' ~ message.content ~ '\n' -}}
|
||||
{%- endfor -%}
|
||||
{%- if tools -%}
|
||||
{{- toolcall_begin_token ~ ns_token ~ '<invoke name="example">' ~ ns_token ~ '</invoke>' ~ toolcall_end_token -}}
|
||||
{%- endif -%}
|
||||
{%- if add_generation_prompt -%}
|
||||
{{- '<mm:think>' -}}
|
||||
{%- endif -%}
|
||||
)";
|
||||
|
||||
common_chat_templates_inputs inputs;
|
||||
common_chat_msg user;
|
||||
user.role = "user";
|
||||
user.content = "Please inspect the extension state.";
|
||||
|
||||
inputs.messages = { user };
|
||||
inputs.add_generation_prompt = true;
|
||||
inputs.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
|
||||
inputs.enable_thinking = true;
|
||||
inputs.tools = {
|
||||
common_chat_tool{
|
||||
/* .name = */ "example",
|
||||
/* .description = */ "Example tool",
|
||||
/* .parameters = */ R"({"type":"object","properties":{}})",
|
||||
},
|
||||
common_chat_tool{
|
||||
/* .name = */ "bash",
|
||||
/* .description = */ "Run shell commands",
|
||||
/* .parameters = */ R"({"type":"object","properties":{"command":{"type":"string"}},"required":["command"]})",
|
||||
},
|
||||
};
|
||||
|
||||
common_chat_templates_ptr tmpls(common_chat_templates_init(/* model = */ nullptr, template_str));
|
||||
auto params = common_chat_templates_apply(tmpls.get(), inputs);
|
||||
t.assert_equal("MiniMax-M3 parser selected", COMMON_CHAT_FORMAT_PEG_MINIMAX_M3, params.format);
|
||||
t.assert_true("MiniMax-M3 parser generated", !params.parser.empty());
|
||||
|
||||
common_peg_arena arena;
|
||||
arena.load(params.parser);
|
||||
|
||||
common_chat_parser_params parser_params(params);
|
||||
parser_params.parser = arena;
|
||||
|
||||
const std::string partial_reasoning =
|
||||
"The user is asking why I'm implementing F3 directly. Looking at the context:\n\n"
|
||||
"1. The user said earlier: \"yes we do F3 manually, by";
|
||||
|
||||
auto partial_msg = common_chat_parse(partial_reasoning, /* is_partial = */ true, parser_params);
|
||||
t.assert_equal("partial reasoning", partial_reasoning, partial_msg.reasoning_content);
|
||||
t.assert_equal("partial visible content", std::string(), partial_msg.content);
|
||||
t.assert_true("partial has no tool calls", partial_msg.tool_calls.empty());
|
||||
|
||||
auto diffs = common_chat_msg_diff::compute_diffs(common_chat_msg{}, partial_msg);
|
||||
t.assert_equal("partial emits one diff", 1u, diffs.size());
|
||||
if (diffs.size() == 1) {
|
||||
t.assert_equal("partial diff is reasoning", partial_reasoning, diffs[0].reasoning_content_delta);
|
||||
t.assert_equal("partial diff has no visible content", std::string(), diffs[0].content_delta);
|
||||
t.assert_equal("partial diff has no tool index", std::string::npos, diffs[0].tool_call_index);
|
||||
}
|
||||
|
||||
const std::string closed_output = partial_reasoning + "</mm:think>You're right to question.";
|
||||
auto closed_msg = common_chat_parse(closed_output, /* is_partial = */ false, parser_params);
|
||||
t.assert_equal("closed reasoning", partial_reasoning, closed_msg.reasoning_content);
|
||||
t.assert_equal("closed visible content", std::string("You're right to question."), closed_msg.content);
|
||||
t.assert_true("closed has no tool calls", closed_msg.tool_calls.empty());
|
||||
|
||||
const std::string command =
|
||||
"# Deploy to electra and erabus\n"
|
||||
"SSH_AUTH_SOCK=/run/user/1001/ssh-agent.socket scp index.ts electra:/tmp/index.ts";
|
||||
const std::string tool_output =
|
||||
"Let me do that."
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"bash\">"
|
||||
"]<]minimax[>[<command>" + command + "]<]minimax[>[</command>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>";
|
||||
|
||||
auto tool_msg = common_chat_parse(tool_output, /* is_partial = */ false, parser_params);
|
||||
t.assert_equal("tool reasoning excludes markers", std::string("Let me do that."), tool_msg.reasoning_content);
|
||||
t.assert_equal("tool visible content", std::string(), tool_msg.content);
|
||||
t.assert_equal("tool call count", 1u, tool_msg.tool_calls.size());
|
||||
if (tool_msg.tool_calls.size() == 1) {
|
||||
t.assert_equal("tool call name", std::string("bash"), tool_msg.tool_calls[0].name);
|
||||
t.assert_equal("tool call args", json({{"command", command}}).dump(), tool_msg.tool_calls[0].arguments);
|
||||
}
|
||||
|
||||
const std::string cmux_command =
|
||||
"cmux rpc surface.scrollback '{\"surface_id\":\"20CD49E8-A0FA-42E3-B349-7CCCC1075425\","
|
||||
"\"workspace_id\":\"826FA4BA-01E5-4D84-893A-D70B0DAF9970\"}' 2>&1 | head -3";
|
||||
const std::string malformed_nested_tool_output =
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"bash\">"
|
||||
"]<]minimax[>[<command>" + cmux_command +
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>老实\n"
|
||||
"</command>]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>\n"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"bash\n"
|
||||
"</parameter>]<]minimax[>[</command>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>";
|
||||
|
||||
auto malformed_nested_tool_msg = common_chat_parse(malformed_nested_tool_output,
|
||||
/* is_partial = */ false,
|
||||
parser_params);
|
||||
t.assert_equal("malformed nested tool call count", 1u, malformed_nested_tool_msg.tool_calls.size());
|
||||
if (malformed_nested_tool_msg.tool_calls.size() == 1) {
|
||||
t.assert_equal("malformed nested tool call name", std::string("bash"), malformed_nested_tool_msg.tool_calls[0].name);
|
||||
t.assert_equal(
|
||||
"malformed nested tool args truncate namespace leak",
|
||||
json({{"command", cmux_command}}).dump(),
|
||||
malformed_nested_tool_msg.tool_calls[0].arguments);
|
||||
t.assert_true("malformed nested tool args contain no minimax marker",
|
||||
malformed_nested_tool_msg.tool_calls[0].arguments.find("]<]minimax[>[") == std::string::npos);
|
||||
}
|
||||
|
||||
inputs.enable_thinking = false;
|
||||
auto params_thinking_off = common_chat_templates_apply(tmpls.get(), inputs);
|
||||
|
||||
common_peg_arena thinking_off_arena;
|
||||
thinking_off_arena.load(params_thinking_off.parser);
|
||||
|
||||
common_chat_parser_params thinking_off_parser_params(params_thinking_off);
|
||||
thinking_off_parser_params.parser = thinking_off_arena;
|
||||
|
||||
const std::string leaked_reasoning =
|
||||
"Right pane is up. Now let me think about the workflow lessons and update "
|
||||
"kickass + smartass to include them explicitly.\n\n"
|
||||
"Looking at kickass.md:\n"
|
||||
"- Rule 4 covers L7 partially\n"
|
||||
"- Rule 2 covers L15\n\n"
|
||||
"Let me update kickass and smartass.";
|
||||
const std::string tool_prelude =
|
||||
"Let me update kickass and smartass with the workflow lessons explicitly. "
|
||||
"First check what's there:";
|
||||
const std::string unopened_closed_tool_output =
|
||||
leaked_reasoning + "</mm:think>" + tool_prelude +
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"bash\">"
|
||||
"]<]minimax[>[<command>cat /home/leaf/.pi/agent/agents/kickass.md 2>&1]<]minimax[>[</command>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>";
|
||||
|
||||
auto unopened_closed_tool_msg = common_chat_parse(unopened_closed_tool_output, /* is_partial = */ false, thinking_off_parser_params);
|
||||
t.assert_equal("unopened closed tool reasoning", leaked_reasoning + tool_prelude, unopened_closed_tool_msg.reasoning_content);
|
||||
t.assert_equal("unopened closed tool visible content", std::string(), unopened_closed_tool_msg.content);
|
||||
t.assert_equal("unopened closed tool call count", 1u, unopened_closed_tool_msg.tool_calls.size());
|
||||
if (unopened_closed_tool_msg.tool_calls.size() == 1) {
|
||||
t.assert_equal("unopened closed tool name", std::string("bash"), unopened_closed_tool_msg.tool_calls[0].name);
|
||||
t.assert_equal(
|
||||
"unopened closed tool args",
|
||||
json({{"command", "cat /home/leaf/.pi/agent/agents/kickass.md 2>&1"}}).dump(),
|
||||
unopened_closed_tool_msg.tool_calls[0].arguments);
|
||||
}
|
||||
|
||||
const std::string migrated_prefix =
|
||||
"Chain 6 started. JakASS is reading the prompt. Now sleeping 1 hour:";
|
||||
auto migrated_content_msg = common_chat_parse(migrated_prefix, /* is_partial = */ true, thinking_off_parser_params);
|
||||
t.assert_equal("migrated prefix parser treats as reasoning", migrated_prefix, migrated_content_msg.reasoning_content);
|
||||
t.assert_equal("migrated prefix parser has no visible content", std::string(), migrated_content_msg.content);
|
||||
|
||||
auto migrated_reasoning_msg = common_chat_parse(migrated_prefix + "</mm:think>",
|
||||
/* is_partial = */ true,
|
||||
thinking_off_parser_params);
|
||||
t.assert_equal("migrated prefix later reasoning", migrated_prefix, migrated_reasoning_msg.reasoning_content);
|
||||
t.assert_equal("migrated prefix later not visible", std::string(), migrated_reasoning_msg.content);
|
||||
|
||||
common_chat_msg streamed_as_content;
|
||||
streamed_as_content.role = "assistant";
|
||||
streamed_as_content.content = migrated_prefix;
|
||||
auto migrated_diffs = common_chat_msg_diff::compute_diffs(streamed_as_content, migrated_reasoning_msg);
|
||||
t.assert_equal("migrated prefix emits no duplicate diff", 0u, migrated_diffs.size());
|
||||
}
|
||||
|
||||
// End-to-end coverage for the dedicated Cohere2MoE (North Code) parser:
|
||||
// template apply -> PEG parse -> assert message. Exercises the reasoning-mode
|
||||
// matrix, including the unopened-thinking-under---reasoning-off case (#1968
|
||||
|
||||
@@ -294,7 +294,7 @@ static void test_minimax_m3_native_tool_parser(void) {
|
||||
common_chat_templates_ptr tmpls(common_chat_templates_init(/* model= */ nullptr, template_str));
|
||||
auto params = common_chat_templates_apply(tmpls.get(), inputs);
|
||||
|
||||
assert(params.format == COMMON_CHAT_FORMAT_PEG_NATIVE);
|
||||
assert(params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3);
|
||||
assert(!params.parser.empty());
|
||||
|
||||
common_peg_arena arena;
|
||||
|
||||
Reference in New Issue
Block a user