mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix(wasm): coerce string parameters to schema-declared types (#498)
* fix(wasm): coerce string parameters to schema-declared types
LLMs frequently pass numeric values as JSON strings ("5" instead of 5)
or booleans as strings ("true" instead of true). The WASM module's
serde deserializer rejects these type mismatches. This adds a
coerce_params_to_schema() helper that walks the params JSON object
and converts string values to their schema-declared types (number,
integer, boolean) before passing to the WASM module.
Adds 5 unit tests covering number, integer, boolean coercion,
already-correct types, and unparseable strings.
Closes #486
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: use in-place mutation and case-insensitive boolean coercion
Address review feedback:
- Use get_mut instead of clone+insert to avoid allocations
- Make boolean coercion case-insensitive (handles "True", "FALSE", etc.)
- Expand boolean test to cover false and mixed-case values
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: collapse nested if-let to satisfy clippy collapsible_if lint
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
89600e2b5c
commit
f99991d27b
@@ -592,6 +592,10 @@ impl WasmToolWrapper {
|
||||
let instance = SandboxedTool::instantiate(&mut store, &component, &linker)
|
||||
.map_err(|e| WasmError::InstantiationFailed(e.to_string()))?;
|
||||
|
||||
// Coerce string-encoded values to their schema-declared types.
|
||||
// LLMs frequently pass numeric values as strings (e.g. "5" instead of 5).
|
||||
let params = coerce_params_to_schema(params, &self.schema);
|
||||
|
||||
// Prepare the request
|
||||
let params_json = serde_json::to_string(¶ms)
|
||||
.map_err(|e| WasmError::InvalidResponseJson(e.to_string()))?;
|
||||
@@ -1083,6 +1087,61 @@ fn is_private_ip(ip: std::net::IpAddr) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Coerce parameter values to match their JSON Schema-declared types.
|
||||
///
|
||||
/// LLMs frequently send numeric values as strings (e.g. `"5"` instead of `5`)
|
||||
/// or booleans as strings (`"true"` instead of `true`). This walks the params
|
||||
/// object and converts string values where the schema expects a different type.
|
||||
fn coerce_params_to_schema(
|
||||
mut params: serde_json::Value,
|
||||
schema: &serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
let properties = schema.get("properties").and_then(|p| p.as_object());
|
||||
|
||||
let properties = match properties {
|
||||
Some(p) => p,
|
||||
None => return params,
|
||||
};
|
||||
|
||||
let obj = match params.as_object_mut() {
|
||||
Some(o) => o,
|
||||
None => return params,
|
||||
};
|
||||
|
||||
for (key, prop_schema) in properties {
|
||||
let declared_type = prop_schema.get("type").and_then(|t| t.as_str());
|
||||
let declared_type = match declared_type {
|
||||
Some(t) => t,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if let Some(current_value) = obj.get_mut(key)
|
||||
&& let Some(s) = current_value.as_str()
|
||||
{
|
||||
if declared_type == "string" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let coerced = match declared_type {
|
||||
"number" => s.parse::<f64>().ok().map(serde_json::Value::from),
|
||||
"integer" => s.parse::<i64>().ok().map(serde_json::Value::from),
|
||||
"boolean" => match s.to_lowercase().as_str() {
|
||||
"true" => Some(serde_json::json!(true)),
|
||||
"false" => Some(serde_json::json!(false)),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(new_val) = coerced {
|
||||
*current_value = new_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
@@ -1588,4 +1647,83 @@ mod tests {
|
||||
let result = super::reject_private_ip("https://8.8.8.8/dns-query");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coerce_params_string_to_number() {
|
||||
let schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": { "type": "number" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
});
|
||||
let params = serde_json::json!({"count": "5", "name": "test"});
|
||||
let result = super::coerce_params_to_schema(params, &schema);
|
||||
assert_eq!(result["count"], serde_json::json!(5.0));
|
||||
assert_eq!(result["name"], serde_json::json!("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coerce_params_string_to_integer() {
|
||||
let schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": { "type": "integer" }
|
||||
}
|
||||
});
|
||||
let params = serde_json::json!({"limit": "10"});
|
||||
let result = super::coerce_params_to_schema(params, &schema);
|
||||
assert_eq!(result["limit"], serde_json::json!(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coerce_params_string_to_boolean() {
|
||||
let schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": { "type": "boolean" },
|
||||
"b": { "type": "boolean" },
|
||||
"c": { "type": "boolean" },
|
||||
"d": { "type": "boolean" }
|
||||
}
|
||||
});
|
||||
let params = serde_json::json!({
|
||||
"a": "true",
|
||||
"b": "false",
|
||||
"c": "True",
|
||||
"d": "FALSE"
|
||||
});
|
||||
let result = super::coerce_params_to_schema(params, &schema);
|
||||
assert_eq!(result["a"], serde_json::json!(true));
|
||||
assert_eq!(result["b"], serde_json::json!(false));
|
||||
assert_eq!(result["c"], serde_json::json!(true));
|
||||
assert_eq!(result["d"], serde_json::json!(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coerce_params_already_correct_type() {
|
||||
let schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": { "type": "number" }
|
||||
}
|
||||
});
|
||||
let params = serde_json::json!({"count": 5});
|
||||
let result = super::coerce_params_to_schema(params, &schema);
|
||||
assert_eq!(result["count"], serde_json::json!(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coerce_params_invalid_string_not_coerced() {
|
||||
let schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": { "type": "number" }
|
||||
}
|
||||
});
|
||||
let params = serde_json::json!({"count": "not-a-number"});
|
||||
let result = super::coerce_params_to_schema(params, &schema);
|
||||
// Should remain as string since it can't be parsed
|
||||
assert_eq!(result["count"], serde_json::json!("not-a-number"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user