fix: normalize function parameters type to "object" for strict OpenAI-compatible providers (#4706)

* fix: normalize function parameters type to "object" for strict OpenAI-compatible providers

Some Responses tools carry parameters with `type: null` (e.g.
codex_app__automation_update), causing HTTP 400 from strict
OpenAI-compatible providers like DeepSeek that require
`{"type": "object", "properties": {...}}`.

This adds normalize_function_parameters() to ensure the type
field is always "object" in both branches of
responses_function_tool_to_chat_tool.

Closes #4705

* style: fix cargo fmt issues

* fix: handle null function parameters
This commit is contained in:
Ryan2128
2026-07-14 19:23:41 +08:00
committed by GitHub
parent c8b0d60c2d
commit 9ca1a41f58
@@ -1096,6 +1096,26 @@ fn serialize_tool_definition_for_description(tool: &Value) -> String {
canonical_json_string(tool)
}
/// Normalize a function's `parameters` JSON Schema so `type` is always `"object"`.
///
/// Some Responses tools carry `parameters: null` or `parameters: {"type": null}`,
/// but OpenAI Chat Completions strictly requires `{"type": "object", "properties": {...}}`.
fn normalize_function_parameters(params: Option<&Value>) -> Value {
let mut params = match params {
Some(Value::Object(obj)) => Value::Object(obj.clone()),
_ => json!({"type": "object", "properties": {}}),
};
if let Some(obj) = params.as_object_mut() {
match obj.get("type").and_then(|v| v.as_str()) {
Some("object") => {}
_ => {
obj.insert("type".to_string(), json!("object"));
}
}
}
params
}
fn responses_function_tool_to_chat_tool(tool: &Value, chat_name: &str) -> Option<Value> {
if tool.get("type").and_then(|v| v.as_str()) != Some("function") {
return None;
@@ -1110,6 +1130,14 @@ fn responses_function_tool_to_chat_tool(tool: &Value, chat_name: &str) -> Option
.get_mut("function")
.and_then(|value| value.as_object_mut())
{
// Ensure parameters.type is "object" for strict OpenAI-compatible providers
if let Some(params) = obj.get("parameters") {
let normalized = normalize_function_parameters(Some(params));
if normalized != *params {
obj.insert("parameters".to_string(), normalized);
}
}
obj.insert("name".to_string(), json!(chat_name));
if let Some(strict) = tool.get("strict").cloned() {
obj.entry("strict".to_string()).or_insert(strict);
@@ -1121,7 +1149,7 @@ fn responses_function_tool_to_chat_tool(tool: &Value, chat_name: &str) -> Option
let mut function = json!({
"name": chat_name,
"description": tool.get("description").cloned().unwrap_or(Value::Null),
"parameters": tool.get("parameters").cloned().unwrap_or_else(|| json!({}))
"parameters": normalize_function_parameters(tool.get("parameters"))
});
if let Some(strict) = tool.get("strict") {
function["strict"] = strict.clone();