Skip to content

Commit 2d97363

Browse files
pwilkintarruda
andauthored
chat: trim messages sent to StepFun parser (fixes long reasoning loops) (ggml-org#25238)
* chat: trim messages sent to StepFun parser (fixes long reasoning loops) * add regression test; remove duplicate template * chat: trim StepFun content parts before rendering The StepFun trim workaround ran on the already-rendered messages, where typed content parts have been concatenated into a single string, so the per-part whitespace could no longer be reached. Move the trim ahead of rendering and apply it to content_parts text as well as the string content and reasoning_content. Adds a content-parts regression test. Co-Authored-By: Piotr Wilkin <ilintar@gmail.com> Assisted-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: tarruda <tpadilha84@gmail.com>
1 parent d4cff11 commit 2d97363

4 files changed

Lines changed: 80 additions & 82 deletions

File tree

common/chat.cpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2378,6 +2378,23 @@ static void func_args_not_string(json & messages) {
23782378
}
23792379
}
23802380

2381+
// Trim leading/trailing whitespace from message contents before rendering. This
2382+
// has to run on the messages (not on the rendered JSON) because templates with
2383+
// string-only content caps concatenate typed content parts into a single string
2384+
// during rendering, after which the per-part whitespace can no longer be reached.
2385+
// Both the plain string content and the text of typed content parts are trimmed.
2386+
static void trim_all_content(std::vector<common_chat_msg> & messages) {
2387+
for (auto & message : messages) {
2388+
message.content = trim_whitespace(message.content);
2389+
message.reasoning_content = trim_whitespace(message.reasoning_content);
2390+
for (auto & part : message.content_parts) {
2391+
if (part.type == "text") {
2392+
part.text = trim_whitespace(part.text);
2393+
}
2394+
}
2395+
}
2396+
}
2397+
23812398
}
23822399

23832400
// MiniCPM5 format:
@@ -2634,7 +2651,16 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_
26342651
params.tools.is_array() && tmpls->template_tool_use ? *tmpls->template_tool_use : *tmpls->template_default;
26352652
const auto & src = tmpl.source();
26362653
const auto & caps = tmpl.original_caps();
2637-
params.messages = render_message_to_json(inputs.messages, tmpl.original_caps());
2654+
std::vector<common_chat_msg> trimmed_messages;
2655+
const std::vector<common_chat_msg> * messages_to_render = &inputs.messages;
2656+
if (src.find("You have access to the following functions in JSONSchema format") != std::string::npos) {
2657+
// StepFun: trim message contents (including typed content parts) before rendering,
2658+
// otherwise leftover whitespace drives the model into reasoning loops (issue #24181)
2659+
trimmed_messages = inputs.messages;
2660+
workaround::trim_all_content(trimmed_messages);
2661+
messages_to_render = &trimmed_messages;
2662+
}
2663+
params.messages = render_message_to_json(*messages_to_render, tmpl.original_caps());
26382664
params.tool_choice = inputs.tool_choice;
26392665
params.reasoning_format = inputs.reasoning_format;
26402666
params.enable_thinking = inputs.enable_thinking;

models/templates/stepfun-ai-Step-3.5-Flash.jinja

Lines changed: 0 additions & 80 deletions
This file was deleted.

tests/test-chat-auto-parser.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1887,7 +1887,6 @@ static void test_role_markers_all_templates(testing & t) {
18871887
{ "Qwen-Qwen3-0.6B.jinja", "<|im_start|>user", "<|im_start|>assistant" },
18881888
{ "Qwen-QwQ-32B.jinja", "<|im_start|>user", "<|im_start|>assistant" },
18891889
{ "StepFun3.5-Flash.jinja", "<|im_start|>user", "<|im_start|>assistant" },
1890-
{ "stepfun-ai-Step-3.5-Flash.jinja", "<|im_start|>user", "<|im_start|>assistant" },
18911890

18921891
// DeepSeek family
18931892
{ "deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja", "<|User|>", "<|Assistant|>" },

tests/test-chat.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3155,6 +3155,59 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
31553155
}
31563156
}
31573157
}
3158+
3159+
{
3160+
// StepFun trimming regression test (see https://github.com/ggml-org/llama.cpp/pull/25238)
3161+
auto tmpls = read_templates("models/templates/StepFun3.5-Flash.jinja");
3162+
3163+
common_chat_msg message_chatbot = simple_assist_msg("Let me check.\n\n", "I am thinking.\n\n");
3164+
3165+
{
3166+
common_chat_templates_inputs inputs;
3167+
inputs.messages = { message_chatbot };
3168+
inputs.add_generation_prompt = true;
3169+
3170+
auto params = common_chat_templates_apply(tmpls.get(), inputs);
3171+
3172+
if (params.prompt.find("Let me check.\n\n") != std::string::npos) {
3173+
throw std::runtime_error("StepFun 3.5: content not trimmed");
3174+
}
3175+
3176+
if (params.prompt.find("I am thinking.\n\n") != std::string::npos) {
3177+
throw std::runtime_error("StepFun 3.5: reasoning_content not trimmed");
3178+
}
3179+
}
3180+
3181+
{
3182+
// Trimming must also reach typed (text) content parts, not just string content
3183+
// (see https://github.com/ggml-org/llama.cpp/pull/25238)
3184+
common_chat_msg message_parts;
3185+
message_parts.role = "user";
3186+
message_parts.content_parts = {
3187+
{ /* .type = */ "text", /* .text = */ "First part.\n\n" },
3188+
{ /* .type = */ "media_marker", /* .text = */ "<__media__>" },
3189+
{ /* .type = */ "text", /* .text = */ "Second part.\n\n" },
3190+
};
3191+
3192+
common_chat_templates_inputs inputs;
3193+
inputs.messages = { message_parts };
3194+
inputs.add_generation_prompt = true;
3195+
3196+
auto params = common_chat_templates_apply(tmpls.get(), inputs);
3197+
3198+
if (params.prompt.find("First part.\n\n") != std::string::npos ||
3199+
params.prompt.find("Second part.\n\n") != std::string::npos) {
3200+
throw std::runtime_error("StepFun 3.5: text content parts not trimmed");
3201+
}
3202+
3203+
// the trimmed text itself must still be present
3204+
if (params.prompt.find("First part.") == std::string::npos ||
3205+
params.prompt.find("Second part.") == std::string::npos) {
3206+
throw std::runtime_error("StepFun 3.5: text content parts missing after trim");
3207+
}
3208+
}
3209+
}
3210+
31583211
}
31593212

31603213
{

0 commit comments

Comments
 (0)