mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(proxy): default Codex tool parameters to object schema (#5315)
Normalize function tool parameters on the Codex Responses -> Chat Completions bridge: default null/missing parameters (direct and nested function forms) to {"type":"object","properties":{}}, coerce explicit type:null, and add a root type for top-level oneOf schemas so strict OpenAI-compatible upstreams (DeepSeek etc.) no longer reject built-in Codex tools like codex_app__automation_update. Also hardens the Codex -> Anthropic tool path with the same object-type guarantee and adds regression coverage for all reported shapes.
This commit is contained in:
@@ -440,14 +440,17 @@ fn chat_tool_to_anthropic_tool(chat_tool: &Value) -> Option<Value> {
|
|||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())?;
|
.filter(|value| !value.is_empty())?;
|
||||||
let mut tool = json!({
|
let mut input_schema = function
|
||||||
"name": name,
|
|
||||||
"input_schema": function
|
|
||||||
.get("parameters")
|
.get("parameters")
|
||||||
.cloned()
|
.cloned()
|
||||||
.filter(|value| value.as_object().is_some_and(|object| !object.is_empty()))
|
.filter(|value| value.as_object().is_some_and(|object| !object.is_empty()))
|
||||||
.unwrap_or_else(|| json!({ "type": "object", "properties": {} }))
|
.unwrap_or_else(|| json!({ "type": "object", "properties": {} }));
|
||||||
});
|
if let Some(schema) = input_schema.as_object_mut() {
|
||||||
|
if schema.get("type").and_then(Value::as_str) != Some("object") {
|
||||||
|
schema.insert("type".to_string(), json!("object"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut tool = json!({ "name": name, "input_schema": input_schema });
|
||||||
if let Some(description) = function.get("description").and_then(|value| value.as_str()) {
|
if let Some(description) = function.get("description").and_then(|value| value.as_str()) {
|
||||||
tool["description"] = json!(description);
|
tool["description"] = json!(description);
|
||||||
}
|
}
|
||||||
@@ -1545,6 +1548,44 @@ mod tests {
|
|||||||
assert_eq!(tools[1]["name"], "apply_patch");
|
assert_eq!(tools[1]["name"], "apply_patch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_request_tool_search_output_schema_defaults_root_type_to_object() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "claude",
|
||||||
|
"max_output_tokens": 100,
|
||||||
|
"input": [
|
||||||
|
{ "role": "user", "content": "hi" },
|
||||||
|
{
|
||||||
|
"type": "tool_search_output",
|
||||||
|
"call_id": "tool_demo123",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"name": "demo_union_tool",
|
||||||
|
"parameters": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "object", "properties": { "mode": { "type": "string" } } },
|
||||||
|
{ "type": "object", "properties": { "name": { "type": "string" } } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = responses_request_to_anthropic(input, 4096).unwrap();
|
||||||
|
let input_schema = &result["tools"][0]["input_schema"];
|
||||||
|
|
||||||
|
assert_eq!(input_schema["type"], "object");
|
||||||
|
assert_eq!(
|
||||||
|
input_schema["oneOf"][0]["properties"]["mode"]["type"],
|
||||||
|
"string"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
input_schema["oneOf"][1]["properties"]["name"]["type"],
|
||||||
|
"string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_request_tool_choice_mapping() {
|
fn test_request_tool_choice_mapping() {
|
||||||
// A function tool must be present, else tool_choice is (correctly) dropped.
|
// A function tool must be present, else tool_choice is (correctly) dropped.
|
||||||
|
|||||||
@@ -1174,12 +1174,8 @@ fn responses_function_tool_to_chat_tool(tool: &Value, chat_name: &str) -> Option
|
|||||||
.and_then(|value| value.as_object_mut())
|
.and_then(|value| value.as_object_mut())
|
||||||
{
|
{
|
||||||
// Ensure parameters.type is "object" for strict OpenAI-compatible providers
|
// Ensure parameters.type is "object" for strict OpenAI-compatible providers
|
||||||
if let Some(params) = obj.get("parameters") {
|
let parameters = normalize_function_parameters(obj.get("parameters"));
|
||||||
let normalized = normalize_function_parameters(Some(params));
|
obj.insert("parameters".to_string(), parameters);
|
||||||
if normalized != *params {
|
|
||||||
obj.insert("parameters".to_string(), normalized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.insert("name".to_string(), json!(chat_name));
|
obj.insert("name".to_string(), json!(chat_name));
|
||||||
if let Some(strict) = tool.get("strict").cloned() {
|
if let Some(strict) = tool.get("strict").cloned() {
|
||||||
@@ -2054,6 +2050,140 @@ mod tests {
|
|||||||
assert_eq!(result["reasoning_effort"], "high");
|
assert_eq!(result["reasoning_effort"], "high");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_request_to_chat_defaults_null_tool_parameters() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"name": "codex_app__automation_update",
|
||||||
|
"description": "Update an automation.",
|
||||||
|
"parameters": null
|
||||||
|
}],
|
||||||
|
"input": "hi"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = responses_to_chat_completions(input).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result["tools"][0]["function"]["parameters"],
|
||||||
|
json!({"type": "object", "properties": {}})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_request_to_chat_defaults_nested_null_tool_parameters() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "codex_app__automation_update",
|
||||||
|
"description": "Update an automation.",
|
||||||
|
"parameters": null
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"input": "hi"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = responses_to_chat_completions(input).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result["tools"][0]["function"]["parameters"],
|
||||||
|
json!({"type": "object", "properties": {}})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_request_to_chat_defaults_nested_missing_tool_parameters() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "codex_app__automation_update",
|
||||||
|
"description": "Update an automation."
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"input": "hi"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = responses_to_chat_completions(input).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result["tools"][0]["function"]["parameters"],
|
||||||
|
json!({"type": "object", "properties": {}})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_request_to_chat_normalizes_explicit_null_tool_parameter_type() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"name": "search",
|
||||||
|
"parameters": {
|
||||||
|
"type": null,
|
||||||
|
"properties": {
|
||||||
|
"query": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"input": "hi"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = responses_to_chat_completions(input).unwrap();
|
||||||
|
let parameters = &result["tools"][0]["function"]["parameters"];
|
||||||
|
|
||||||
|
assert_eq!(parameters["type"], "object");
|
||||||
|
assert_eq!(parameters["properties"]["query"]["type"], "string");
|
||||||
|
assert_eq!(parameters["required"], json!(["query"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_request_to_chat_defaults_top_level_one_of_tool_parameters_to_object() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"name": "lookup",
|
||||||
|
"parameters": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"id": {"type": "string"}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"slug": {"type": "string"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"input": "hi"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = responses_to_chat_completions(input).unwrap();
|
||||||
|
let parameters = &result["tools"][0]["function"]["parameters"];
|
||||||
|
|
||||||
|
assert_eq!(parameters["type"], "object");
|
||||||
|
assert_eq!(
|
||||||
|
parameters["oneOf"],
|
||||||
|
json!([
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"id": {"type": "string"}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"slug": {"type": "string"}}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn responses_request_to_chat_exposes_tool_search_and_loaded_namespace_tools() {
|
fn responses_request_to_chat_exposes_tool_search_and_loaded_namespace_tools() {
|
||||||
let input = json!({
|
let input = json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user