for (const auto & [param_name, param_schema] : properties.items()) {
bool is_required = required.find(param_name) != required.end();
std::string type = "object";
- auto type_obj = param_schema.contains("type") ? param_schema.at("type") : json::object();
- if (type_obj.is_string()) {
- type_obj.get_to(type);
- } else if (type_obj.is_object()) {
- if (type_obj.contains("type") && type_obj.at("type").is_string()) {
- type_obj.at("type").get_to(type);
+ if (param_schema.contains("type")) {
+ const auto & type_obj = param_schema.at("type");
+ if (type_obj.is_string()) {
+ type_obj.get_to(type);
+ } else if (type_obj.is_array()) {
+ // Handle nullable types like ["string", "null"]
+ for (const auto & t : type_obj) {
+ if (t.is_string() && t.get<std::string>() != "null") {
+ type = t.get<std::string>();
+ break;
+ }
+ }
+ } else if (type_obj.is_object()) {
+ if (type_obj.contains("type") && type_obj.at("type").is_string()) {
+ type_obj.at("type").get_to(type);
+ }
+ }
+ }
+ // Infer string type from enum values when type is unspecified
+ if (type == "object" && param_schema.contains("enum")) {
+ const auto & enum_vals = param_schema.at("enum");
+ if (enum_vals.is_array()) {
+ for (const auto & v : enum_vals) {
+ if (v.is_string()) {
+ type = "string";
+ break;
+ }
+ }
}
}
std::vector<arg_entry> arg_entries;
for (const auto & [param_name, param_schema] : properties.items()) {
- std::string type = "object";
- auto type_v = param_schema.contains("type") ? param_schema.at("type") : json::object();
- if (type_v.is_string()) type_v.get_to(type);
+ std::string type = "object";
+ if (param_schema.contains("type")) {
+ const auto & type_v = param_schema.at("type");
+ if (type_v.is_string()) {
+ type_v.get_to(type);
+ } else if (type_v.is_array()) {
+ // Handle nullable types like ["string", "null"]
+ for (const auto & t : type_v) {
+ if (t.is_string() && t.get<std::string>() != "null") {
+ type = t.get<std::string>();
+ break;
+ }
+ }
+ }
+ }
+ // Infer string type from enum values when type is unspecified
+ if (type == "object" && param_schema.contains("enum")) {
+ const auto & enum_vals = param_schema.at("enum");
+ if (enum_vals.is_array()) {
+ for (const auto & v : enum_vals) {
+ if (v.is_string()) {
+ type = "string";
+ break;
+ }
+ }
+ }
+ }
common_peg_parser value_parser = p.eps();
if (type == "string") {
if (!s.schema) {
return true;
}
- if (s.raw && s.schema->contains("type") && s.schema->at("type").is_string() && s.schema->at("type") == "string") {
+ if (s.raw && s.schema->contains("type")) {
+ const auto & type_val = s.schema->at("type");
+ if (type_val.is_string() && type_val == "string") {
+ return true;
+ }
+ // Handle nullable types like ["string", "null"] - delegate when the
+ // non-null type is string, since the tagged format uses raw text
+ if (type_val.is_array()) {
+ for (const auto & t : type_val) {
+ if (t.is_string() && t.get<std::string>() != "null") {
+ return t.get<std::string>() == "string";
+ }
+ }
+ }
+ }
+ // Delegate for enum schemas in raw mode - enum values are literal strings
+ if (s.raw && !s.schema->contains("type") && s.schema->contains("enum")) {
return true;
}
return false;
})",
};
+static common_chat_tool nullable_string_tool{
+ /* .name = */ "set_nullable_str",
+ /* .description = */ "Set a nullable string value",
+ /* .parameters = */ R"({
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": ["string", "null"],
+ "description": "A nullable string"
+ }
+ },
+ "required": ["name"]
+ })",
+};
+
+static common_chat_tool nullable_string_null_first_tool{
+ /* .name = */ "set_nullable_str_nf",
+ /* .description = */ "Set a nullable string value with null first in type array",
+ /* .parameters = */ R"({
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": ["null", "string"],
+ "description": "A nullable string with null first"
+ }
+ },
+ "required": ["name"]
+ })",
+};
+
+static common_chat_tool nullable_int_tool{
+ /* .name = */ "set_nullable_int",
+ /* .description = */ "Set a nullable integer value",
+ /* .parameters = */ R"({
+ "type": "object",
+ "properties": {
+ "count": {
+ "type": ["integer", "null"],
+ "description": "A nullable integer"
+ }
+ },
+ "required": ["count"]
+ })",
+};
+
+static common_chat_tool enum_no_type_tool{
+ /* .name = */ "set_unit",
+ /* .description = */ "Set a temperature unit",
+ /* .parameters = */ R"({
+ "type": "object",
+ "properties": {
+ "unit": {
+ "enum": ["celsius", "fahrenheit"],
+ "description": "Temperature unit"
+ }
+ },
+ "required": ["unit"]
+ })",
+};
+
static common_chat_tool string_param_tool{
/* .name = */ "string_param",
/* .description = */ "Tool with string parameter for testing",
}
})
.run();
+
}
{
})
.expect_reconstruction()
.run();
+
+ // nullable string type ["string", "null"]
+ tst.test(
+ "<tool_call>\n"
+ "<function=set_nullable_str>\n"
+ "<parameter=name>\nhello world\n</parameter>\n"
+ "</function>\n"
+ "</tool_call>")
+ .tools({ nullable_string_tool })
+ .expect_tool_calls({
+ { "set_nullable_str", R"({"name": "hello world"})", {} },
+ })
+ .run();
+
+ // nullable string with null first in type array ["null", "string"]
+ tst.test(
+ "<tool_call>\n"
+ "<function=set_nullable_str_nf>\n"
+ "<parameter=name>\nhello world\n</parameter>\n"
+ "</function>\n"
+ "</tool_call>")
+ .tools({ nullable_string_null_first_tool })
+ .expect_tool_calls({
+ { "set_nullable_str_nf", R"({"name": "hello world"})", {} },
+ })
+ .run();
+
+ // nullable integer type ["integer", "null"] - should use JSON value path, not string
+ tst.test(
+ "<tool_call>\n"
+ "<function=set_nullable_int>\n"
+ "<parameter=count>\n42\n</parameter>\n"
+ "</function>\n"
+ "</tool_call>")
+ .tools({ nullable_int_tool })
+ .expect_tool_calls({
+ { "set_nullable_int", R"({"count": 42})", {} },
+ })
+ .run();
+
+ // enum without explicit type key - should infer string from enum values
+ tst.test(
+ "<tool_call>\n"
+ "<function=set_unit>\n"
+ "<parameter=unit>\ncelsius\n</parameter>\n"
+ "</function>\n"
+ "</tool_call>")
+ .tools({ enum_no_type_tool })
+ .expect_tool_calls({
+ { "set_unit", R"({"unit": "celsius"})", {} },
+ })
+ .run();
}
{
auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V3.1.jinja", detailed_debug);