fix(openai): preserve order in Responses API history (#7500)

Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
Rabi Mishra
2026-02-27 00:59:57 +05:30
committed by GitHub
parent 1abf05d422
commit 4082285da4
@@ -277,59 +277,37 @@ pub enum ContentPart {
}, },
} }
fn add_conversation_history(input_items: &mut Vec<Value>, messages: &[Message]) { fn add_message_items(input_items: &mut Vec<Value>, messages: &[Message]) {
for message in messages.iter().filter(|m| m.is_agent_visible()) { for message in messages.iter().filter(|m| m.is_agent_visible()) {
let has_only_tool_content = message.content.iter().all(|c| {
matches!(
c,
MessageContent::ToolRequest(_) | MessageContent::ToolResponse(_)
)
});
if has_only_tool_content {
continue;
}
if message.role != Role::User && message.role != Role::Assistant {
continue;
}
let role = match message.role { let role = match message.role {
Role::User => "user", Role::User => "user",
Role::Assistant => "assistant", Role::Assistant => "assistant",
}; };
let mut content_items = Vec::new(); let mut text_items = Vec::new();
for content in &message.content { for content in &message.content {
if let MessageContent::Text(text) = content { match content {
if !text.text.is_empty() { MessageContent::Text(text) if !text.text.is_empty() => {
let content_type = if message.role == Role::Assistant { let content_type = if message.role == Role::Assistant {
"output_text" "output_text"
} else { } else {
"input_text" "input_text"
}; };
content_items.push(json!({ text_items.push(json!({
"type": content_type, "type": content_type,
"text": text.text "text": text.text
})); }));
} }
} MessageContent::ToolRequest(request) if message.role == Role::Assistant => {
} if !text_items.is_empty() {
input_items.push(json!({
"role": role,
"content": text_items
}));
text_items = Vec::new();
}
if !content_items.is_empty() {
input_items.push(json!({
"role": role,
"content": content_items
}));
}
}
}
fn add_function_calls(input_items: &mut Vec<Value>, messages: &[Message]) {
for message in messages.iter().filter(|m| m.is_agent_visible()) {
if message.role == Role::Assistant {
for content in &message.content {
if let MessageContent::ToolRequest(request) = content {
if let Ok(tool_call) = &request.tool_call { if let Ok(tool_call) = &request.tool_call {
let arguments_str = tool_call let arguments_str = tool_call
.arguments .arguments
@@ -352,57 +330,64 @@ fn add_function_calls(input_items: &mut Vec<Value>, messages: &[Message]) {
})); }));
} }
} }
} MessageContent::ToolResponse(response) => {
} if !text_items.is_empty() {
} input_items.push(json!({
} "role": role,
"content": text_items
}));
text_items = Vec::new();
}
fn add_function_call_outputs(input_items: &mut Vec<Value>, messages: &[Message]) { match &response.tool_result {
for message in messages { Ok(contents) => {
for content in &message.content { let text_content: Vec<String> = contents
if let MessageContent::ToolResponse(response) = content { .content
match &response.tool_result { .iter()
Ok(contents) => { .filter_map(|c| {
let text_content: Vec<String> = contents if let RawContent::Text(t) = c.deref() {
.content Some(t.text.clone())
.iter() } else {
.filter_map(|c| { None
if let RawContent::Text(t) = c.deref() { }
Some(t.text.clone()) })
} else { .collect();
None
}
})
.collect();
if !text_content.is_empty() { 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")
}));
}
}
Err(error_data) => {
tracing::debug!( tracing::debug!(
"Sending function_call_output with call_id: {}", "Sending function_call_output error with call_id: {}",
response.id response.id
); );
input_items.push(json!({ input_items.push(json!({
"type": "function_call_output", "type": "function_call_output",
"call_id": response.id, "call_id": response.id,
"output": text_content.join("\n") "output": format!("Error: {}", error_data.message)
})); }));
} }
} }
Err(error_data) => {
// Handle error responses - must send them back to the API
// to avoid "No tool output found" errors
tracing::debug!(
"Sending function_call_output error with call_id: {}",
response.id
);
input_items.push(json!({
"type": "function_call_output",
"call_id": response.id,
"output": format!("Error: {}", error_data.message)
}));
}
} }
_ => {}
} }
} }
if !text_items.is_empty() {
input_items.push(json!({
"role": role,
"content": text_items
}));
}
} }
} }
@@ -424,9 +409,7 @@ pub fn create_responses_request(
})); }));
} }
add_conversation_history(&mut input_items, messages); add_message_items(&mut input_items, messages);
add_function_calls(&mut input_items, messages);
add_function_call_outputs(&mut input_items, messages);
let mut payload = json!({ let mut payload = json!({
"model": model_config.model_name, "model": model_config.model_name,
@@ -761,7 +744,10 @@ where
mod tests { mod tests {
use super::*; use super::*;
use crate::conversation::message::MessageContent; use crate::conversation::message::MessageContent;
use crate::model::ModelConfig;
use futures::StreamExt; use futures::StreamExt;
use rmcp::model::CallToolRequestParams;
use rmcp::object;
#[tokio::test] #[tokio::test]
async fn test_responses_stream_ignores_keepalive_event() -> anyhow::Result<()> { async fn test_responses_stream_ignores_keepalive_event() -> anyhow::Result<()> {
@@ -829,4 +815,60 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn test_history_preserves_chronological_order() {
let model_config = ModelConfig {
model_name: "gpt-5.2-codex".to_string(),
context_limit: None,
temperature: None,
max_tokens: None,
toolshim: false,
toolshim_model: None,
fast_model_config: None,
request_params: None,
};
let messages = vec![
Message::assistant()
.with_text("I'll create that file.")
.with_tool_request(
"call_1",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "shell".into(),
arguments: Some(object!({"command": "echo hello"})),
}),
),
Message::assistant()
.with_text("Now let me verify.")
.with_tool_request(
"call_2",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "shell".into(),
arguments: Some(object!({"command": "cat file.txt"})),
}),
),
];
let result = create_responses_request(&model_config, "", &messages, &[]).unwrap();
let input = result["input"].as_array().unwrap();
let types: Vec<&str> = input
.iter()
.map(|item| {
item.get("type")
.and_then(|v| v.as_str())
.unwrap_or_else(|| item["role"].as_str().unwrap())
})
.collect();
assert_eq!(
types,
vec!["assistant", "function_call", "assistant", "function_call"]
);
}
} }