@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 880fc032f4
Fork 0

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 &param.name,
107 - &param.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(&params.convert("value", value)).unwrap()
516 + serde_json::to_string(&params.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 }