fix(openai): preserve Responses API tool call/output linkage (#7759)

Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
Rabi Mishra
2026-03-12 01:38:46 +05:30
committed by GitHub
parent 7a14aa380d
commit 5b28f8fd3b
@@ -1,4 +1,5 @@
use crate::conversation::message::{Message, MessageContent};
use crate::mcp_utils::extract_text_from_resource;
use crate::model::ModelConfig;
use crate::providers::base::{ProviderUsage, Usage};
use crate::providers::utils::extract_reasoning_effort;
@@ -274,7 +275,8 @@ pub enum ResponseOutputItemInfo {
FunctionCall {
id: String,
status: String,
call_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
call_id: Option<String>,
name: String,
arguments: String,
},
@@ -362,29 +364,62 @@ fn add_message_items(input_items: &mut Vec<Value>, messages: &[Message]) {
match &response.tool_result {
Ok(contents) => {
let text_content: Vec<String> = contents
let has_images = contents
.content
.iter()
.filter_map(|c| {
if let RawContent::Text(t) = c.deref() {
Some(t.text.clone())
} else {
None
}
})
.collect();
.any(|c| matches!(c.deref(), RawContent::Image(_)));
if !text_content.is_empty() {
tracing::debug!(
"Sending function_call_output with call_id: {}",
response.id
);
input_items.push(json!({
"type": "function_call_output",
"call_id": response.id,
"output": text_content.join("\n")
}));
}
let output = if has_images {
json!(contents
.content
.iter()
.map(|c| match c.deref() {
RawContent::Text(t) => json!({
"type": "input_text", "text": t.text
}),
RawContent::Resource(r) => json!({
"type": "input_text",
"text": extract_text_from_resource(&r.resource)
}),
RawContent::Image(image) => json!({
"type": "input_image",
"image_url": format!(
"data:{};base64,{}",
image.mime_type, image.data
)
}),
RawContent::Audio(_) => json!({
"type": "input_text", "text": "[Audio content]"
}),
RawContent::ResourceLink(_) => json!({
"type": "input_text", "text": "[Resource link]"
}),
})
.collect::<Vec<Value>>())
} else {
json!(contents
.content
.iter()
.filter_map(|c| match c.deref() {
RawContent::Text(t) => Some(t.text.clone()),
RawContent::Resource(r) => {
Some(extract_text_from_resource(&r.resource))
}
RawContent::Audio(_) => Some("[Audio content]".into()),
RawContent::ResourceLink(_) => {
Some("[Resource link]".into())
}
RawContent::Image(_) => None,
})
.collect::<Vec<String>>()
.join("\n"))
};
input_items.push(json!({
"type": "function_call_output",
"call_id": response.id,
"output": output
}));
}
Err(error_data) => {
tracing::debug!(
@@ -518,11 +553,12 @@ pub fn responses_api_to_message(response: &ResponsesApiResponse) -> anyhow::Resu
}
ResponseOutputItem::FunctionCall {
id,
call_id,
name,
arguments,
..
} => {
tracing::debug!("Received FunctionCall with id: {}, name: {}", id, name);
let request_id = call_id.as_ref().unwrap_or(id).clone();
let parsed_args = if arguments.is_empty() {
json!({})
} else {
@@ -530,7 +566,7 @@ pub fn responses_api_to_message(response: &ResponsesApiResponse) -> anyhow::Resu
};
content.push(MessageContent::tool_request(
id.clone(),
request_id,
Ok(CallToolRequestParams::new(name.clone())
.with_arguments(object(parsed_args))),
));
@@ -595,11 +631,13 @@ fn process_streaming_output_items(
}
}
ResponseOutputItemInfo::FunctionCall {
id,
call_id,
name,
arguments,
..
} => {
let request_id = call_id.unwrap_or(id);
let parsed_args = if arguments.is_empty() {
json!({})
} else {
@@ -607,7 +645,7 @@ fn process_streaming_output_items(
};
content.push(MessageContent::tool_request(
call_id,
request_id,
Ok(CallToolRequestParams::new(name).with_arguments(object(parsed_args))),
));
}
@@ -1000,6 +1038,33 @@ mod tests {
);
}
#[test]
fn test_responses_api_to_message_uses_call_id_for_tool_request_id() {
let response = ResponsesApiResponse {
id: "resp_1".to_string(),
object: "response".to_string(),
created_at: 0,
status: "completed".to_string(),
model: "gpt-5.3-codex".to_string(),
output: vec![ResponseOutputItem::FunctionCall {
id: "fc_123".to_string(),
status: "completed".to_string(),
call_id: Some("call_abc".to_string()),
name: "test__get_person_zip_code".to_string(),
arguments: r#"{"name":"Alice Burns"}"#.to_string(),
}],
reasoning: None,
usage: None,
};
let message = responses_api_to_message(&response).unwrap();
assert_eq!(message.content.len(), 1);
let MessageContent::ToolRequest(tool_request) = &message.content[0] else {
panic!("expected tool request content");
};
assert_eq!(tool_request.id, "call_abc");
}
#[test]
fn test_deserialize_reasoning_info_with_null_effort() {
let json = r#"{"effort": null}"#;