mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(normalization): guard function.arguments against malformed JSON and default to {} (#1645)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
parent
59e469c3df
commit
8ae921dd68
@@ -12,7 +12,9 @@ mod compatible_stream;
|
||||
mod compatible_types;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use compatible_parse::{parse_sse_line, strip_think_tags};
|
||||
pub(crate) use compatible_parse::{
|
||||
parse_provider_tool_call_from_value, parse_sse_line, strip_think_tags,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use compatible_types::ResponsesResponse;
|
||||
|
||||
@@ -993,10 +995,12 @@ impl OpenAiCompatibleProvider {
|
||||
None
|
||||
} else {
|
||||
// Try to parse as JSON first so downstream
|
||||
// `normalize_function_arguments` can handle the
|
||||
// usual Value path; fall back to a JSON-string
|
||||
// value if the accumulated text isn't valid
|
||||
// JSON yet.
|
||||
// `normalize_function_arguments` can take the
|
||||
// usual Value (object) path; fall back to a
|
||||
// JSON-string value for partially-assembled or
|
||||
// permanently malformed fragments.
|
||||
// `normalize_function_arguments` validates and
|
||||
// discards malformed strings (OPENHUMAN-TAURI-6F).
|
||||
Some(
|
||||
serde_json::from_str(&c.arguments)
|
||||
.unwrap_or(serde_json::Value::String(c.arguments)),
|
||||
|
||||
@@ -119,8 +119,18 @@ pub(crate) fn normalize_function_arguments(arguments: Option<serde_json::Value>)
|
||||
Some(serde_json::Value::String(raw)) => {
|
||||
if raw.trim().is_empty() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
} else if serde_json::from_str::<serde_json::Value>(&raw).is_ok() {
|
||||
raw
|
||||
} else {
|
||||
// OPENHUMAN-TAURI-6F: model emitted malformed JSON in
|
||||
// `function.arguments`. Log the discard so it's traceable
|
||||
// without leaking argument contents (which may contain PII).
|
||||
log::warn!(
|
||||
"[providers] normalize_function_arguments: \
|
||||
discarding malformed JSON string (len={}) — substituting {{}}",
|
||||
raw.len()
|
||||
);
|
||||
"{}".to_string()
|
||||
}
|
||||
}
|
||||
Some(serde_json::Value::Null) | None => "{}".to_string(),
|
||||
@@ -140,11 +150,12 @@ pub(crate) fn parse_provider_tool_call_from_value(
|
||||
call.id
|
||||
},
|
||||
name: call.name,
|
||||
arguments: if call.arguments.trim().is_empty() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
call.arguments
|
||||
},
|
||||
// Route through normalize_function_arguments so malformed
|
||||
// JSON strings in pre-deserialized ProviderToolCall values
|
||||
// receive the same guard as the function.arguments path below.
|
||||
arguments: normalize_function_arguments(Some(serde_json::Value::String(
|
||||
call.arguments,
|
||||
))),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,3 +1147,58 @@ fn parse_sse_line_done_sentinel() {
|
||||
let result = parse_sse_line(line).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_function_arguments_valid_json_string_preserved() {
|
||||
let v = Some(serde_json::Value::String(r#"{"path":"/tmp"}"#.to_string()));
|
||||
assert_eq!(normalize_function_arguments(v), r#"{"path":"/tmp"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_function_arguments_invalid_json_string_falls_back_to_empty_object() {
|
||||
// OPENHUMAN-TAURI-6F: model emitted malformed JSON in `function.arguments`.
|
||||
// Forwarding the raw string back upstream causes a 400 from the backend's
|
||||
// `json.loads`. Substitute `{}` instead.
|
||||
for raw in ["{a:1}", "{'k':'v'}", "{\n", "{,}"] {
|
||||
let v = Some(serde_json::Value::String(raw.to_string()));
|
||||
assert_eq!(normalize_function_arguments(v), "{}", "raw = {raw:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_function_arguments_empty_or_null_becomes_empty_object() {
|
||||
assert_eq!(
|
||||
normalize_function_arguments(Some(serde_json::Value::String(" ".to_string()))),
|
||||
"{}"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_function_arguments(Some(serde_json::Value::Null)),
|
||||
"{}"
|
||||
);
|
||||
assert_eq!(normalize_function_arguments(None), "{}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_function_arguments_object_value_serializes() {
|
||||
let v = Some(serde_json::json!({"path": "/tmp"}));
|
||||
assert_eq!(normalize_function_arguments(v), r#"{"path":"/tmp"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_provider_tool_call_from_value_guards_malformed_arguments() {
|
||||
// OPENHUMAN-TAURI-6F: the early-return path in
|
||||
// `parse_provider_tool_call_from_value` previously bypassed
|
||||
// `normalize_function_arguments`, forwarding malformed JSON strings
|
||||
// directly. Verify the guard now applies on both code paths.
|
||||
let value = serde_json::json!({
|
||||
"id": "call_bad",
|
||||
"name": "shell",
|
||||
"arguments": "{a:1}"
|
||||
});
|
||||
let result = parse_provider_tool_call_from_value(&value);
|
||||
let call = result.expect("should produce a ToolCall");
|
||||
assert_eq!(
|
||||
call.arguments, "{}",
|
||||
"malformed arguments string must be normalised to {{}} via the first-path guard"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user