diff --git a/common/chat.cpp b/common/chat.cpp index ef151691c..4401225c5 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1116,15 +1116,21 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { - auto start = p.rule("start", p.literal("<|start|>assistant")); + // llm-jp-4 thinking models emit a stray leading space after + // <|start|>/<|channel|> (add_space_prefix detokenization), e.g. + // "<|channel|> analysis<|message|>" instead of the canonical + // "<|channel|>analysis<|message|>". Tolerate an optional space + // right after those special tokens so the peg-native rules match. + auto opt_space = p.optional(p.space()); + auto start = p.rule("start", p.literal("<|start|>") + opt_space + p.literal("assistant")); auto end = p.rule("end", p.literal("<|end|>")); auto content = p.rule("message-content", p.until("<|end|>")); - auto channel = p.literal("<|channel|>") + (p.literal("commentary") | p.literal("analysis")); + auto channel = p.literal("<|channel|>") + opt_space + (p.literal("commentary") | p.literal("analysis")); auto constrain_type = p.chars("[A-Za-z0-9_-]", 1, -1); // 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"))); - auto start_analysis = stray_commentary + p.literal("<|channel|>analysis<|message|>"); + auto stray_commentary = p.optional(p.literal("<|channel|>") + opt_space + p.literal("commentary") + p.optional(p.literal(" to=assistant"))); + auto start_analysis = stray_commentary + p.literal("<|channel|>") + opt_space + p.literal("analysis<|message|>"); if (extract_reasoning) { p.rule("analysis", start_analysis + p.reasoning(content) + end); @@ -1133,8 +1139,8 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp } 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 preamble = p.rule("preamble", p.literal("<|channel|>") + opt_space + p.literal("commentary<|message|>") + p.content(content) + end); + auto final_msg = p.rule("final", stray_commentary + p.literal("<|channel|>") + opt_space + p.literal("final<|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)); @@ -1144,7 +1150,7 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp 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.literal("<|channel|>") + opt_space + p.literal("final") + constraint + p.literal("<|message|>") + p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema))); return p.zero_or_more(start + analysis) + start + response_format; @@ -1164,11 +1170,11 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp // recipient in role header // <|start|>assistant to=functions.NAME<|channel|>(commentary|analysis)[constraint]<|message|>ARGS - auto tool_in_role = p.tool(p.tool_open(func_name + channel + constraint + p.literal("<|message|>")) + args); + auto tool_in_role = p.tool(p.tool_open(func_name + channel + constraint + opt_space + p.literal("<|message|>")) + args); // recipient in channel header // <|channel|>(commentary|analysis) to=functions.NAME[constraint]<|message|>ARGS - auto tool_in_channel = p.tool(p.tool_open(channel + func_name + constraint + p.literal("<|message|>")) + args); + auto tool_in_channel = p.tool(p.tool_open(channel + func_name + constraint + opt_space + p.literal("<|message|>")) + args); tool_choice |= p.rule("tool-" + name, tool_in_role | tool_in_channel); });