ggml-org/llama.cpp · #31380
chat : strip the think block in the qwen3-coder with reasoning_format none
common/parsers/qwen3-coder.cpp12 + / 4 −
@@ -74,16 +74,24 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat data.parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { auto generation_prompt = p.literal(GEN_PREFIX); + // The template strips the newline before </think>, so leave it out of the reasoning+ auto think_body = p.until_one_of({ "\n</think>", "</think>", "<tool_call>" });+ auto think_close = p.literal("\n</think>") | p.literal("</think>") | p.peek(p.literal("<tool_call>"));++ auto reasoning_extracted = p.optional("<think>" + p.space() + p.reasoning(think_body) + think_close);+ auto reasoning_skipped = p.optional("<think>" + p.space() + think_body + think_close);+ auto reasoning = p.eps(); if (supports_reasoning && extract_reasoning) {- // The template strips the newline before </think>, so leave it out of the reasoning- reasoning = p.optional("<think>" + p.space() +- p.reasoning(p.until_one_of({ "\n</think>", "</think>", "<tool_call>" })) +- (p.literal("\n</think>") | p.literal("</think>") | p.peek(p.literal("<tool_call>"))));+ reasoning = reasoning_extracted; } // Response format parser if (has_response_format) {+ // The answer must be bare JSON, so the think block is consumed even when it is not extracted+ if (supports_reasoning && !extract_reasoning) {+ reasoning = reasoning_skipped;+ } return generation_prompt + (reasoning << p.content(p.schema(p.json(), "response-format", inputs.json_schema))); } tests/test-chat.cpp25 + / 0 −
@@ -5629,6 +5629,31 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .expect(message_assist_call_thoughts) .run(); + const std::string answer_schema = R"({"type":"object","properties":{"answer":{"type":"integer"}},"required":["answer"]})";++ tst.test("I'm\nthinking</think>{\"answer\":42}")+ .enable_thinking(true)+ .reasoning_format(COMMON_REASONING_FORMAT_AUTO)+ .json_schema(answer_schema)+ .expect_reasoning("I'm\nthinking")+ .expect_content(R"({"answer":42})")+ .run();++ // the reasoning block of the generation prompt must still be accepted when it is not extracted+ tst.test("{\"answer\":42}")+ .enable_thinking(false)+ .reasoning_format(COMMON_REASONING_FORMAT_NONE)+ .json_schema(answer_schema)+ .expect_content(R"({"answer":42})")+ .run();++ tst.test("I'm\nthinking</think>{\"answer\":42}")+ .enable_thinking(true)+ .reasoning_format(COMMON_REASONING_FORMAT_NONE)+ .json_schema(answer_schema)+ .expect_content(R"({"answer":42})")+ .run();+ // a deferred tool is left out of <tools>, a tool_reference renders it, and its schema types the call const auto deferred_tools = common_chat_tools_parse_oaicompat(json::parse(R"([ {"type": "function", "function": {"name": "tool_search", "description": "Search for tools",