chat: Cohere2MoE/North Code: parse unopened thinking under --reasoning off (follow-up to #1968) (#2012)

* Handle Cohere2MoE unopened thinking before tools

* Cohere2MoE: route unopened thinking to reasoning_content; test in active target

Follow-up to #1968. Gate extract_reasoning on reasoning_format only (drop the
"&& enable_thinking" addition) so the unopened-thinking handling does not also
change where an opened thinking block is routed. Under --reasoning off
(enable_thinking=false, reasoning_format defaults to DEEPSEEK) an orphaned
thinking block is now quarantined in reasoning_content with clean content and a
native tool call, instead of leaking the thinking prose into the user-facing
answer.

Move the Cohere2MoE end-to-end parser cases into tests/test-chat-auto-parser.cpp,
which CMake actually builds. tests/test-chat.cpp has been disabled in
tests/CMakeLists.txt since #723, so cohere coverage added there never ran in CI;
revert the local band-aids to that file.

* Cohere2MoE: harden parser from NMC eval findings

---------

Co-authored-by: Joel Farthing <262452229+joelfarthing@users.noreply.github.com>
This commit is contained in:
Joel Farthing
2026-06-24 09:04:41 +02:00
committed by GitHub
co-authored by Joel Farthing
parent 5a4fa17947
commit 8686ea708b
2 changed files with 185 additions and 7 deletions
+27 -7
View File
@@ -2144,22 +2144,42 @@ static common_chat_params common_chat_params_init_cohere2moe(const common_chat_t
};
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
// Surface reasoning as reasoning_content whenever the output format requests it
// (Cohere2MoE has a non-empty THINK_START, so this matches the narrow condition
// from the reasoning-delimiter work). Under --reasoning off the model may still
// emit an (un)opened thinking block; keeping it in reasoning_content quarantines
// it from the user-facing content rather than leaking it into the answer.
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
auto generation_prompt = p.prefix(inputs.generation_prompt, THINK_START);
auto end = p.optional(p.literal(TURN_END)) + p.end();
// Cohere2MoE can emit a stray text terminator after an action envelope.
auto end = p.optional(p.literal(TEXT_END)) + p.optional(p.literal(TURN_END)) + p.end();
auto thinking_body = [&]() {
return p.until_one_of({ THINK_END, TEXT_START, ACTION_START });
};
common_peg_parser reasoning = p.eps();
if (extract_reasoning) {
reasoning = p.optional(p.literal(THINK_START) +
p.reasoning(p.until_one_of({ THINK_END, TEXT_START, ACTION_START })) +
p.optional(p.literal(THINK_END)));
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 }));
} 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 }));
} else {
reasoning = p.optional(p.content(p.literal(THINK_START) +
p.until_one_of({ THINK_END, TEXT_START, ACTION_START }) +
p.optional(p.literal(THINK_END))));
auto opened = p.literal(THINK_START) +
p.content(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 }));
}
auto text_content = p.literal(TEXT_START) + p.content(p.until(TEXT_END)) + p.optional(p.literal(TEXT_END));