diff --git a/ds4_server.c b/ds4_server.c
index ee008a163..a201d9eb9 100644
--- a/ds4_server.c
+++ b/ds4_server.c
@@ -729,6 +729,7 @@ typedef struct {
/* Distinguish the Responses hosted tool from a normal function that
* happens to be named "tool_search". */
bool responses_tool_search;
+ char *parameters;
char **prop;
int len;
int cap;
@@ -906,6 +907,7 @@ static void tool_schema_order_free(tool_schema_order *o) {
free(o->name);
free(o->wire_name);
free(o->namespace);
+ free(o->parameters);
for (int i = 0; i < o->len; i++) free(o->prop[i]);
free(o->prop);
memset(o, 0, sizeof(*o));
@@ -1687,7 +1689,8 @@ static void tool_schema_orders_add_json_wire(tool_schema_orders *orders,
goto done;
}
parse_schema_properties(schema, &order);
- free(schema);
+ free(order.parameters);
+ order.parameters = schema;
} else if (!json_skip_value(&p)) {
free(key);
goto done;
@@ -5740,11 +5743,138 @@ static void trim_const_span(const char **start, const char **end) {
while (*end > *start && isspace((unsigned char)(*end)[-1])) (*end)--;
}
+/* GLM's XML has no DSML string flag. Only use a JSON literal when the
+ * request's property type disambiguates it; "001" and "true" may be text.
+ * This is type recovery, not schema validation. Unknown schemas stay text. */
+static unsigned glm_type_mask(const char *type) {
+ if (!strcmp(type, "integer")) return 1;
+ if (!strcmp(type, "number")) return 1 | 2;
+ if (!strcmp(type, "boolean")) return 4;
+ if (!strcmp(type, "null")) return 8;
+ if (!strcmp(type, "array")) return 16;
+ if (!strcmp(type, "object")) return 32;
+ return 64; /* Strings and unknown types must not be coerced. */
+}
+
+static unsigned glm_property_types(const char *schema) {
+ json_args fields = {0};
+ unsigned types = 0;
+ if (!json_args_parse(schema, &fields)) return 64;
+ int i = json_args_find_unused(&fields, "type");
+ if (i >= 0) {
+ const json_arg *arg = &fields.v[i];
+ if (arg->is_string) {
+ types = glm_type_mask(arg->value);
+ } else {
+ const char *p = arg->value;
+ json_ws(&p);
+ if (*p == '[') {
+ p++;
+ for (;;) {
+ char *type = NULL;
+ if (!json_string(&p, &type)) { types = 64; break; }
+ types |= glm_type_mask(type);
+ free(type);
+ json_ws(&p);
+ if (*p != ',') break;
+ p++;
+ }
+ if (*p != ']') types = 64;
+ }
+ }
+ }
+ json_args_free(&fields);
+ return types ? types : 64;
+}
+
+/* The request reader intentionally accepts strtod extensions. Generated
+ * literals must additionally obey JSON's lexical rules before copying them
+ * verbatim into API arguments (including numbers inside arrays/objects). */
+static bool glm_json_literal_valid(const char *value) {
+ const char *end = value;
+ if (!json_skip_value(&end)) return false;
+ json_ws(&end);
+ if (*end) return false;
+ const char *p = value;
+ while (*p) {
+ json_ws(&p);
+ if (!*p) break;
+ if (*p == '"') {
+ const char *start = p;
+ char *text = NULL;
+ if (!json_string(&p, &text)) return false;
+ free(text);
+ for (; start < p; start++)
+ if ((unsigned char)*start < 0x20) return false;
+ } else if (strchr("{}[],:", *p)) {
+ p++;
+ } else if (!strncmp(p, "true", 4)) {
+ p += 4;
+ } else if (!strncmp(p, "false", 5)) {
+ p += 5;
+ } else if (!strncmp(p, "null", 4)) {
+ p += 4;
+ } else {
+ if (*p == '-') p++;
+ if (*p == '0') p++;
+ else {
+ if (*p < '1' || *p > '9') return false;
+ while (*p >= '0' && *p <= '9') p++;
+ }
+ if (*p == '.') {
+ p++;
+ if (*p < '0' || *p > '9') return false;
+ while (*p >= '0' && *p <= '9') p++;
+ }
+ if (*p == 'e' || *p == 'E') {
+ p++;
+ if (*p == '+' || *p == '-') p++;
+ if (*p < '0' || *p > '9') return false;
+ while (*p >= '0' && *p <= '9') p++;
+ }
+ if (*p && !strchr(" \t\r\n,]}", *p)) return false;
+ }
+ }
+ return true;
+}
+
+static bool glm_arg_is_json(json_args *properties, const char *key,
+ const char *value) {
+ int i = json_args_find_unused(properties, key);
+ if (i < 0 || properties->v[i].is_string) return false;
+ unsigned types = glm_property_types(properties->v[i].value);
+ if (types & 64 || !glm_json_literal_valid(value)) return false;
+ const char *p = value;
+ json_ws(&p);
+ unsigned kind = 0;
+ if (*p == '[') kind = 16;
+ else if (*p == '{') kind = 32;
+ else if (*p == 't' || *p == 'f') kind = 4;
+ else if (*p == 'n') kind = 8;
+ else if (*p == '-' || (*p >= '0' && *p <= '9'))
+ kind = strpbrk(p, ".eE") ? 2 : 1;
+ return (types & kind) != 0;
+}
+
+static void glm_tool_properties(const tool_schema_orders *orders,
+ const char *name, json_args *properties) {
+ const tool_schema_order *order = tool_schema_orders_find(orders, name);
+ if (!order || !order->parameters) return;
+ json_args schema = {0};
+ if (json_args_parse(order->parameters, &schema)) {
+ int i = json_args_find_unused(&schema, "properties");
+ if (i >= 0 && !schema.v[i].is_string)
+ json_args_parse(schema.v[i].value, properties);
+ }
+ json_args_free(&schema);
+}
+
static bool parse_glm_generated_message_ex(const char *text,
bool require_thinking_closed,
char **content_out,
char **reasoning_out,
- tool_calls *calls) {
+ tool_calls *calls,
+ const tool_schema_orders *orders) {
static const char tool_start[] = "";
static const char tool_end[] = "";
static const char arg_key_start[] = "";
@@ -5801,6 +5931,8 @@ static bool parse_glm_generated_message_ex(const char *text,
char *name = xstrndup(name_start, (size_t)(name_end - name_start));
p = name_end;
+ json_args properties = {0};
+ glm_tool_properties(orders, name, &properties);
buf args = {0};
for (;;) {
p = skip_ascii_ws(p);
@@ -5810,6 +5942,7 @@ static bool parse_glm_generated_message_ex(const char *text,
}
if (strncmp(p, arg_key_start, strlen(arg_key_start)) != 0) {
free(name);
+ json_args_free(&properties);
buf_free(&args);
return false;
}
@@ -5817,6 +5950,7 @@ static bool parse_glm_generated_message_ex(const char *text,
const char *key_end = strstr(p, arg_key_end);
if (!key_end || key_end > close) {
free(name);
+ json_args_free(&properties);
buf_free(&args);
return false;
}
@@ -5833,6 +5967,7 @@ static bool parse_glm_generated_message_ex(const char *text,
if (strncmp(p, arg_value_start, strlen(arg_value_start)) != 0) {
free(name);
free(key);
+ json_args_free(&properties);
buf_free(&args);
return false;
}
@@ -5841,19 +5976,22 @@ static bool parse_glm_generated_message_ex(const char *text,
if (!value_end || value_end > close) {
free(name);
free(key);
+ json_args_free(&properties);
buf_free(&args);
return false;
}
char *raw_value = xstrndup(p, (size_t)(value_end - p));
char *value = xstrdup(raw_value);
ds4_tool_text_unescape(value, arg_value_end);
- tool_call_json_args_add(&args, key, value, "true");
+ tool_call_json_args_add(&args, key, value,
+ glm_arg_is_json(&properties, key, value) ? "false" : "true");
free(key);
free(raw_value);
free(value);
p = value_end + strlen(arg_value_end);
}
+ json_args_free(&properties);
tool_call tc = {0};
tc.name = name;
buf wrapped = {0};
@@ -5889,11 +6027,12 @@ static bool parse_generated_message_ex_for_syntax(server_model_syntax syntax,
bool require_thinking_closed,
char **content_out,
char **reasoning_out,
- tool_calls *calls) {
+ tool_calls *calls,
+ const tool_schema_orders *orders) {
if (syntax == SERVER_MODEL_SYNTAX_GLM) {
return parse_glm_generated_message_ex(text, require_thinking_closed,
content_out, reasoning_out,
- calls);
+ calls, orders);
}
return parse_deepseek_generated_message_ex(text, require_thinking_closed,
content_out, reasoning_out,
@@ -5911,7 +6050,7 @@ static DS4_SERVER_MAYBE_UNUSED bool parse_generated_message_ex(
require_thinking_closed,
content_out,
reasoning_out,
- calls);
+ calls, NULL);
}
/* Try to repair a truncated DSML block.
@@ -5998,7 +6137,8 @@ static bool parse_generated_message_for_response_for_syntax(server_model_syntax
char **content_out,
char **reasoning_out,
tool_calls *calls,
- bool *recovered_out) {
+ bool *recovered_out,
+ const tool_schema_orders *orders) {
if (recovered_out) *recovered_out = false;
bool parsed_ok = parse_generated_message_ex_for_syntax(syntax,
@@ -6006,7 +6146,7 @@ static bool parse_generated_message_for_response_for_syntax(server_model_syntax
require_thinking_closed,
content_out,
reasoning_out,
- calls);
+ calls, orders);
if (parsed_ok) return true;
free(*content_out);
@@ -6044,7 +6184,7 @@ static DS4_SERVER_MAYBE_UNUSED bool parse_generated_message_for_response(
return parse_generated_message_for_response_for_syntax(
SERVER_MODEL_SYNTAX_DEEPSEEK, text, has_tools, saw_tool_start,
require_thinking_closed, finish_io, err, errlen, content_out,
- reasoning_out, calls, recovered_out);
+ reasoning_out, calls, recovered_out, NULL);
}
static void append_json_object_string(buf *b, const char *json) {
@@ -11278,7 +11418,7 @@ static bool complete_tool_call_inside_thinking(server_model_syntax syntax,
char *content = NULL, *reasoning = NULL;
tool_calls calls = {0};
bool complete = parse_generated_message_ex_for_syntax(syntax, start, false,
- &content, &reasoning, &calls) && calls.len > 0;
+ &content, &reasoning, &calls, NULL) && calls.len > 0;
free(content);
free(reasoning);
tool_calls_free(&calls);
@@ -13135,7 +13275,7 @@ static void generate_job_inner(server *s, server_slot *slot, job *j) {
char *test_reasoning = NULL;
bool repair_ok = parse_generated_message_ex_for_syntax(
j->req.model_syntax, repaired.ptr, false,
- &test_content, &test_reasoning, &test_calls);
+ &test_content, &test_reasoning, &test_calls, NULL);
free(test_content);
free(test_reasoning);
if (repair_ok && test_calls.len > 0) {
@@ -13248,7 +13388,7 @@ static void generate_job_inner(server *s, server_slot *slot, job *j) {
&parsed_content,
&parsed_reasoning,
&parsed_calls,
- &recovered_tool_parse_failure);
+ &recovered_tool_parse_failure, &j->req.tool_orders);
if (!parsed_ok && recovered_tool_parse_failure && j->req.has_tools && saw_tool_start) {
/* parse_generated_message failed even though DSML was present.
* Semantic repair is intentionally avoided: if the parser cannot
@@ -15941,7 +16081,9 @@ static void test_openai_glm_tool_stream_suppresses_raw_tool_call(void) {
r.think_mode = DS4_THINK_NONE;
r.has_tools = true;
r.model_syntax = SERVER_MODEL_SYNTAX_GLM;
- r.tool_orders = make_bash_order();
+ tool_schema_orders_add_json(&r.tool_orders,
+ "{\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"properties\":{"
+ "\"command\":{\"type\":\"string\"},\"timeout\":{\"type\":\"integer\"}}}}");
TEST_ASSERT(sse_chunk(sv[0], &r, "chatcmpl_glm_tool", NULL, NULL));
@@ -15951,6 +16093,7 @@ static void test_openai_glm_tool_stream_suppresses_raw_tool_call(void) {
"Before.\n\n"
"bash"
"commandpwd"
+ "timeout10"
"";
TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_glm_tool", &st,
raw, strlen(raw), false));
@@ -15960,8 +16103,9 @@ static void test_openai_glm_tool_stream_suppresses_raw_tool_call(void) {
tool_calls calls = {0};
TEST_ASSERT(parse_generated_message_ex_for_syntax(
SERVER_MODEL_SYNTAX_GLM, raw, false,
- &parsed_content, &parsed_reasoning, &calls));
+ &parsed_content, &parsed_reasoning, &calls, &r.tool_orders));
TEST_ASSERT(calls.len == 1);
+ TEST_ASSERT(strstr(calls.v[0].arguments, "\"timeout\": 10") != NULL);
TEST_ASSERT(openai_sse_finish_live(sv[0], NULL, &r, "chatcmpl_glm_tool", &st,
raw, strlen(raw), &calls,
"tool_calls", 10, 4));
@@ -16736,6 +16880,75 @@ static void test_parse_short_dsml_and_canonical_suffix(void) {
request_free(&r);
}
+static void test_glm_tool_argument_types(void) {
+ struct { const char *schema, *value, *expected; } cases[] = {
+ {"{\"type\":\"integer\"}", "40", "40"},
+ {"{\"type\":\"integer\"}", "-40", "-40"},
+ {"{\"type\":\"integer\"}", "9007199254740993", "9007199254740993"},
+ {"{\"type\":\"number\"}", " -1.25e+3 ", "-1.25e+3"},
+ {"{\"type\":\"boolean\"}", "true", "true"},
+ {"{\"type\":\"boolean\"}", "false", "false"},
+ {"{\"type\":\"null\"}", "null", "null"},
+ {"{\"type\":[\"integer\",\"null\"]}", "40", "40"},
+ {"{\"type\":[\"integer\",\"null\"]}", "null", "null"},
+ {"{\"type\":\"array\"}", "[1, true, {\"x\": null}]", "[1,true,{\"x\":null}]"},
+ {"{\"type\":\"object\"}", "{\"x\": [2, false]}", "{\"x\":[2,false]}"},
+ {"{\"type\":\"string\"}", "001", "\"001\""},
+ {"{\"type\":\"string\"}", "40", "\"40\""},
+ {"{\"type\":\"string\"}", "true", "\"true\""},
+ {"{\"type\":\"string\"}", "null", "\"null\""},
+ {"{\"type\":\"string\"}", "[1,2]", "\"[1,2]\""},
+ {"{\"type\":\"string\"}", " x\ny ", "\" x\\ny \""},
+ {"{\"type\":[\"string\",\"integer\"]}", "40", "\"40\""},
+ {"{}", "40", "\"40\""},
+ {"{\"$ref\":\"#/$defs/value\"}", "40", "\"40\""},
+ {"{\"type\":\"integer\"}", "001", "\"001\""},
+ {"{\"type\":\"integer\"}", "1.5", "\"1.5\""},
+ {"{\"type\":\"number\"}", "+1", "\"+1\""},
+ {"{\"type\":\"number\"}", "0x10", "\"0x10\""},
+ {"{\"type\":\"number\"}", "NaN", "\"NaN\""},
+ {"{\"type\":\"number\"}", "Infinity", "\"Infinity\""},
+ {"{\"type\":\"number\"}", "1.", "\"1.\""},
+ {"{\"type\":\"number\"}", "1e", "\"1e\""},
+ {"{\"type\":\"boolean\"}", "True", "\"True\""},
+ {"{\"type\":\"boolean\"}", "1", "\"1\""},
+ {"{\"type\":\"array\"}", "[1,]", "\"[1,]\""},
+ {"{\"type\":\"array\"}", "[NaN]", "\"[NaN]\""},
+ {"{\"type\":\"array\"}", "[01]", "\"[01]\""},
+ {"{\"type\":\"array\"}", "[] trailing", "\"[] trailing\""},
+ {"{\"type\":\"object\"}", "{\"x\":Infinity}", "\"{\\\"x\\\":Infinity}\""},
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+ tool_schema_orders orders = {0};
+ buf schema = {0}, raw = {0}, expected = {0};
+ buf_printf(&schema, "{\"name\":\"report\",\"parameters\":{\"type\":\"object\","
+ "\"properties\":{\"value\":%s}}}", cases[i].schema);
+ tool_schema_orders_add_json(&orders, schema.ptr);
+ buf_printf(&raw, "reportvalue"
+ "%s", cases[i].value);
+ buf_printf(&expected, "{\"value\": %s}", cases[i].expected);
+ char *content = NULL, *reasoning = NULL;
+ tool_calls calls = {0};
+ const char *finish = "tool_calls";
+ bool recovered = false;
+ TEST_ASSERT(parse_generated_message_for_response_for_syntax(
+ SERVER_MODEL_SYNTAX_GLM, raw.ptr, true, true, false, &finish,
+ NULL, 0, &content, &reasoning, &calls, &recovered, &orders));
+ TEST_ASSERT(calls.len == 1);
+ if (calls.len == 1) {
+ TEST_ASSERT(!strcmp(calls.v[0].arguments, expected.ptr));
+ TEST_ASSERT(!strcmp(calls.raw_tool_text, raw.ptr));
+ }
+ free(content);
+ free(reasoning);
+ tool_calls_free(&calls);
+ tool_schema_orders_free(&orders);
+ buf_free(&schema);
+ buf_free(&raw);
+ buf_free(&expected);
+ }
+}
+
static void test_parse_glm_tool_call_message(void) {
const char *generated =
"need bashOK\n\n"
@@ -16749,7 +16962,7 @@ static void test_parse_glm_tool_call_message(void) {
TEST_ASSERT(parse_generated_message_ex_for_syntax(
SERVER_MODEL_SYNTAX_GLM, generated, true,
- &content, &reasoning, &calls));
+ &content, &reasoning, &calls, NULL));
TEST_ASSERT(reasoning && !strcmp(reasoning, "need bash"));
TEST_ASSERT(content && !strcmp(content, "OK"));
TEST_ASSERT(calls.len == 1);
@@ -17104,7 +17317,7 @@ static void test_incomplete_tool_call_keeps_stop_reason(void) {
TEST_ASSERT(!parse_generated_message_for_response_for_syntax(
glm ? SERVER_MODEL_SYNTAX_GLM : SERVER_MODEL_SYNTAX_DEEPSEEK,
raw[glm], true, true, false, &finish, err, sizeof(err),
- &content, &reasoning, &calls, &recovered));
+ &content, &reasoning, &calls, &recovered, NULL));
TEST_ASSERT(!strcmp(finish, reasons[i]));
TEST_ASSERT(recovered && calls.len == 0);
TEST_ASSERT(content && !strcmp(content, raw[glm]));
@@ -17263,7 +17476,7 @@ static void test_glm_tool_checkpoint_suffix_is_canonical(void) {
tool_calls calls = {0};
TEST_ASSERT(parse_generated_message_ex_for_syntax(
SERVER_MODEL_SYNTAX_GLM, generated, false,
- &content, &reasoning, &calls));
+ &content, &reasoning, &calls, NULL));
TEST_ASSERT(calls.len == 1);
request r;
@@ -18103,7 +18316,7 @@ static void test_tool_control_text_inside_arguments(void) {
tool_calls calls = {0};
char *content = NULL, *reasoning = NULL;
TEST_ASSERT(parse_generated_message_ex_for_syntax(tracker.model_syntax,
- raw.ptr, true, &content, &reasoning, &calls));
+ raw.ptr, true, &content, &reasoning, &calls, NULL));
TEST_ASSERT(calls.len == 1);
TEST_ASSERT(reasoning && !strcmp(reasoning, "reason"));
if (calls.len) {
@@ -18157,7 +18370,7 @@ static void test_tool_body_escape_round_trip(void) {
append_tool_calls_text_for_syntax(&raw, syntax, &original, NULL);
char *content = NULL, *reasoning = NULL;
TEST_ASSERT(parse_generated_message_ex_for_syntax(syntax, raw.ptr, false,
- &content, &reasoning, &parsed));
+ &content, &reasoning, &parsed, NULL));
TEST_ASSERT(parsed.len == 1);
if (parsed.len) {
json_args args = {0};
@@ -20223,6 +20436,7 @@ static void ds4_server_unit_tests_run(void) {
test_openai_tool_stream_handles_multiple_calls();
test_streaming_holds_partial_utf8();
test_parse_short_dsml_and_canonical_suffix();
+ test_glm_tool_argument_types();
test_parse_glm_tool_call_message();
test_dsml_parser_recovers_loose_nested_parameters();
test_dsml_repair_produces_parseable_calls();
diff --git a/tests/ds4_test.c b/tests/ds4_test.c
index adea6aeba..517b8f488 100644
--- a/tests/ds4_test.c
+++ b/tests/ds4_test.c
@@ -6464,7 +6464,7 @@ static bool test_generate_chat_turn(ds4_engine *engine, ds4_session *session,
&turn->content,
&turn->reasoning,
&turn->calls,
- &recovered);
+ &recovered, &r->tool_orders);
if (turn->calls.len > 0) turn->finish = "tool_calls";
if (!parsed) {
fprintf(stderr,