From 1052f7e1d62611d328ccc65d38c77cd194ee59fc Mon Sep 17 00:00:00 2001 From: Tarek Dakhran Date: Sat, 31 Oct 2026 04:38:04 +0100 Subject: [PATCH] chat : strip the think block in the qwen3-coder with reasoning_format none (#31380) * chat : accept the think block in the qwen3-coder response format with reasoning_format none Assisted-by: Claude Opus 5.5 * chat : define both qwen3-coder think block variants up front --- common/parsers/qwen3-coder.cpp | 16 ++++++++++++---- tests/test-chat.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/common/parsers/qwen3-coder.cpp b/common/parsers/qwen3-coder.cpp index 7cb9cce90..a68a6835e 100644 --- a/common/parsers/qwen3-coder.cpp +++ b/common/parsers/qwen3-coder.cpp @@ -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 , so leave it out of the reasoning + auto think_body = p.until_one_of({ "\n", "", "" }); + auto think_close = p.literal("\n") | p.literal("") | p.peek(p.literal("")); + + auto reasoning_extracted = p.optional("" + p.space() + p.reasoning(think_body) + think_close); + auto reasoning_skipped = p.optional("" + p.space() + think_body + think_close); + auto reasoning = p.eps(); if (supports_reasoning && extract_reasoning) { - // The template strips the newline before , so leave it out of the reasoning - reasoning = p.optional("" + p.space() + - p.reasoning(p.until_one_of({ "\n", "", "" })) + - (p.literal("\n") | p.literal("") | p.peek(p.literal("")))); + 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))); } diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 0ab8e65ab..fa4525389 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -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{\"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{\"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 , 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",