diff --git a/crates/aether-ai-formats/src/formats/openai/responses/request.rs b/crates/aether-ai-formats/src/formats/openai/responses/request.rs index 30767515a..6d81abc02 100644 --- a/crates/aether-ai-formats/src/formats/openai/responses/request.rs +++ b/crates/aether-ai-formats/src/formats/openai/responses/request.rs @@ -9,7 +9,7 @@ use crate::{ }, protocol::canonical::{ canonical_response_format_to_openai_responses, canonical_tool_is_openai_custom, - canonical_tool_use_to_openai_responses_item, is_claude_messages_request, + canonical_tool_use_to_openai_responses_input_item, is_claude_messages_request, is_claude_system_instruction, is_claude_thinking_block, is_claude_tool_result, is_openai_responses_input_message, is_openai_thinking_block, media_data_or_url, namespace_extension_object, openai_content_text, openai_extensions, @@ -331,7 +331,7 @@ fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option let call_id = responses_tool_call_id(id, &mut next_generated_tool_call_index); let tool_name = responses_tool_name(name); pending_tool_call_ids.push_back(call_id.clone()); - input.push(canonical_tool_use_to_openai_responses_item( + input.push(canonical_tool_use_to_openai_responses_input_item( &call_id, &tool_name, arguments, extensions, )); } @@ -1430,7 +1430,7 @@ mod tests { assert_eq!(body["input"].as_array().expect("input").len(), 2); assert_eq!(body["input"][0]["type"], "function_call"); - assert_eq!(body["input"][0]["id"], "call_auto_0"); + assert!(body["input"][0].get("id").is_none()); assert_eq!(body["input"][0]["call_id"], "call_auto_0"); assert_eq!(body["input"][0]["name"], "unknown"); assert_eq!(body["input"][0]["arguments"], "{\"q\":\"rust\"}"); diff --git a/crates/aether-ai-formats/src/formats/registry.rs b/crates/aether-ai-formats/src/formats/registry.rs index 28151162b..4ed2ebbac 100644 --- a/crates/aether-ai-formats/src/formats/registry.rs +++ b/crates/aether-ai-formats/src/formats/registry.rs @@ -2609,7 +2609,7 @@ mod tests { .value; assert_eq!(converted["input"][0]["type"], "function_call"); - assert_eq!(converted["input"][0]["id"], "call_lookup_1"); + assert!(converted["input"][0].get("id").is_none()); assert_eq!(converted["input"][0]["call_id"], "call_lookup_1"); assert_eq!(converted["input"][1]["type"], "function_call_output"); assert_eq!(converted["input"][1]["call_id"], "call_lookup_1"); diff --git a/crates/aether-ai-formats/src/protocol/canonical.rs b/crates/aether-ai-formats/src/protocol/canonical.rs index 8e030631f..70877ab92 100644 --- a/crates/aether-ai-formats/src/protocol/canonical.rs +++ b/crates/aether-ai-formats/src/protocol/canonical.rs @@ -808,6 +808,66 @@ pub(crate) fn canonical_tool_use_to_openai_responses_item( }) } +pub(crate) fn canonical_tool_use_to_openai_responses_input_item( + id: &str, + name: &str, + input: &Value, + extensions: &BTreeMap, +) -> Value { + if let Some(item_type) = openai_responses_hosted_tool_call_item_type(extensions) { + let mut item = Map::new(); + item.insert("type".to_string(), Value::String(item_type.to_string())); + item.insert("id".to_string(), Value::String(id.to_string())); + item.insert("call_id".to_string(), Value::String(id.to_string())); + item.insert("status".to_string(), Value::String("completed".to_string())); + if let Some(input_object) = input.as_object() { + for field in openai_responses_hosted_tool_input_fields(item_type) { + if let Some(value) = input_object.get(*field) { + item.insert((*field).to_string(), value.clone()); + } + } + } else if item_type == "apply_patch_call" { + item.insert("operation".to_string(), input.clone()); + } else if !name.trim().is_empty() { + item.insert("name".to_string(), Value::String(name.to_string())); + } + return Value::Object(item); + } + if is_openai_custom_tool_call(extensions) { + let mut item = Map::new(); + item.insert( + "type".to_string(), + Value::String("custom_tool_call".to_string()), + ); + if let Some(item_id) = openai_responses_request_tool_call_item_id(extensions, "ctc") { + item.insert("id".to_string(), Value::String(item_id)); + } + item.insert("call_id".to_string(), Value::String(id.to_string())); + item.insert("status".to_string(), Value::String("completed".to_string())); + item.insert("name".to_string(), Value::String(name.to_string())); + item.insert( + "input".to_string(), + Value::String(openai_custom_tool_input_text(input)), + ); + return Value::Object(item); + } + let mut item = Map::new(); + item.insert( + "type".to_string(), + Value::String("function_call".to_string()), + ); + if let Some(item_id) = openai_responses_request_tool_call_item_id(extensions, "fc") { + item.insert("id".to_string(), Value::String(item_id)); + } + item.insert("call_id".to_string(), Value::String(id.to_string())); + item.insert("name".to_string(), Value::String(name.to_string())); + item.insert( + "arguments".to_string(), + Value::String(canonicalize_tool_arguments(input)), + ); + Value::Object(item) +} + fn openai_responses_tool_call_item_id( call_id: &str, extensions: &BTreeMap, @@ -828,6 +888,18 @@ fn openai_responses_tool_call_item_id( } } +fn openai_responses_request_tool_call_item_id( + extensions: &BTreeMap, + prefix: &str, +) -> Option { + openai_responses_extension(extensions) + .and_then(|value| value.get("item_id").or_else(|| value.get("id"))) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| value.starts_with(prefix)) + .map(ToString::to_string) +} + fn openai_responses_hosted_tool_call_item_type( extensions: &BTreeMap, ) -> Option<&str> {