A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 2, 04:45 UTC
[Rust Frontend] Support recursive tool parameter conversion (#44299)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Bugen Zhao committed
Jun 2, 2026 at 22:45 UTC
880fc032f4064762acb61afed478c3ada073472e
2 files changed
+435
-79
rust/src/tool-parser/src/deepseek_dsml/mod.rs
+1
-1
@@ -104,7 +104,7 @@ impl DeepSeekDsmlToolParser {
104
self.tool_parameters.convert_param_with_schema(
105
&name,
106
¶m.name,
107
- ¶m.value,
107
+ param.value,
108
)
109
};
110
arguments.insert(param.name, value);
rust/src/tool-parser/src/parameters.rs
+434
-78
@@ -1,6 +1,6 @@
1
use std::collections::BTreeMap;
2
3
-use serde_json::{Number, Value};
3
+use serde_json::{Map, Number, Value};
4
5
use crate::Tool;
6
@@ -21,6 +21,29 @@ pub(super) struct ToolSchema {
21
params: BTreeMap<String, JsonParamType>,
22
}
23
24
+/// Parameter input for schema-aware conversion.
25
+///
26
+/// It can be either a raw text string, or a structured input with named child elements.
27
+#[derive(Debug, Clone, PartialEq, Eq)]
28
+pub(super) enum ParamInput {
29
+ Text(String),
30
+ #[allow(dead_code)]
31
+ Elements(Vec<ParamElement>),
32
+}
33
+
34
+impl From<String> for ParamInput {
35
+ fn from(value: String) -> Self {
36
+ Self::Text(value)
37
+ }
38
+}
39
+
40
+/// One named structured parameter child.
41
+#[derive(Debug, Clone, PartialEq, Eq)]
42
+pub(super) struct ParamElement {
43
+ pub name: String,
44
+ pub value: ParamInput,
45
+}
46
+
47
/// Normalized JSON parameter type used for raw string coercion.
48
#[derive(Debug, Clone, PartialEq, Eq)]
49
pub(super) enum JsonParamType {
@@ -28,8 +51,13 @@ pub(super) enum JsonParamType {
51
Integer,
52
Number,
53
Boolean,
31
- Object,
32
- Array,
54
+ Object {
55
+ properties: BTreeMap<String, JsonParamType>,
56
+ additional_properties: Option<Box<JsonParamType>>,
57
+ },
58
+ Array {
59
+ items: Option<Box<JsonParamType>>,
60
+ },
61
Null,
62
OneOf(Vec<JsonParamType>),
63
}
@@ -45,33 +73,39 @@ impl ToolSchemas {
73
Self { tools }
74
}
75
48
- /// Convert raw string parameter values for one named tool.
76
+ /// Convert parameter values for one named tool.
77
///
78
/// Unknown tool names use an empty schema, so all parameters fall back to
51
- /// strings.
52
- pub(super) fn convert_params_with_schema(
79
+ /// strings or object-like JSON for structured inputs.
80
+ pub(super) fn convert_params_with_schema<P>(
81
&self,
82
function_name: &str,
55
- params: Vec<(String, String)>,
56
- ) -> serde_json::Map<String, Value> {
83
+ params: Vec<(String, P)>,
84
+ ) -> Map<String, Value>
85
+ where
86
+ P: Into<ParamInput>,
87
+ {
88
let tool_schema = self.tools.get(function_name).unwrap_or(ToolSchema::empty());
58
- let mut converted = serde_json::Map::with_capacity(params.len());
89
+ let mut converted = Map::with_capacity(params.len());
90
for (name, value) in params {
60
- let value = tool_schema.convert(&name, &value);
91
+ let value = tool_schema.convert(&name, value.into());
92
converted.insert(name, value);
93
}
94
converted
95
}
96
66
- /// Convert one raw string parameter value for one named tool.
67
- pub(super) fn convert_param_with_schema(
97
+ /// Convert one parameter value for one named tool.
98
+ pub(super) fn convert_param_with_schema<P>(
99
&self,
100
function_name: &str,
101
name: &str,
71
- value: &str,
72
- ) -> Value {
102
+ value: P,
103
+ ) -> Value
104
+ where
105
+ P: Into<ParamInput>,
106
+ {
107
let tool_schema = self.tools.get(function_name).unwrap_or(ToolSchema::empty());
74
- tool_schema.convert(name, value)
108
+ tool_schema.convert(name, value.into())
109
}
110
}
111
@@ -101,21 +135,13 @@ impl ToolSchema {
135
Self { params }
136
}
137
104
- /// Convert one raw parameter value using its normalized schema type.
138
+ /// Convert one parameter value using its normalized schema type.
139
///
140
/// If the parameter name is unknown, or we don't have a schema for it, or
141
/// the value fails to convert, this falls back to returning the raw
108
- /// string as a JSON string value.
109
- fn convert(&self, name: &str, value: &str) -> Value {
110
- if value.eq_ignore_ascii_case("null") {
111
- return Value::Null;
112
- }
113
-
114
- let Some(param_type) = self.params.get(name) else {
115
- return Value::String(value.to_string());
116
- };
117
-
118
- convert_value(param_type, value).unwrap_or_else(|| Value::String(value.to_string()))
142
+ /// string as a JSON string value, or object-like JSON for structured input.
143
+ fn convert(&self, name: &str, input: ParamInput) -> Value {
144
+ convert_with_optional_schema(self.params.get(name), &input)
145
}
146
}
147
@@ -125,7 +151,7 @@ impl JsonParamType {
151
let schema = schema.as_object()?;
152
153
if let Some(type_value) = schema.get("type") {
128
- return Self::from_type_value(type_value);
154
+ return Self::from_type_value(type_value, schema);
155
}
156
157
if let Some(composite) = schema.get("anyOf").or_else(|| schema.get("oneOf")) {
@@ -134,32 +160,34 @@ impl JsonParamType {
160
.map(|schemas| schemas.iter().filter_map(Self::from_schema).collect::<Vec<_>>())
161
.filter(|types| !types.is_empty())
162
.map(Self::one_of)
137
- .unwrap_or(Self::Object);
163
+ .unwrap_or_else(|| Self::object_from_schema(Some(schema)));
164
return Some(param_type);
165
}
166
167
+ // Typically, these types are already handled by checking the "type" field, but
168
+ // we can also infer them from their characteristic fields if "type" is missing.
169
if schema.contains_key("enum") {
170
return Some(Self::String);
171
}
172
if schema.contains_key("items") {
145
- return Some(Self::Array);
173
+ return Some(Self::array_from_schema(Some(schema)));
174
}
147
- if schema.contains_key("properties") {
148
- return Some(Self::Object);
175
+ if schema.contains_key("properties") || schema.contains_key("additionalProperties") {
176
+ return Some(Self::object_from_schema(Some(schema)));
177
}
178
179
None
180
}
181
182
/// Normalize a JSON schema `type` value.
155
- fn from_type_value(type_value: &Value) -> Option<Self> {
183
+ fn from_type_value(type_value: &Value, schema: &Map<String, Value>) -> Option<Self> {
184
match type_value {
157
- Value::String(kind) => Self::from_type_name(kind),
185
+ Value::String(kind) => Self::from_type_name(kind, Some(schema)),
186
Value::Array(kinds) => {
187
let types = kinds
188
.iter()
189
.filter_map(Value::as_str)
162
- .filter_map(Self::from_type_name)
190
+ .filter_map(|kind| Self::from_type_name(kind, Some(schema)))
191
.collect::<Vec<_>>();
192
if types.is_empty() {
193
None
@@ -172,15 +200,15 @@ impl JsonParamType {
200
}
201
202
/// Normalize one JSON schema type name.
175
- fn from_type_name(kind: &str) -> Option<Self> {
203
+ fn from_type_name(kind: &str, schema: Option<&Map<String, Value>>) -> Option<Self> {
204
let kind = kind.trim().to_ascii_lowercase();
205
match kind.as_str() {
206
"string" | "str" | "text" | "varchar" | "char" | "enum" => Some(Self::String),
207
"integer" | "int" => Some(Self::Integer),
208
"number" | "float" | "double" => Some(Self::Number),
209
"boolean" | "bool" | "binary" => Some(Self::Boolean),
182
- "object" | "dict" | "map" => Some(Self::Object),
183
- "array" | "arr" | "list" | "sequence" => Some(Self::Array),
210
+ "object" | "dict" | "map" => Some(Self::object_from_schema(schema)),
211
+ "array" | "arr" | "list" | "sequence" => Some(Self::array_from_schema(schema)),
212
"null" => Some(Self::Null),
213
_ if kind.starts_with("int")
214
|| kind.starts_with("uint")
@@ -191,12 +219,52 @@ impl JsonParamType {
219
Some(Self::Integer)
220
}
221
_ if kind.starts_with("num") || kind.starts_with("float") => Some(Self::Number),
194
- _ if kind.starts_with("dict") => Some(Self::Object),
195
- _ if kind.starts_with("list") => Some(Self::Array),
222
+ _ if kind.starts_with("dict") => Some(Self::object_from_schema(schema)),
223
+ _ if kind.starts_with("list") => Some(Self::array_from_schema(schema)),
224
_ => None,
225
}
226
}
227
228
+ /// Normalize object schema fields.
229
+ fn object_from_schema(schema: Option<&Map<String, Value>>) -> Self {
230
+ let properties = schema
231
+ .and_then(|schema| schema.get("properties"))
232
+ .and_then(Value::as_object)
233
+ .map(|properties| {
234
+ properties
235
+ .iter()
236
+ .filter_map(|(name, schema)| {
237
+ Self::from_schema(schema).map(|param_type| (name.clone(), param_type))
238
+ })
239
+ .collect()
240
+ })
241
+ .unwrap_or_default();
242
+
243
+ let additional_properties =
244
+ schema.and_then(|schema| schema.get("additionalProperties")).and_then(|schema| {
245
+ if schema.is_object() {
246
+ Self::from_schema(schema).map(Box::new)
247
+ } else {
248
+ None
249
+ }
250
+ });
251
+
252
+ Self::Object {
253
+ properties,
254
+ additional_properties,
255
+ }
256
+ }
257
+
258
+ /// Normalize array schema fields.
259
+ fn array_from_schema(schema: Option<&Map<String, Value>>) -> Self {
260
+ let items = schema
261
+ .and_then(|schema| schema.get("items"))
262
+ .and_then(Self::from_schema)
263
+ .map(Box::new);
264
+
265
+ Self::Array { items }
266
+ }
267
+
268
/// Collapse a candidate type list into one normalized type.
269
fn one_of(mut types: Vec<Self>) -> Self {
270
if types.len() == 1 {
@@ -207,23 +275,126 @@ impl JsonParamType {
275
}
276
}
277
278
+/// Convert one parameter input to a normalized JSON value.
279
+fn convert_with_optional_schema(param_type: Option<&JsonParamType>, input: &ParamInput) -> Value {
280
+ // For literal `null`, always convert to JSON null value.
281
+ if let ParamInput::Text(value) = input
282
+ && value.eq_ignore_ascii_case("null")
283
+ {
284
+ return Value::Null;
285
+ }
286
+
287
+ // If we have a schema, try to convert the value using it.
288
+ if let Some(param_type) = param_type
289
+ && let Some(value) = try_convert_value(param_type, input)
290
+ {
291
+ return value;
292
+ }
293
+ // We don't have a schema, or conversion failed, use fallback logic.
294
+ match input {
295
+ ParamInput::Text(value) => Value::String(value.clone()),
296
+ ParamInput::Elements(elements) => {
297
+ // Convert structured input to object without a schema.
298
+ Value::Object(convert_elements_to_object(elements, &BTreeMap::new(), None))
299
+ }
300
+ }
301
+}
302
+
303
+/// Convert one parameter input to a normalized JSON type.
304
+fn try_convert_value(param_type: &JsonParamType, input: &ParamInput) -> Option<Value> {
305
+ match input {
306
+ ParamInput::Text(value) => try_convert_text_value(param_type, value),
307
+ ParamInput::Elements(elements) => try_convert_elements_value(param_type, elements),
308
+ }
309
+}
310
+
311
/// Convert one raw string value to a normalized JSON type.
211
-fn convert_value(param_type: &JsonParamType, value: &str) -> Option<Value> {
312
+fn try_convert_text_value(param_type: &JsonParamType, value: &str) -> Option<Value> {
313
match param_type {
314
JsonParamType::String => Some(Value::String(value.to_string())),
315
JsonParamType::Integer => value.parse::<i64>().ok().map(Number::from).map(Value::Number),
215
- JsonParamType::Number => convert_number(value),
216
- JsonParamType::Boolean => convert_boolean(value),
217
- JsonParamType::Object | JsonParamType::Array => serde_json::from_str(value).ok(),
316
+ JsonParamType::Number => try_convert_number(value),
317
+ JsonParamType::Boolean => try_convert_boolean(value),
318
+ JsonParamType::Object { .. } if value.is_empty() => Some(Value::Object(Map::new())),
319
+ JsonParamType::Array { .. } if value.is_empty() => Some(Value::Array(Vec::new())),
320
+ JsonParamType::Object { .. } | JsonParamType::Array { .. } => {
321
+ // For composite types with string input, simply interpret the string as JSON.
322
+ serde_json::from_str(value).ok()
323
+ }
324
JsonParamType::Null => value.eq_ignore_ascii_case("null").then_some(Value::Null),
325
JsonParamType::OneOf(types) => {
220
- types.iter().find_map(|param_type| convert_value(param_type, value))
326
+ types.iter().find_map(|param_type| try_convert_text_value(param_type, value))
327
}
328
}
329
}
330
331
+/// Convert one structured parameter input to a normalized JSON type.
332
+fn try_convert_elements_value(
333
+ param_type: &JsonParamType,
334
+ elements: &[ParamElement],
335
+) -> Option<Value> {
336
+ match param_type {
337
+ JsonParamType::Object {
338
+ properties,
339
+ additional_properties,
340
+ } => Some(Value::Object(convert_elements_to_object(
341
+ elements,
342
+ properties,
343
+ additional_properties.as_deref(),
344
+ ))),
345
+ JsonParamType::Array { items } => Some(Value::Array(
346
+ // Collect all child elements into an array, regardless of their names.
347
+ elements
348
+ .iter()
349
+ .map(|element| convert_with_optional_schema(items.as_deref(), &element.value))
350
+ .collect(),
351
+ )),
352
+ JsonParamType::OneOf(types) => types
353
+ .iter()
354
+ .find_map(|param_type| try_convert_elements_value(param_type, elements)),
355
+
356
+ // Primitive types can't be converted from structured input.
357
+ JsonParamType::String
358
+ | JsonParamType::Integer
359
+ | JsonParamType::Number
360
+ | JsonParamType::Boolean
361
+ | JsonParamType::Null => None,
362
+ }
363
+}
364
+
365
+/// Convert structured elements to an object, using field schemas when present.
366
+fn convert_elements_to_object(
367
+ elements: &[ParamElement],
368
+ properties: &BTreeMap<String, JsonParamType>,
369
+ additional_properties: Option<&JsonParamType>,
370
+) -> Map<String, Value> {
371
+ let mut object = Map::with_capacity(elements.len());
372
+ for element in elements {
373
+ let param_type = properties.get(&element.name).or(additional_properties);
374
+ let value = convert_with_optional_schema(param_type, &element.value);
375
+ insert_object_value(&mut object, element.name.clone(), value);
376
+ }
377
+ object
378
+}
379
+
380
+/// Insert an object field while preserving duplicate keys as arrays.
381
+fn insert_object_value(object: &mut Map<String, Value>, key: String, value: Value) {
382
+ if let Some(existing) = object.get_mut(&key) {
383
+ match existing {
384
+ // Collect values under the same key into an array.
385
+ Value::Array(values) => values.push(value),
386
+ existing => {
387
+ let first = std::mem::replace(existing, Value::Null);
388
+ *existing = Value::Array(vec![first, value]);
389
+ }
390
+ }
391
+ } else {
392
+ object.insert(key, value);
393
+ }
394
+}
395
+
396
/// Convert one raw string value to a JSON number.
226
-fn convert_number(value: &str) -> Option<Value> {
397
+fn try_convert_number(value: &str) -> Option<Value> {
398
serde_json::from_str::<Number>(value)
399
.or_else(|_| value.parse::<i64>().map(Number::from))
400
.or_else(|_| value.parse::<f64>().ok().and_then(Number::from_f64).ok_or(()))
@@ -232,7 +403,7 @@ fn convert_number(value: &str) -> Option<Value> {
403
}
404
405
/// Convert one raw string value to a boolean.
235
-fn convert_boolean(value: &str) -> Option<Value> {
406
+fn try_convert_boolean(value: &str) -> Option<Value> {
407
match value.trim().to_ascii_lowercase().as_str() {
408
"true" | "1" => Some(Value::Bool(true)),
409
"false" | "0" => Some(Value::Bool(false)),
@@ -242,9 +413,9 @@ fn convert_boolean(value: &str) -> Option<Value> {
413
414
#[cfg(test)]
415
mod tests {
245
- use serde_json::json;
416
+ use serde_json::{Value, json};
417
247
- use super::{ToolSchema, ToolSchemas};
418
+ use super::{ParamElement, ParamInput, ToolSchema, ToolSchemas};
419
use crate::Tool;
420
421
fn test_tool(name: &str, parameters: serde_json::Value) -> Tool {
@@ -260,8 +431,8 @@ mod tests {
431
fn invalid_schema_converts_everything_as_string() {
432
let params = ToolSchema::from_schema(&json!({ "type": "object" }));
433
263
- assert_eq!(params.convert("count", "42"), json!("42"));
264
- assert_eq!(params.convert("count", "null"), json!(null));
434
+ assert_eq!(params.convert("count", text("42")), json!("42"));
435
+ assert_eq!(params.convert("count", text("null")), json!(null));
436
}
437
438
#[test]
@@ -275,9 +446,9 @@ mod tests {
446
}
447
}));
448
278
- assert_eq!(params.convert("unknown_schema", "42"), json!("42"));
279
- assert_eq!(params.convert("unknown_type", "42"), json!("42"));
280
- assert_eq!(params.convert("known", "42"), json!(42));
449
+ assert_eq!(params.convert("unknown_schema", text("42")), json!("42"));
450
+ assert_eq!(params.convert("unknown_type", text("42")), json!("42"));
451
+ assert_eq!(params.convert("known", text("42")), json!(42));
452
}
453
454
#[test]
@@ -298,16 +469,25 @@ mod tests {
469
}
470
}));
471
301
- assert_eq!(params.convert("text", "42"), json!("42"));
302
- assert_eq!(params.convert("count", "42"), json!(42));
303
- assert_eq!(params.convert("size", "5.0"), json!(5.0));
304
- assert_eq!(params.convert("ratio", "2.5"), json!(2.5));
305
- assert_eq!(params.convert("enabled", "1"), json!(true));
306
- assert_eq!(params.convert("payload", r#"{"k":1}"#), json!({ "k": 1 }));
307
- assert_eq!(params.convert("mapping", r#"{"k":1}"#), json!({ "k": 1 }));
308
- assert_eq!(params.convert("items", "[1,2]"), json!([1, 2]));
309
- assert_eq!(params.convert("names", r#"["a","b"]"#), json!(["a", "b"]));
310
- assert_eq!(params.convert("nothing", "null"), json!(null));
472
+ assert_eq!(params.convert("text", text("42")), json!("42"));
473
+ assert_eq!(params.convert("count", text("42")), json!(42));
474
+ assert_eq!(params.convert("size", text("5.0")), json!(5.0));
475
+ assert_eq!(params.convert("ratio", text("2.5")), json!(2.5));
476
+ assert_eq!(params.convert("enabled", text("1")), json!(true));
477
+ assert_eq!(
478
+ params.convert("payload", text(r#"{"k":1}"#)),
479
+ json!({ "k": 1 })
480
+ );
481
+ assert_eq!(
482
+ params.convert("mapping", text(r#"{"k":1}"#)),
483
+ json!({ "k": 1 })
484
+ );
485
+ assert_eq!(params.convert("items", text("[1,2]")), json!([1, 2]));
486
+ assert_eq!(
487
+ params.convert("names", text(r#"["a","b"]"#)),
488
+ json!(["a", "b"])
489
+ );
490
+ assert_eq!(params.convert("nothing", text("null")), json!(null));
491
}
492
493
#[test]
@@ -333,7 +513,22 @@ mod tests {
513
}
514
515
fn converted_number_text(params: &ToolSchema, value: &str) -> String {
336
- serde_json::to_string(¶ms.convert("value", value)).unwrap()
516
+ serde_json::to_string(¶ms.convert("value", text(value))).unwrap()
517
+ }
518
+
519
+ fn text(value: &str) -> ParamInput {
520
+ ParamInput::Text(value.to_string())
521
+ }
522
+
523
+ fn elem(name: &str, value: ParamInput) -> ParamElement {
524
+ ParamElement {
525
+ name: name.to_string(),
526
+ value,
527
+ }
528
+ }
529
+
530
+ fn elements(elements: Vec<ParamElement>) -> ParamInput {
531
+ ParamInput::Elements(elements)
532
}
533
534
#[test]
@@ -350,12 +545,12 @@ mod tests {
545
}
546
}));
547
353
- assert_eq!(params.convert("s", "x"), json!("x"));
354
- assert_eq!(params.convert("i", "7"), json!(7));
355
- assert_eq!(params.convert("n", "7.5"), json!(7.5));
356
- assert_eq!(params.convert("b", "true"), json!(true));
357
- assert_eq!(params.convert("a", "[1]"), json!([1]));
358
- assert_eq!(params.convert("o", r#"{"x":1}"#), json!({ "x": 1 }));
548
+ assert_eq!(params.convert("s", text("x")), json!("x"));
549
+ assert_eq!(params.convert("i", text("7")), json!(7));
550
+ assert_eq!(params.convert("n", text("7.5")), json!(7.5));
551
+ assert_eq!(params.convert("b", text("true")), json!(true));
552
+ assert_eq!(params.convert("a", text("[1]")), json!([1]));
553
+ assert_eq!(params.convert("o", text(r#"{"x":1}"#)), json!({ "x": 1 }));
554
}
555
556
#[test]
@@ -373,8 +568,8 @@ mod tests {
568
}
569
}));
570
376
- assert_eq!(integer_first.convert("value", "42"), json!(42));
377
- assert_eq!(string_first.convert("value", "42"), json!("42"));
571
+ assert_eq!(integer_first.convert("value", text("42")), json!(42));
572
+ assert_eq!(string_first.convert("value", text("42")), json!("42"));
573
}
574
575
#[test]
@@ -396,9 +591,9 @@ mod tests {
591
}
592
}));
593
399
- assert_eq!(params.convert("choice", "42"), json!(42));
594
+ assert_eq!(params.convert("choice", text("42")), json!(42));
595
assert_eq!(
401
- params.convert("fallback_object", r#"{"x":1}"#),
596
+ params.convert("fallback_object", text(r#"{"x":1}"#)),
597
json!({ "x": 1 })
598
);
599
}
@@ -414,9 +609,12 @@ mod tests {
609
}
610
}));
611
417
- assert_eq!(params.convert("choice", "a"), json!("a"));
418
- assert_eq!(params.convert("items", "[1,2]"), json!([1, 2]));
419
- assert_eq!(params.convert("payload", r#"{"x":1}"#), json!({ "x": 1 }));
612
+ assert_eq!(params.convert("choice", text("a")), json!("a"));
613
+ assert_eq!(params.convert("items", text("[1,2]")), json!([1, 2]));
614
+ assert_eq!(
615
+ params.convert("payload", text(r#"{"x":1}"#)),
616
+ json!({ "x": 1 })
617
+ );
618
}
619
620
#[test]
@@ -518,4 +716,162 @@ mod tests {
716
assert_eq!(converted.get("topn"), Some(&json!("5")));
717
assert_eq!(converted.get("nullish"), Some(&json!(null)));
718
}
719
+
720
+ #[test]
721
+ fn converts_structured_inputs_with_recursive_schema() {
722
+ let schemas = ToolSchemas::from_tools(&[test_tool(
723
+ "create_order",
724
+ json!({
725
+ "type": "object",
726
+ "properties": {
727
+ "user_id": { "type": "integer" },
728
+ "urgent": { "type": "boolean" },
729
+ "note": { "type": "string" },
730
+ "nil": { "type": "string" },
731
+ "shipping": {
732
+ "type": "object",
733
+ "properties": {
734
+ "city": { "type": "string" },
735
+ "zip": { "type": "integer" }
736
+ }
737
+ },
738
+ "items": {
739
+ "type": "array",
740
+ "items": {
741
+ "type": "object",
742
+ "properties": {
743
+ "sku": { "type": "string" },
744
+ "qty": { "type": "integer" }
745
+ }
746
+ }
747
+ },
748
+ "metadata": {
749
+ "type": "object",
750
+ "additionalProperties": { "type": "integer" }
751
+ },
752
+ "duplicate_demo": {
753
+ "type": "object",
754
+ "properties": {
755
+ "tag": { "type": "string" }
756
+ }
757
+ },
758
+ "schema_mismatch_array": {
759
+ "type": "array",
760
+ "items": { "type": "integer" }
761
+ },
762
+ "closed_object": {
763
+ "type": "object",
764
+ "additionalProperties": false
765
+ },
766
+ "open_object": {
767
+ "type": "object",
768
+ "additionalProperties": true
769
+ },
770
+ "payload_text": { "type": "object" },
771
+ "items_text": { "type": "array" }
772
+ }
773
+ }),
774
+ )]);
775
+
776
+ let converted = schemas.convert_params_with_schema(
777
+ "create_order",
778
+ vec![
779
+ ("user_id".to_string(), text("42")),
780
+ ("urgent".to_string(), text("true")),
781
+ ("note".to_string(), text("Please leave at front desk.")),
782
+ ("nil".to_string(), text("NULL")),
783
+ (
784
+ "shipping".to_string(),
785
+ elements(vec![
786
+ elem("city", text("Singapore")),
787
+ elem("zip", text("018956")),
788
+ ]),
789
+ ),
790
+ (
791
+ "items".to_string(),
792
+ elements(vec![
793
+ elem(
794
+ "item1",
795
+ elements(vec![elem("sku", text("book-001")), elem("qty", text("2"))]),
796
+ ),
797
+ elem(
798
+ "item2",
799
+ elements(vec![elem("sku", text("pen-007")), elem("qty", text("5"))]),
800
+ ),
801
+ ]),
802
+ ),
803
+ (
804
+ "metadata".to_string(),
805
+ elements(vec![elem("score", text("42")), elem("rank", text("7"))]),
806
+ ),
807
+ (
808
+ "duplicate_demo".to_string(),
809
+ elements(vec![elem("tag", text("a")), elem("tag", text("b"))]),
810
+ ),
811
+ (
812
+ "closed_object".to_string(),
813
+ elements(vec![elem("unknown", text("x"))]),
814
+ ),
815
+ (
816
+ "open_object".to_string(),
817
+ elements(vec![elem("unknown", text("y"))]),
818
+ ),
819
+ ("payload_text".to_string(), text(r#"{"x":1}"#)),
820
+ ("items_text".to_string(), text("[1,2]")),
821
+ (
822
+ "unknown_struct".to_string(),
823
+ elements(vec![
824
+ elem("a", text("1")),
825
+ elem("a", text("2")),
826
+ elem("nil", text("null")),
827
+ ]),
828
+ ),
829
+ ],
830
+ );
831
+
832
+ assert_eq!(
833
+ Value::Object(converted),
834
+ json!({
835
+ "user_id": 42,
836
+ "urgent": true,
837
+ "note": "Please leave at front desk.",
838
+ "nil": null,
839
+ "shipping": {
840
+ "city": "Singapore",
841
+ "zip": 18956
842
+ },
843
+ "items": [
844
+ {
845
+ "sku": "book-001",
846
+ "qty": 2
847
+ },
848
+ {
849
+ "sku": "pen-007",
850
+ "qty": 5
851
+ }
852
+ ],
853
+ "metadata": {
854
+ "score": 42,
855
+ "rank": 7
856
+ },
857
+ "duplicate_demo": {
858
+ "tag": ["a", "b"]
859
+ },
860
+ "closed_object": {
861
+ "unknown": "x"
862
+ },
863
+ "open_object": {
864
+ "unknown": "y"
865
+ },
866
+ "payload_text": {
867
+ "x": 1
868
+ },
869
+ "items_text": [1, 2],
870
+ "unknown_struct": {
871
+ "a": ["1", "2"],
872
+ "nil": null
873
+ }
874
+ })
875
+ );
876
+ }
877
}