diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index a4f0751a0f..f84b6ba298 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -301,6 +301,25 @@ void common_chat_peg_unified_mapper::map(const common_peg_ast_node & node) { } else { buffer_needs_closing_quote = true; } + } else if (is_arg_string_value) { + // Schema declares this as string type but it parsed as non-string (e.g., number) + // Force treatment as string value - add opening quote and escape content + if (!current_tool->name.empty()) { + if (!needs_closing_quote) { + value_to_add = "\""; + needs_closing_quote = true; + } + } else { + if (!buffer_needs_closing_quote) { + value_to_add = "\""; + buffer_needs_closing_quote = true; + } + } + std::string escaped = json(value_content).dump(); + if (escaped.size() >= 2 && escaped.front() == '"' && escaped.back() == '"') { + escaped = escaped.substr(1, escaped.size() - 2); + } + value_to_add += escaped; } else { // For non-string values (number, bool, null, object, array), add raw value content // Using raw content instead of dump() ensures monotonicity for streaming diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 5db03cc1c0..18d8052a5d 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -321,6 +321,23 @@ static common_chat_tool edit_tool{ })", }; +static common_chat_tool magic_tool{ + /* .name = */ "magic", + /* .description = */ "Magic tool that takes a hash", + /* .parameters = */ R"({ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "ref": { + "type": "string" + } + }, + "required": ["name", "ref"] + })", +}; + static std::vector tools{ special_function_tool, special_function_tool_with_optional_param, python_tool, html_tool, todo_list }; @@ -2079,6 +2096,25 @@ static void test_template_output_peg_parsers(bool detailed_debug) { expect_reasoning("I was thinking"). expect_content("Now I'm not.") .run(); + + // Test that numeric-looking string values are coerced to strings per the schema + tst.test( + "Let me call the magic tool\n" + "\n" + "\n" + "\n" + "\nfooBar\n\n" + "\n5123123\n\n" + "\n" + "") + .enable_thinking(true) + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .tools({ magic_tool }) + .expect_reasoning("Let me call the magic tool") + .expect_tool_calls({ + { "magic", R"({"name": "fooBar", "ref": "5123123"})", {} }, + }) + .run(); } }