diff --git a/common/chat.cpp b/common/chat.cpp index ef151691c38..09dcf388eb9 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1762,7 +1762,13 @@ static common_chat_params common_chat_params_init_lfm2_5(const common_chat_templ ) ); - auto content = p.content(p.until_one_of({"<|tool_call_start|>", "["})); + // A bare '[' in ordinary content (a list, an index, a type hint, etc.) is not a tool call; + // treating it as one would fail the parse and surface as an HTTP 500. + std::vector content_stops = { "<|tool_call_start|>" }; + foreach_function(inputs.tools, [&](const json & tool) { + content_stops.push_back("[" + tool.at("function").at("name").get() + "("); + }); + auto content = p.content(p.until_one_of(content_stops)); auto maybe_start = p.optional(p.literal("<|tool_call_start|>")); return generation_prompt + reasoning + content + maybe_start + tool_calls + end; }); diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 30ea2c07213..fbb3a6f202f 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -4177,6 +4177,29 @@ static void test_template_output_peg_parsers(bool detailed_debug) { )) .run(); + // Bare-bracket content must not abort the request. LFM2.5 has no tool-call wrapper + // token; content must only stop at an actual "[{tool_name}(" prefix (matching the + // grammar triggers). A '[' that doesn't start a defined tool call (a list/index/ + // type hint) used to fail the final parse and surface as an HTTP 500. + tst.test("Here is a Python list: [1, 2, 3] and that is all.") + .tools({ special_function_tool }) + .expect_content("Here is a Python list: [1, 2, 3] and that is all.") + .run(); + + // The reported production shape: a code-mode reply that ends up as content + // containing a bracketed data structure. + tst.test("trades = [\n {\"timestamp\": \"09:30:00\", \"price\": 150.10, \"size\": 100}\n]") + .tools({ special_function_tool }) + .expect_content("trades = [\n {\"timestamp\": \"09:30:00\", \"price\": 150.10, \"size\": 100}\n]") + .run(); + + // Streaming: a bare '[' mid-content must not retract previously streamed content. + tst.test("Here is a Python list: [1, 2") + .tools({ special_function_tool }) + .is_partial(true) + .expect_content("Here is a Python list: [1, 2") + .run(); + // Partial tool call (streaming) tst.test("[special_function(arg1=") .tools({ special_function_tool })