Commit 4402e6540 for llama.cpp
commit 4402e65401f11587e1ed2e549b2c0c0285d871ee
Author: frogscooper <frogscooper@gmail.com>
Date: Mon Nov 2 21:55:11 2026 -0800
chat : allow constraint type after gpt-oss final channel (#31556)
diff --git a/common/parsers/gpt-oss.cpp b/common/parsers/gpt-oss.cpp
index 5ccdff57d..4cbf929f9 100644
--- a/common/parsers/gpt-oss.cpp
+++ b/common/parsers/gpt-oss.cpp
@@ -71,6 +71,7 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template &
auto content = p.rule("message-content", p.until("<|end|>"));
auto channel = p.literal("<|channel|>") + (p.literal("commentary") | p.literal("analysis"));
auto constrain_type = p.chars("[A-Za-z0-9_-]", 1, -1);
+ auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type);
// Occasionally, gpt-oss-20b will prefix channels with this commentary
auto stray_commentary = p.optional(p.literal("<|channel|>commentary") + p.optional(p.literal(" to=assistant")));
@@ -84,7 +85,7 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template &
auto analysis = p.ref("analysis");
auto preamble = p.rule("preamble", p.literal("<|channel|>commentary<|message|>") + p.content(content) + end);
- auto final_msg = p.rule("final", stray_commentary + p.literal("<|channel|>final<|message|>") + p.content(content));
+ auto final_msg = p.rule("final", stray_commentary + p.literal("<|channel|>final") + constraint + p.literal("<|message|>") + p.content(content));
// Consume any unsolicited tool calls, e.g. builtin functions
auto unsolicited = p.rule("unsolicited", p.atomic(p.optional(channel) + p.literal(" to=") + content + end));
@@ -92,7 +93,6 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template &
auto any = p.rule("any", preamble | analysis);
if (has_response_format) {
- auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type);
auto response_format = p.rule("response-format",
p.literal("<|channel|>final") + constraint + p.literal("<|message|>") +
p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)));
@@ -109,7 +109,6 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template &
const auto params = common_chat_tool_parameters(function);
auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name));
- auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type);
auto args = p.tool_args(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-schema", params));
// recipient in role header
diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
index fa4525389..4d10815e5 100644
--- a/tests/test-chat.cpp
+++ b/tests/test-chat.cpp
@@ -6937,6 +6937,10 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
// Basic content only - final channel
tst.test("<|channel|>final<|message|>Hello, world!\nWhat's up?").expect(message_assist).run();
+ // Final channel with a constraint type, with and without <|constrain|> (#31520)
+ tst.test("<|channel|>final code<|message|>Hello, world!\nWhat's up?").expect(message_assist).run();
+ tst.test("<|channel|>final <|constrain|>json<|message|>Hello, world!\nWhat's up?").expect(message_assist).run();
+
// Basic content only - commentary channel
tst.test("<|channel|>commentary<|message|>Hello, world!\nWhat's up?").expect(message_assist).run();