result.suffix = "";
// pick prefix = all as representation
}
+
+ // When left has no unique content (result.left is empty), left is entirely
+ // shared with right. The simultaneous prefix/suffix segment matching can
+ // incorrectly consume trailing segments of left as suffix when those same
+ // segments also appear at the end of right (e.g. "\n" at the end of both
+ // the shared content and the generation prompt). This rotates the diff.
+ // Fix: if left is a prefix of right, enforce that directly.
+ if (result.left.empty() && !result.right.empty() &&
+ left.size() <= right.size() &&
+ right.substr(0, left.size()) == left) {
+ result.prefix = left;
+ result.suffix = "";
+ result.right = right.substr(left.size());
+ }
+
return result;
}
static void test_calculate_diff_split_single_char(testing & t);
static void test_calculate_diff_split_overlaps(testing & t);
static void test_calculate_diff_split_tag_boundaries(testing & t);
+static void test_calculate_diff_split_generation_prompt(testing & t);
static void test_calculate_diff_split(testing & t);
static void test_until_common_prefix_basic(testing & t);
t.test("calculate_diff_split single char", test_calculate_diff_split_single_char);
t.test("calculate_diff_split overlaps", test_calculate_diff_split_overlaps);
t.test("calculate_diff_split tag boundaries", test_calculate_diff_split_tag_boundaries);
+ t.test("calculate_diff_split generation prompt", test_calculate_diff_split_generation_prompt);
}
static void test_calculate_diff_split_basic(testing & t) {
}
}
+static void test_calculate_diff_split_generation_prompt(testing & t) {
+ // ChatML thinking template: left is a prefix of right, generation_prompt is the appended part.
+ // The trailing \n in left matches the trailing \n in the generation_prompt, causing
+ // the suffix matcher to steal it and rotate the diff result.
+ {
+ // Simplified reproduction: left ends with \n, right = left + "<|im_start|>assistant\n<think>\n"
+ std::string left = "<|im_start|>user\nHello<|im_end|>\n";
+ std::string right = left + "<|im_start|>assistant\n<think>\n";
+ diff_split result = calculate_diff_split(left, right);
+ t.assert_equal("chatml prefix", left, result.prefix);
+ t.assert_equal("chatml left", "", result.left);
+ t.assert_equal("chatml right should be generation prompt",
+ "<|im_start|>assistant\n<think>\n", result.right);
+ t.assert_equal("chatml suffix", "", result.suffix);
+ }
+
+ {
+ // More realistic: longer conversation ending with tool_response
+ std::string common =
+ "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
+ "<|im_start|>user\nSearch for files<|im_end|>\n"
+ "<|im_start|>assistant\n<think>\nLet me search.\n</think>\n\n"
+ "<tool_call>\n<function=search>\n</function>\n</tool_call><|im_end|>\n"
+ "<|im_start|>user\n<tool_response>\nNo files found\n</tool_response><|im_end|>\n";
+ std::string left = common;
+ std::string right = common + "<|im_start|>assistant\n<think>\n";
+ diff_split result = calculate_diff_split(left, right);
+ t.assert_equal("tool_response left", "", result.left);
+ t.assert_equal("tool_response right should be generation prompt",
+ "<|im_start|>assistant\n<think>\n", result.right);
+ }
+}
+
static void test_until_common_prefix(testing & t) {
t.test("until_common_prefix basic", test_until_common_prefix_basic);
}
params.chat_parser_params.reasoning_in_content = params.stream && (reasoning_format == COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY);
params.chat_parser_params.generation_prompt = json_value(data, "generation_prompt", std::string());
params.sampling.generation_prompt = params.chat_parser_params.generation_prompt;
+ SRV_DBG("Generation prompt: '%s'\n", params.chat_parser_params.generation_prompt.c_str());
params.chat_parser_params.parse_tool_calls = json_value(data, "parse_tool_calls", false);
if (data.contains("chat_parser")) {
params.chat_parser_params.parser.load(data.at("chat_parser").get<std::string>());