[Rust Frontend] Keep literal "null" string for string-typed tool params (#46827)
Signed-off-by: Blas Rodriguez Irizar <rodrigblas@gmail.com>
Blas Rodriguez Irizar committed
Jun 29, 2026 at 14:46 UTC
6185d73882c0cdfd9ee13cea16a9b50d2b5267be
2 files changed
+48
-17
rust/src/parser/src/tool/deepseek_dsml/deepseek_v32.rs
+1
-1
@@ -154,7 +154,7 @@ mod tests {
154
"flag": true,
155
"payload": { "nested": true },
156
"items": [1, 2],
157
- "empty": null,
157
+ "empty": "null",
158
})
159
);
160
}
rust/src/parser/src/tool/parameters.rs
+47
-16
@@ -166,7 +166,13 @@ impl JsonParamType {
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") {
169
+ if let Some(values) = schema.get("enum").and_then(Value::as_array) {
170
+ // Enum values are treated as strings, except that a `null` member
171
+ // makes the parameter nullable (mirrors Python's enum type
172
+ // inference), so a literal "null" coerces to JSON null.
173
+ if values.iter().any(Value::is_null) {
174
+ return Some(Self::one_of(vec![Self::String, Self::Null]));
175
+ }
176
return Some(Self::String);
177
}
178
if schema.contains_key("items") {
@@ -277,9 +283,12 @@ impl JsonParamType {
283
284
/// Convert one parameter input to a normalized JSON value.
285
fn convert_with_optional_schema(param_type: Option<&JsonParamType>, input: &ParamInput) -> Value {
280
- // For literal `null`, always convert to JSON null value.
286
+ // Coerce the literal text `null` to JSON null, except for `string`-typed
287
+ // params, where it must stay the string "null": a model emitting the literal
288
+ // text "null" for a string field means the string, not a missing value.
289
if let ParamInput::Text(value) = input
290
&& value.eq_ignore_ascii_case("null")
291
+ && param_type != Some(&JsonParamType::String)
292
{
293
return Value::Null;
294
}
@@ -685,21 +694,43 @@ mod tests {
694
}
695
696
#[test]
688
- fn convert_params_preserves_null_for_known_param() {
689
- let schemas = ToolSchemas::from_tools(&[test_tool(
690
- "convert",
691
- json!({
692
- "type": "object",
693
- "properties": {
694
- "value": { "type": "string" }
695
- }
696
- }),
697
- )]);
697
+ fn string_param_preserves_literal_null_text() {
698
+ // A `string`-typed param whose value is the literal text "null"/"NULL"
699
+ // must stay a string (the original case is preserved), rather than being
700
+ // coerced to JSON null. Non-string types keep coercing "null" to null.
701
+ let params = ToolSchema::from_schema(&json!({
702
+ "type": "object",
703
+ "properties": {
704
+ "name": { "type": "string" },
705
+ "count": { "type": "integer" },
706
+ "anything": {}
707
+ }
708
+ }));
709
+
710
+ assert_eq!(params.convert("name", text("null")), json!("null"));
711
+ assert_eq!(params.convert("name", text("NULL")), json!("NULL"));
712
+ // Non-string and schema-less params are unchanged: "null" -> null.
713
+ assert_eq!(params.convert("count", text("null")), json!(null));
714
+ assert_eq!(params.convert("anything", text("null")), json!(null));
715
+ }
716
699
- let converted = schemas
700
- .convert_params_with_schema("convert", vec![("value".to_string(), "NULL".to_string())]);
717
+ #[test]
718
+ fn nullable_enum_param_coerces_literal_null() {
719
+ // An enum that includes `null` admits a null value, so a literal "null"
720
+ // must coerce to JSON null (matching Python's `extract_types_from_schema`,
721
+ // which infers `null` from the enum values), while a non-null enum keeps
722
+ // "null" as a string.
723
+ let params = ToolSchema::from_schema(&json!({
724
+ "type": "object",
725
+ "properties": {
726
+ "mode": { "enum": [null, "auto"] },
727
+ "color": { "enum": ["red", "green"] }
728
+ }
729
+ }));
730
702
- assert_eq!(converted.get("value"), Some(&json!(null)));
731
+ assert_eq!(params.convert("mode", text("null")), json!(null));
732
+ assert_eq!(params.convert("mode", text("auto")), json!("auto"));
733
+ assert_eq!(params.convert("color", text("null")), json!("null"));
734
}
735
736
#[test]
@@ -841,7 +872,7 @@ mod tests {
872
"user_id": 42,
873
"urgent": true,
874
"note": "Please leave at front desk.",
844
- "nil": null,
875
+ "nil": "NULL",
876
"shipping": {
877
"city": "Singapore",
878
"zip": 18956