diff --git a/common/chat-peg-parser.h b/common/chat-peg-parser.h index 7304ca7e61..f5d49a403a 100644 --- a/common/chat-peg-parser.h +++ b/common/chat-peg-parser.h @@ -77,7 +77,7 @@ class common_chat_peg_unified_builder : public common_chat_peg_builder { // Use for schema-declared string types - won't be treated as potential JSON container common_peg_parser tool_arg_string_value(const common_peg_parser & p) { return tag(TOOL_ARG_STRING_VALUE, p); } - common_peg_parser tool_arg_json_value(const common_peg_parser & p) { return tag(TOOL_ARG_VALUE, p); } + common_peg_parser tool_arg_json_value(const common_peg_parser & p) { return atomic(tag(TOOL_ARG_VALUE, p)); } // Legacy-compatible helper for building standard JSON tool calls // Used by tests and manual parsers diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 7a4c1cc398..f1b10b21a5 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -1307,7 +1307,11 @@ common_peg_parser common_peg_parser_builder::json_number() { auto int_part = choice({ literal("0"), sequence({ digit1_9, chars("[0-9]", 0, -1) }) }); auto frac = sequence({ literal("."), digits }); auto exp = sequence({ choice({ literal("e"), literal("E") }), optional(chars("[+-]", 1, 1)), digits }); - return sequence({ optional(literal("-")), int_part, optional(frac), optional(exp), space() }); + // Negative lookahead: only commit the number when the next character can't extend it. + // At EOF in partial mode, chars returns NEED_MORE → negate propagates NEED_MORE → number not committed. + // This prevents premature commits of partial numbers (e.g. "3" when "3.14" is incoming). + auto not_number_continuation = negate(chars("[0-9.eE+-]", 1, 1)); + return sequence({ optional(literal("-")), int_part, optional(frac), optional(exp), not_number_continuation, space() }); }); } diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index f55ab398bd..e64e362129 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -355,6 +355,21 @@ static common_chat_tool magic_int_tool{ })", }; +static common_chat_tool amount_tool{ + /* .name = */ "amount", + /* .description = */ "Amount converter", + /* .parameters = */ R"({ + "type": "object", + "properties": { + "orig": { + "type": "number" + } + }, + "required": ["orig"] + })", +}; + + static common_chat_tool string_param_tool{ /* .name = */ "string_param", /* .description = */ "Tool with string parameter for testing", @@ -2274,6 +2289,40 @@ static void test_template_output_peg_parsers(bool detailed_debug) { }) .run(); + tst.test( + "Test negative number\n" + "\n" + "\n" + "\n" + "\n-14\n\n" + "\n" + "") + .enable_thinking(true) + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .tools({ magic_int_tool }) + .expect_reasoning("Test negative number") + .expect_tool_calls({ + { "magic_int", R"({ "ref" : -14 })", {} } + }) + .run(); + + tst.test( + "Test decimal number\n" + "\n" + "\n" + "\n" + "\n3.14\n\n" + "\n" + "") + .enable_thinking(true) + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .tools({ amount_tool }) + .expect_reasoning("Test decimal number") + .expect_tool_calls({ + { "amount", R"({ "orig" : 3.14 })", {} } + }) + .run(); + } }