diff --git a/ds4_server.c b/ds4_server.c
index 8968eca79..6306f0936 100644
--- a/ds4_server.c
+++ b/ds4_server.c
@@ -559,6 +559,10 @@ typedef struct {
* happens to be named "tool_search". */
bool responses_tool_search;
char **prop;
+ /* Declared JSON schema "type" of each property (parallel to prop), or NULL
+ * when the schema does not declare a plain string type. Used to restore
+ * argument types for tool-call wire formats that are untyped (GLM). */
+ char **prop_type;
int len;
int cap;
} tool_schema_order;
@@ -725,6 +729,8 @@ static void tool_schema_order_free(tool_schema_order *o) {
free(o->namespace);
for (int i = 0; i < o->len; i++) free(o->prop[i]);
free(o->prop);
+ for (int i = 0; i < o->len; i++) free(o->prop_type ? o->prop_type[i] : NULL);
+ free(o->prop_type);
memset(o, 0, sizeof(*o));
}
@@ -734,12 +740,23 @@ static void tool_schema_orders_free(tool_schema_orders *orders) {
memset(orders, 0, sizeof(*orders));
}
-static void tool_schema_order_prop_push(tool_schema_order *o, char *prop) {
+static void tool_schema_order_prop_push(tool_schema_order *o, char *prop, char *prop_type) {
if (o->len == o->cap) {
o->cap = o->cap ? o->cap * 2 : 8;
o->prop = xrealloc(o->prop, (size_t)o->cap * sizeof(o->prop[0]));
+ o->prop_type = xrealloc(o->prop_type, (size_t)o->cap * sizeof(o->prop_type[0]));
}
- o->prop[o->len++] = prop;
+ o->prop[o->len] = prop;
+ o->prop_type[o->len] = prop_type;
+ o->len++;
+}
+
+static const char *tool_schema_order_prop_type(const tool_schema_order *o, const char *key) {
+ if (!o || !o->prop_type || !key) return NULL;
+ for (int i = 0; i < o->len; i++) {
+ if (o->prop[i] && !strcmp(o->prop[i], key)) return o->prop_type[i];
+ }
+ return NULL;
}
static int tool_schema_orders_find_index(const tool_schema_orders *orders, const char *name) {
@@ -1400,6 +1417,41 @@ static char *responses_namespace_function_schema_from_tool(const char *raw,
return out;
}
+/* Extract the top-level "type" string from a property schema object, e.g.
+ * {"type":"number","description":...} -> "number". Returns NULL when the
+ * value is not an object or "type" is absent or not a plain string (union
+ * types stay untyped, which keeps the current stringly behavior). */
+static char *json_schema_value_type(const char *value) {
+ const char *p = value ? value : "";
+ json_ws(&p);
+ if (*p != '{') return NULL;
+ p++;
+ json_ws(&p);
+ while (*p && *p != '}') {
+ char *key = NULL;
+ if (!json_string(&p, &key)) return NULL;
+ json_ws(&p);
+ if (*p != ':') {
+ free(key);
+ return NULL;
+ }
+ p++;
+ if (!strcmp(key, "type")) {
+ free(key);
+ char *type = NULL;
+ json_ws(&p);
+ if (*p != '"' || !json_string(&p, &type)) return NULL;
+ return type;
+ }
+ free(key);
+ if (!json_skip_value(&p)) return NULL;
+ json_ws(&p);
+ if (*p == ',') p++;
+ json_ws(&p);
+ }
+ return NULL;
+}
+
static bool parse_schema_properties(const char *json, tool_schema_order *order) {
const char *p = json;
json_ws(&p);
@@ -1430,8 +1482,15 @@ static bool parse_schema_properties(const char *json, tool_schema_order *order)
return false;
}
p++;
- tool_schema_order_prop_push(order, prop);
- if (!json_skip_value(&p)) return false;
+ const char *value_start = p;
+ if (!json_skip_value(&p)) {
+ tool_schema_order_prop_push(order, prop, NULL);
+ return false;
+ }
+ char *raw_value = xstrndup(value_start, (size_t)(p - value_start));
+ tool_schema_order_prop_push(order, prop,
+ json_schema_value_type(raw_value));
+ free(raw_value);
json_ws(&p);
if (*p == ',') p++;
json_ws(&p);
@@ -2184,6 +2243,50 @@ static bool json_args_parse(const char *json, json_args *args) {
return true;
}
+/* Return true when `text` is exactly one valid JSON value whose kind matches
+ * the declared schema type. */
+static bool json_text_matches_schema_type(const char *text, const char *type) {
+ const char *p = text ? text : "";
+ json_ws(&p);
+ char c = *p;
+ bool kind_ok = false;
+ if (!strcmp(type, "number") || !strcmp(type, "integer")) {
+ kind_ok = (c == '-' || (c >= '0' && c <= '9'));
+ } else if (!strcmp(type, "boolean")) {
+ kind_ok = (c == 't' || c == 'f');
+ } else if (!strcmp(type, "array")) {
+ kind_ok = (c == '[');
+ } else if (!strcmp(type, "object")) {
+ kind_ok = (c == '{');
+ } else if (!strcmp(type, "null")) {
+ kind_ok = (c == 'n');
+ }
+ if (!kind_ok) return false;
+ if (!json_skip_value(&p)) return false;
+ json_ws(&p);
+ return *p == '\0';
+}
+
+/* GLM's tool-call wire format (kv)
+ * carries no type information, so the parser stores every argument as a JSON
+ * string. Before serializing arguments back to the client, restore the types
+ * the client declared in the tool schema: a string value whose declared type
+ * is non-string and whose text is valid JSON of that type is emitted raw.
+ * Values that don't cleanly match their declared type are left untouched. */
+static void json_args_restore_schema_types(json_args *args,
+ const tool_schema_order *order) {
+ if (!order || !order->prop_type) return;
+ for (int i = 0; i < args->len; i++) {
+ json_arg *arg = &args->v[i];
+ if (!arg->is_string || !arg->value) continue;
+ const char *type = tool_schema_order_prop_type(order, arg->key);
+ if (!type || !strcmp(type, "string")) continue;
+ if (json_text_matches_schema_type(arg->value, type)) {
+ arg->is_string = false;
+ }
+ }
+}
+
static void append_dsml_attr_escaped(buf *b, const char *s) {
for (s = s ? s : ""; *s; s++) {
if (*s == '&') buf_puts(b, "&");
@@ -2326,12 +2429,14 @@ static void append_json_arg_pair(buf *b, const json_arg *arg) {
else buf_puts(b, arg->value);
}
-static void append_json_object_or_empty(buf *b, const char *json) {
+static void append_json_object_or_empty_typed(buf *b, const char *json,
+ const tool_schema_order *order) {
json_args args = {0};
if (!json_args_parse(json, &args)) {
buf_puts(b, "{}");
return;
}
+ json_args_restore_schema_types(&args, order);
buf_putc(b, '{');
bool wrote = false;
for (int i = 0; i < args.len; i++) {
@@ -5241,16 +5346,16 @@ static DS4_SERVER_MAYBE_UNUSED bool parse_generated_message_for_response(
reasoning_out, calls, recovered_out);
}
-static void append_json_object_string(buf *b, const char *json) {
+static void append_json_object_string(buf *b, const char *json,
+ const tool_schema_order *order) {
buf tmp = {0};
- append_json_object_or_empty(&tmp, json);
+ append_json_object_or_empty_typed(&tmp, json, order);
json_escape(b, tmp.ptr ? tmp.ptr : "{}");
buf_free(&tmp);
}
static void append_tool_calls_json(buf *b, const tool_calls *calls, const char *id_prefix,
const tool_schema_orders *orders) {
- (void)orders;
buf_putc(b, '[');
for (int i = 0; i < calls->len; i++) {
const tool_call *tc = &calls->v[i];
@@ -5262,7 +5367,8 @@ static void append_tool_calls_json(buf *b, const tool_calls *calls, const char *
buf_puts(b, ",\"type\":\"function\",\"function\":{\"name\":");
json_escape(b, tc->name ? tc->name : "");
buf_puts(b, ",\"arguments\":");
- append_json_object_string(b, tc->arguments);
+ append_json_object_string(b, tc->arguments,
+ tool_schema_orders_find(orders, tc->name));
buf_puts(b, "}}");
}
buf_putc(b, ']');
@@ -5270,7 +5376,6 @@ static void append_tool_calls_json(buf *b, const tool_calls *calls, const char *
static void append_tool_call_deltas_json(buf *b, const tool_calls *calls, const char *id_prefix,
const tool_schema_orders *orders) {
- (void)orders;
buf_putc(b, '[');
for (int i = 0; i < calls->len; i++) {
const tool_call *tc = &calls->v[i];
@@ -5284,7 +5389,8 @@ static void append_tool_call_deltas_json(buf *b, const tool_calls *calls, const
buf_puts(b, ",\"type\":\"function\",\"function\":{\"name\":");
json_escape(b, tc->name ? tc->name : "");
buf_puts(b, ",\"arguments\":");
- append_json_object_string(b, tc->arguments);
+ append_json_object_string(b, tc->arguments,
+ tool_schema_orders_find(orders, tc->name));
buf_puts(b, "}}");
}
buf_putc(b, ']');
@@ -6845,7 +6951,7 @@ static void responses_append_function_call_item(buf *b, const tool_call *tc,
"{\"id\":\"%s\",\"type\":\"tool_search_call\",\"status\":\"%s\","
"\"call_id\":\"%s\",\"execution\":\"client\",\"arguments\":",
item->fc_id, item_status, item->call_id);
- if (with_args) append_json_object_or_empty(b, tc->arguments);
+ if (with_args) append_json_object_or_empty_typed(b, tc->arguments, order);
else buf_puts(b, "{}");
buf_putc(b, '}');
return;
@@ -6870,7 +6976,7 @@ static void responses_append_function_call_item(buf *b, const tool_call *tc,
} else if (item->is_custom) {
json_escape(b, tc->arguments ? tc->arguments : "");
} else {
- append_json_object_string(b, tc->arguments);
+ append_json_object_string(b, tc->arguments, order);
}
buf_putc(b, '}');
}
@@ -6909,7 +7015,7 @@ static bool responses_sse_function_call_arguments_done(int fd, responses_stream
const tool_schema_order *order = tool_schema_orders_find(orders, tc->name);
if (item->is_custom || responses_tool_call_is_tool_search(tc, order)) return true;
buf args = {0};
- append_json_object_string(&args, tc->arguments);
+ append_json_object_string(&args, tc->arguments, order);
buf b = {0};
buf_printf(&b,
"{\"type\":\"response.function_call_arguments.delta\","
@@ -7340,7 +7446,6 @@ static const char *anthropic_stop_reason(const char *finish) {
static void append_anthropic_tool_use(buf *b, const tool_call *tc, const char *id_prefix, int i,
const tool_schema_orders *orders) {
- (void)orders;
char idbuf[128];
snprintf(idbuf, sizeof(idbuf), "toolu_%s_%d", id_prefix, i);
buf_puts(b, "{\"type\":\"tool_use\",\"id\":");
@@ -7348,7 +7453,8 @@ static void append_anthropic_tool_use(buf *b, const tool_call *tc, const char *i
buf_puts(b, ",\"name\":");
json_escape(b, tc->name ? tc->name : "");
buf_puts(b, ",\"input\":");
- append_json_object_or_empty(b, tc->arguments);
+ append_json_object_or_empty_typed(b, tc->arguments,
+ tool_schema_orders_find(orders, tc->name));
buf_putc(b, '}');
}
@@ -8032,7 +8138,6 @@ static bool anthropic_sse_stream_update(int fd, server *s, const request *r, con
static bool anthropic_sse_tool_blocks_live(int fd, const request *r, const char *id,
anthropic_stream *st,
const tool_calls *calls) {
- (void)r;
if (!calls) return true;
buf b = {0};
@@ -8062,7 +8167,8 @@ static bool anthropic_sse_tool_blocks_live(int fd, const request *r, const char
"{\"type\":\"content_block_delta\",\"index\":%d,"
"\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":",
st->next_index);
- append_json_object_string(&b, tc->arguments);
+ append_json_object_string(&b, tc->arguments,
+ tool_schema_orders_find(&r->tool_orders, tc->name));
buf_puts(&b, "}}");
ok = sse_event(fd, "content_block_delta", b.ptr);
buf_free(&b);
@@ -13914,6 +14020,55 @@ static void test_openai_tool_args_preserve_call_order(void) {
request_free(&r);
}
+/* GLM's untyped tool-call wire format makes the parser store every argument
+ * as a JSON string; serialization must restore the client-declared schema
+ * types (see json_args_restore_schema_types). */
+static void test_openai_tool_args_restore_schema_types(void) {
+ request r;
+ request_init(&r, REQ_CHAT, 128);
+ tool_schema_orders_add_json(&r.tool_orders,
+ "{\"name\":\"web_search\",\"input_schema\":{\"type\":\"object\",\"properties\":{"
+ "\"query\":{\"type\":\"string\"},"
+ "\"max_results\":{\"type\":\"number\"},"
+ "\"deep\":{\"type\":\"boolean\"},"
+ "\"exclude_domains\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}}}}");
+
+ tool_calls calls = {0};
+ tool_call tc = {0};
+ tc.name = xstrdup("web_search");
+ /* What the GLM parser produces today: every value is a string. */
+ tc.arguments = xstrdup("{\"query\":\"chips 2026\",\"max_results\":\"10\","
+ "\"deep\":\"true\",\"exclude_domains\":\"[\\\"x.com\\\"]\","
+ "\"unknown\":\"7\"}");
+ tool_calls_push(&calls, tc);
+
+ buf b = {0};
+ append_tool_calls_json(&b, &calls, "test", &r.tool_orders);
+ /* Declared non-string types are restored... */
+ TEST_ASSERT(strstr(b.ptr, "\\\"max_results\\\":10") != NULL);
+ TEST_ASSERT(strstr(b.ptr, "\\\"deep\\\":true") != NULL);
+ TEST_ASSERT(strstr(b.ptr, "\\\"exclude_domains\\\":[\\\"x.com\\\"]") != NULL);
+ /* ...while strings and undeclared properties stay strings. */
+ TEST_ASSERT(strstr(b.ptr, "\\\"query\\\":\\\"chips 2026\\\"") != NULL);
+ TEST_ASSERT(strstr(b.ptr, "\\\"unknown\\\":\\\"7\\\"") != NULL);
+ buf_free(&b);
+
+ /* A value that is not valid JSON of the declared type stays a string. */
+ tool_calls calls2 = {0};
+ tool_call tc2 = {0};
+ tc2.name = xstrdup("web_search");
+ tc2.arguments = xstrdup("{\"max_results\":\"ten\"}");
+ tool_calls_push(&calls2, tc2);
+ buf b2 = {0};
+ append_tool_calls_json(&b2, &calls2, "test", &r.tool_orders);
+ TEST_ASSERT(strstr(b2.ptr, "\\\"max_results\\\":\\\"ten\\\"") != NULL);
+ buf_free(&b2);
+
+ tool_calls_free(&calls);
+ tool_calls_free(&calls2);
+ request_free(&r);
+}
+
static void test_anthropic_thinking_and_tool_args_preserve_call_order(void) {
request r;
request_init(&r, REQ_CHAT, 128);
@@ -16624,6 +16779,7 @@ static void ds4_server_unit_tests_run(void) {
test_responses_output_sends_tool_search_call_item();
test_dsml_tool_args_preserve_call_order();
test_openai_tool_args_preserve_call_order();
+ test_openai_tool_args_restore_schema_types();
test_anthropic_thinking_and_tool_args_preserve_call_order();
test_context_length_error_uses_protocol_standard_shape();
test_cors_headers_are_opt_in();