use anyhow::{Result, anyhow};use collections::HashMap;use futures::{Stream, StreamExt};use language_model_core::{ CompactionContent, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelCustomToolFormat, LanguageModelCustomToolGrammarSyntax, LanguageModelImage, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestToolInput, LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, LanguageModelToolUseInput, MessageContent, Role, StopReason, TokenUsage, util::{fix_streamed_json, is_context_window_exceeded_message, parse_tool_arguments},};use std::pin::Pin;use std::sync::Arc; use crate::responses::{ ContextManagement, Request as ResponseRequest, ResponseCompactionItem, ResponseCustomToolCallItem, ResponseCustomToolCallOutputItem, ResponseError, ResponseFunctionCallItem, ResponseFunctionCallOutputContent, ResponseFunctionCallOutputItem, ResponseIncludable, ResponseInputContent, ResponseInputItem, ResponseMessageItem, ResponseOutputItem, ResponseOutputMessage, ResponseReasoningInputItem, ResponseReasoningItem, ResponseReasoningSummaryPart, ResponseSummary as ResponsesSummary, ResponseUsage as ResponsesUsage, StreamEvent as ResponsesStreamEvent,};use crate::{ FunctionContent, FunctionDefinition, ImageUrl, MessagePart, ReasoningEffort, ResponseStreamEvent, ServiceTier, ToolCall, ToolCallContent,}; const RESPONSE_MESSAGE_PHASE_COMMENTARY: &str = "commentary";const RESPONSE_MESSAGE_PHASE_FINAL_ANSWER: &str = "final_answer"; /// Translates the request's `Speed` into the corresponding OpenAI service tier./// Only `Fast` produces a value; `Standard` leaves the field unset so that the/// project's default tier applies.fn service_tier_for(speed: Option<language_model_core::Speed>) -> Option<ServiceTier> { match speed? { language_model_core::Speed::Fast => Some(ServiceTier::Priority), language_model_core::Speed::Standard => None, }} #[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum ChatCompletionMaxTokensParameter { MaxCompletionTokens, MaxTokens,} pub fn into_open_ai( request: LanguageModelRequest, model_id: &str, supports_parallel_tool_calls: bool, supports_prompt_cache_key: bool, max_output_tokens: Option<u64>, max_tokens_parameter: ChatCompletionMaxTokensParameter, reasoning_effort: Option<ReasoningEffort>, interleaved_reasoning: bool,) -> Result<crate::Request> { if request .tools .iter() .any(|tool| matches!(tool.input, LanguageModelRequestToolInput::Custom { .. })) { return Err(anyhow!( "OpenAI Chat Completions does not support custom tools; use Responses API instead" )); } let stream = !model_id.starts_with("o1-"); let service_tier = service_tier_for(request.speed); let mut messages = Vec::new(); let mut current_reasoning: Option<String> = None; for message in request.messages { for content in message.content { match content { MessageContent::Thinking { text, .. } if interleaved_reasoning => { current_reasoning.get_or_insert_default().push_str(&text); } MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { let should_add = if message.role == Role::User { // Including whitespace-only user messages can cause error with OpenAI compatible APIs // See https://github.com/zed-industries/zed/issues/40097 !text.trim().is_empty() } else { !text.is_empty() }; if should_add { add_message_content_part( MessagePart::Text { text }, message.role, &mut messages, ); if let Some(reasoning) = current_reasoning.take() { if let Some(crate::RequestMessage::Assistant { reasoning_content, .. }) = messages.last_mut() { *reasoning_content = Some(reasoning); } } } } MessageContent::RedactedThinking(_) | MessageContent::Compaction(_) => {} MessageContent::Image(image) => { add_message_content_part( MessagePart::Image { image_url: ImageUrl { url: image.to_base64_url(), detail: None, }, }, message.role, &mut messages, ); } MessageContent::ToolUse(tool_use) => { let LanguageModelToolUseInput::Json(input) = &tool_use.input else { return Err(anyhow!( "OpenAI Chat Completions cannot replay custom tool call `{}`", tool_use.name )); }; let tool_call = ToolCall { id: tool_use.id.to_string(), content: ToolCallContent::Function { function: FunctionContent { name: tool_use.name.to_string(), arguments: serde_json::to_string(input).unwrap_or_default(), }, }, }; if let Some(crate::RequestMessage::Assistant { tool_calls, .. }) = messages.last_mut() { tool_calls.push(tool_call); } else { messages.push(crate::RequestMessage::Assistant { content: None, tool_calls: vec![tool_call], reasoning_content: current_reasoning.take(), }); } } MessageContent::ToolResult(tool_result) => { let content: Vec<MessagePart> = tool_result .content .iter() .map(|part| match part { LanguageModelToolResultContent::Text(text) => MessagePart::Text { text: text.to_string(), }, LanguageModelToolResultContent::Image(image) => MessagePart::Image { image_url: ImageUrl { url: image.to_base64_url(), detail: None, }, }, }) .collect(); messages.push(crate::RequestMessage::Tool { content: content.into(), tool_call_id: tool_result.tool_use_id.to_string(), }); } } } } Ok(crate::Request { model: model_id.into(), messages, stream, stream_options: if stream { Some(crate::StreamOptions::default()) } else { None }, stop: request.stop, temperature: request.temperature.or(Some(1.0)), max_completion_tokens: match max_tokens_parameter { ChatCompletionMaxTokensParameter::MaxCompletionTokens => max_output_tokens, ChatCompletionMaxTokensParameter::MaxTokens => None, }, max_tokens: match max_tokens_parameter { ChatCompletionMaxTokensParameter::MaxCompletionTokens => None, ChatCompletionMaxTokensParameter::MaxTokens => max_output_tokens, }, parallel_tool_calls: if supports_parallel_tool_calls && !request.tools.is_empty() { Some(supports_parallel_tool_calls) } else { None }, prompt_cache_key: if supports_prompt_cache_key { request.thread_id } else { None }, tools: request .tools .into_iter() .map(|tool| match tool.input { LanguageModelRequestToolInput::Function { input_schema, .. } => { Ok(crate::ToolDefinition::Function { function: FunctionDefinition { name: tool.name, description: Some(tool.description), parameters: Some(input_schema), }, }) } LanguageModelRequestToolInput::Custom { .. } => Err(anyhow!( "OpenAI Chat Completions does not support custom tools; use Responses API instead" )), }) .collect::<Result<_>>()?, tool_choice: request.tool_choice.map(|choice| match choice { LanguageModelToolChoice::Auto => crate::ToolChoice::Auto, LanguageModelToolChoice::Any => crate::ToolChoice::Required, LanguageModelToolChoice::None => crate::ToolChoice::None, }), reasoning_effort, service_tier, })} pub fn into_open_ai_response( request: LanguageModelRequest, model_id: &str, supports_parallel_tool_calls: bool, supports_prompt_cache_key: bool, max_output_tokens: Option<u64>, default_reasoning_effort: Option<ReasoningEffort>, supports_none_reasoning_effort: bool,) -> ResponseRequest { let stream = !model_id.starts_with("o1-"); let LanguageModelRequest { thread_id, prompt_id: _, intent: _, messages, tools, tool_choice, stop: _, temperature, thinking_allowed, thinking_effort, speed, compact_at_tokens, } = request; let service_tier = service_tier_for(speed); let mut input_items = Vec::new(); let mut replayed_reasoning_item_indexes = HashMap::default(); let mut tool_use_kinds_by_id = HashMap::default(); for (index, message) in messages.into_iter().enumerate() { append_message_to_response_items( message, index, &mut replayed_reasoning_item_indexes, &mut tool_use_kinds_by_id, &mut input_items, ); } let tools: Vec<_> = tools .into_iter() .map(|tool| match tool.input { LanguageModelRequestToolInput::Function { input_schema, .. } => { crate::responses::ToolDefinition::Function { name: tool.name, description: Some(tool.description), parameters: Some(input_schema), strict: None, } } LanguageModelRequestToolInput::Custom { format } => { crate::responses::ToolDefinition::Custom { name: tool.name, description: if tool.description.is_empty() { None } else { Some(tool.description) }, format: format.map(custom_tool_format_into_open_ai), } } }) .collect(); let default_reasoning_effort = default_reasoning_effort.filter(|effort| *effort != ReasoningEffort::None); let reasoning_effort = if thinking_allowed { thinking_effort .as_deref() .and_then(|effort| effort.parse::<ReasoningEffort>().ok()) .filter(|effort| *effort != ReasoningEffort::None) .or(default_reasoning_effort) } else if supports_none_reasoning_effort { Some(ReasoningEffort::None) } else { None }; let reasoning = reasoning_effort.map(|effort| crate::responses::ReasoningConfig { effort, summary: if effort == ReasoningEffort::None { None } else { Some(crate::responses::ReasoningSummaryMode::Auto) }, }); let include = if reasoning .as_ref() .is_some_and(|reasoning| reasoning.effort != ReasoningEffort::None) || input_items .iter() .any(|item| matches!(item, ResponseInputItem::Reasoning(_))) { vec![ResponseIncludable::ReasoningEncryptedContent] } else { Vec::new() }; ResponseRequest { model: model_id.into(), instructions: None, input: input_items, store: Some(false), include, stream, temperature, top_p: None, max_output_tokens, parallel_tool_calls: if tools.is_empty() { None } else { Some(supports_parallel_tool_calls) }, tool_choice: tool_choice.map(|choice| match choice { LanguageModelToolChoice::Auto => crate::ToolChoice::Auto, LanguageModelToolChoice::Any => crate::ToolChoice::Required, LanguageModelToolChoice::None => crate::ToolChoice::None, }), tools, prompt_cache_key: if supports_prompt_cache_key { thread_id } else { None }, reasoning, service_tier, context_management: compact_at_tokens .map(|compact_threshold| vec![ContextManagement::Compaction { compact_threshold }]), }} fn append_message_to_response_items( message: LanguageModelRequestMessage, index: usize, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, tool_use_kinds_by_id: &mut HashMap<LanguageModelToolUseId, ReplayToolKind>, input_items: &mut Vec<ResponseInputItem>,) { let mut content_parts: Vec<ResponseInputContent> = Vec::new(); let LanguageModelRequestMessage { role, content, reasoning_details, .. } = message; let phase = if role == Role::Assistant { response_message_phase_from_details(reasoning_details.as_deref()) } else { None }; if role == Role::Assistant { append_reasoning_details_to_response_items( reasoning_details.as_deref(), replayed_reasoning_item_indexes, input_items, ); } for content in content { match content { MessageContent::Text(text) => { push_response_text_part(&role, text, &mut content_parts); } MessageContent::Thinking { .. } | MessageContent::RedactedThinking(_) => {} MessageContent::Compaction(CompactionContent::Encrypted { id, encrypted_content, }) => { flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, ); input_items.push(ResponseInputItem::Compaction(ResponseCompactionItem { id, encrypted_content, })); } // Summary compaction blocks come from other providers, and a // Pending block is a streaming-only UI signal; neither is replayed. MessageContent::Compaction( CompactionContent::Summary { .. } | CompactionContent::Pending, ) => {} MessageContent::Image(image) => { push_response_image_part(&role, image, &mut content_parts); } MessageContent::ToolUse(tool_use) => { flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, ); let call_id = tool_use.id.to_string(); match tool_use.input { LanguageModelToolUseInput::Json(_) => { tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Function); input_items.push(ResponseInputItem::FunctionCall( ResponseFunctionCallItem { call_id, name: tool_use.name.to_string(), arguments: tool_use.raw_input, }, )); } LanguageModelToolUseInput::Text(_) => { tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Custom); input_items.push(ResponseInputItem::CustomToolCall( ResponseCustomToolCallItem { id: None, call_id, name: tool_use.name.to_string(), input: tool_use.raw_input, }, )); } } } MessageContent::ToolResult(tool_result) => { flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, ); let output = match tool_result.content.as_slice() { [LanguageModelToolResultContent::Text(text)] => { ResponseFunctionCallOutputContent::Text(text.to_string()) } _ => { let parts = tool_result .content .into_iter() .map(|part| match part { LanguageModelToolResultContent::Text(text) => { ResponseInputContent::Text { text: text.to_string(), } } LanguageModelToolResultContent::Image(image) => { ResponseInputContent::Image { image_url: image.to_base64_url(), } } }) .collect(); ResponseFunctionCallOutputContent::List(parts) } }; match tool_use_kinds_by_id.get(&tool_result.tool_use_id) { Some(ReplayToolKind::Custom) => { input_items.push(ResponseInputItem::CustomToolCallOutput( ResponseCustomToolCallOutputItem { call_id: tool_result.tool_use_id.to_string(), output, }, )); } Some(ReplayToolKind::Function) | None => { input_items.push(ResponseInputItem::FunctionCallOutput( ResponseFunctionCallOutputItem { call_id: tool_result.tool_use_id.to_string(),Lines 1–500 of 4113. Load more
use anyhow::{Result, anyhow}; use collections::HashMap; use futures::{Stream, StreamExt}; use language_model_core::{ CompactionContent, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelCustomToolFormat, LanguageModelCustomToolGrammarSyntax, LanguageModelImage, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestToolInput, LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, LanguageModelToolUseInput, MessageContent, Role, StopReason, TokenUsage, util::{fix_streamed_json, is_context_window_exceeded_message, parse_tool_arguments}, }; use std::pin::Pin; use std::sync::Arc; use crate::responses::{ ContextManagement, Request as ResponseRequest, ResponseCompactionItem, ResponseCustomToolCallItem, ResponseCustomToolCallOutputItem, ResponseError, ResponseFunctionCallItem, ResponseFunctionCallOutputContent, ResponseFunctionCallOutputItem, ResponseIncludable, ResponseInputContent, ResponseInputItem, ResponseMessageItem, ResponseOutputItem, ResponseOutputMessage, ResponseReasoningInputItem, ResponseReasoningItem, ResponseReasoningSummaryPart, ResponseSummary as ResponsesSummary, ResponseUsage as ResponsesUsage, StreamEvent as ResponsesStreamEvent, }; use crate::{ FunctionContent, FunctionDefinition, ImageUrl, MessagePart, ReasoningEffort, ResponseStreamEvent, ServiceTier, ToolCall, ToolCallContent, }; const RESPONSE_MESSAGE_PHASE_COMMENTARY: &str = "commentary"; const RESPONSE_MESSAGE_PHASE_FINAL_ANSWER: &str = "final_answer"; /// Translates the request's `Speed` into the corresponding OpenAI service tier. /// Only `Fast` produces a value; `Standard` leaves the field unset so that the /// project's default tier applies. fn service_tier_for(speed: Option<language_model_core::Speed>) -> Option<ServiceTier> { match speed? { language_model_core::Speed::Fast => Some(ServiceTier::Priority), language_model_core::Speed::Standard => None, } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ChatCompletionMaxTokensParameter { MaxCompletionTokens, MaxTokens, } pub fn into_open_ai( request: LanguageModelRequest, model_id: &str, supports_parallel_tool_calls: bool, supports_prompt_cache_key: bool, max_output_tokens: Option<u64>, max_tokens_parameter: ChatCompletionMaxTokensParameter, reasoning_effort: Option<ReasoningEffort>, interleaved_reasoning: bool, ) -> Result<crate::Request> { if request .tools .iter() .any(|tool| matches!(tool.input, LanguageModelRequestToolInput::Custom { .. })) { return Err(anyhow!( "OpenAI Chat Completions does not support custom tools; use Responses API instead" )); } let stream = !model_id.starts_with("o1-"); let service_tier = service_tier_for(request.speed); let mut messages = Vec::new(); let mut current_reasoning: Option<String> = None; for message in request.messages { for content in message.content { match content { MessageContent::Thinking { text, .. } if interleaved_reasoning => { current_reasoning.get_or_insert_default().push_str(&text); } MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { let should_add = if message.role == Role::User { // Including whitespace-only user messages can cause error with OpenAI compatible APIs // See https://github.com/zed-industries/zed/issues/40097 !text.trim().is_empty() } else { !text.is_empty() }; if should_add { add_message_content_part( MessagePart::Text { text }, message.role, &mut messages, ); if let Some(reasoning) = current_reasoning.take() { if let Some(crate::RequestMessage::Assistant { reasoning_content, .. }) = messages.last_mut() { *reasoning_content = Some(reasoning); } } } } MessageContent::RedactedThinking(_) | MessageContent::Compaction(_) => {} MessageContent::Image(image) => { add_message_content_part( MessagePart::Image { image_url: ImageUrl { url: image.to_base64_url(), detail: None, }, }, message.role, &mut messages, ); } MessageContent::ToolUse(tool_use) => { let LanguageModelToolUseInput::Json(input) = &tool_use.input else { return Err(anyhow!( "OpenAI Chat Completions cannot replay custom tool call `{}`", tool_use.name )); }; let tool_call = ToolCall { id: tool_use.id.to_string(), content: ToolCallContent::Function { function: FunctionContent { name: tool_use.name.to_string(), arguments: serde_json::to_string(input).unwrap_or_default(), }, }, }; if let Some(crate::RequestMessage::Assistant { tool_calls, .. }) = messages.last_mut() { tool_calls.push(tool_call); } else { messages.push(crate::RequestMessage::Assistant { content: None, tool_calls: vec![tool_call], reasoning_content: current_reasoning.take(), }); } } MessageContent::ToolResult(tool_result) => { let content: Vec<MessagePart> = tool_result .content .iter() .map(|part| match part { LanguageModelToolResultContent::Text(text) => MessagePart::Text { text: text.to_string(), }, LanguageModelToolResultContent::Image(image) => MessagePart::Image { image_url: ImageUrl { url: image.to_base64_url(), detail: None, }, }, }) .collect(); messages.push(crate::RequestMessage::Tool { content: content.into(), tool_call_id: tool_result.tool_use_id.to_string(), }); } } } } Ok(crate::Request { model: model_id.into(), messages, stream, stream_options: if stream { Some(crate::StreamOptions::default()) } else { None }, stop: request.stop, temperature: request.temperature.or(Some(1.0)), max_completion_tokens: match max_tokens_parameter { ChatCompletionMaxTokensParameter::MaxCompletionTokens => max_output_tokens, ChatCompletionMaxTokensParameter::MaxTokens => None, }, max_tokens: match max_tokens_parameter { ChatCompletionMaxTokensParameter::MaxCompletionTokens => None, ChatCompletionMaxTokensParameter::MaxTokens => max_output_tokens, }, parallel_tool_calls: if supports_parallel_tool_calls && !request.tools.is_empty() { Some(supports_parallel_tool_calls) } else { None }, prompt_cache_key: if supports_prompt_cache_key { request.thread_id } else { None }, tools: request .tools .into_iter() .map(|tool| match tool.input { LanguageModelRequestToolInput::Function { input_schema, .. } => { Ok(crate::ToolDefinition::Function { function: FunctionDefinition { name: tool.name, description: Some(tool.description), parameters: Some(input_schema), }, }) } LanguageModelRequestToolInput::Custom { .. } => Err(anyhow!( "OpenAI Chat Completions does not support custom tools; use Responses API instead" )), }) .collect::<Result<_>>()?, tool_choice: request.tool_choice.map(|choice| match choice { LanguageModelToolChoice::Auto => crate::ToolChoice::Auto, LanguageModelToolChoice::Any => crate::ToolChoice::Required, LanguageModelToolChoice::None => crate::ToolChoice::None, }), reasoning_effort, service_tier, }) } pub fn into_open_ai_response( request: LanguageModelRequest, model_id: &str, supports_parallel_tool_calls: bool, supports_prompt_cache_key: bool, max_output_tokens: Option<u64>, default_reasoning_effort: Option<ReasoningEffort>, supports_none_reasoning_effort: bool, ) -> ResponseRequest { let stream = !model_id.starts_with("o1-"); let LanguageModelRequest { thread_id, prompt_id: _, intent: _, messages, tools, tool_choice, stop: _, temperature, thinking_allowed, thinking_effort, speed, compact_at_tokens, } = request; let service_tier = service_tier_for(speed); let mut input_items = Vec::new(); let mut replayed_reasoning_item_indexes = HashMap::default(); let mut tool_use_kinds_by_id = HashMap::default(); for (index, message) in messages.into_iter().enumerate() { append_message_to_response_items( message, index, &mut replayed_reasoning_item_indexes, &mut tool_use_kinds_by_id, &mut input_items, ); } let tools: Vec<_> = tools .into_iter() .map(|tool| match tool.input { LanguageModelRequestToolInput::Function { input_schema, .. } => { crate::responses::ToolDefinition::Function { name: tool.name, description: Some(tool.description), parameters: Some(input_schema), strict: None, } } LanguageModelRequestToolInput::Custom { format } => { crate::responses::ToolDefinition::Custom { name: tool.name, description: if tool.description.is_empty() { None } else { Some(tool.description) }, format: format.map(custom_tool_format_into_open_ai), } } }) .collect(); let default_reasoning_effort = default_reasoning_effort.filter(|effort| *effort != ReasoningEffort::None); let reasoning_effort = if thinking_allowed { thinking_effort .as_deref() .and_then(|effort| effort.parse::<ReasoningEffort>().ok()) .filter(|effort| *effort != ReasoningEffort::None) .or(default_reasoning_effort) } else if supports_none_reasoning_effort { Some(ReasoningEffort::None) } else { None }; let reasoning = reasoning_effort.map(|effort| crate::responses::ReasoningConfig { effort, summary: if effort == ReasoningEffort::None { None } else { Some(crate::responses::ReasoningSummaryMode::Auto) }, }); let include = if reasoning .as_ref() .is_some_and(|reasoning| reasoning.effort != ReasoningEffort::None) || input_items .iter() .any(|item| matches!(item, ResponseInputItem::Reasoning(_))) { vec![ResponseIncludable::ReasoningEncryptedContent] } else { Vec::new() }; ResponseRequest { model: model_id.into(), instructions: None, input: input_items, store: Some(false), include, stream, temperature, top_p: None, max_output_tokens, parallel_tool_calls: if tools.is_empty() { None } else { Some(supports_parallel_tool_calls) }, tool_choice: tool_choice.map(|choice| match choice { LanguageModelToolChoice::Auto => crate::ToolChoice::Auto, LanguageModelToolChoice::Any => crate::ToolChoice::Required, LanguageModelToolChoice::None => crate::ToolChoice::None, }), tools, prompt_cache_key: if supports_prompt_cache_key { thread_id } else { None }, reasoning, service_tier, context_management: compact_at_tokens .map(|compact_threshold| vec![ContextManagement::Compaction { compact_threshold }]), } } fn append_message_to_response_items( message: LanguageModelRequestMessage, index: usize, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, tool_use_kinds_by_id: &mut HashMap<LanguageModelToolUseId, ReplayToolKind>, input_items: &mut Vec<ResponseInputItem>, ) { let mut content_parts: Vec<ResponseInputContent> = Vec::new(); let LanguageModelRequestMessage { role, content, reasoning_details, .. } = message; let phase = if role == Role::Assistant { response_message_phase_from_details(reasoning_details.as_deref()) } else { None }; if role == Role::Assistant { append_reasoning_details_to_response_items( reasoning_details.as_deref(), replayed_reasoning_item_indexes, input_items, ); } for content in content { match content { MessageContent::Text(text) => { push_response_text_part(&role, text, &mut content_parts); } MessageContent::Thinking { .. } | MessageContent::RedactedThinking(_) => {} MessageContent::Compaction(CompactionContent::Encrypted { id, encrypted_content, }) => { flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, ); input_items.push(ResponseInputItem::Compaction(ResponseCompactionItem { id, encrypted_content, })); } // Summary compaction blocks come from other providers, and a // Pending block is a streaming-only UI signal; neither is replayed. MessageContent::Compaction( CompactionContent::Summary { .. } | CompactionContent::Pending, ) => {} MessageContent::Image(image) => { push_response_image_part(&role, image, &mut content_parts); } MessageContent::ToolUse(tool_use) => { flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, ); let call_id = tool_use.id.to_string(); match tool_use.input { LanguageModelToolUseInput::Json(_) => { tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Function); input_items.push(ResponseInputItem::FunctionCall( ResponseFunctionCallItem { call_id, name: tool_use.name.to_string(), arguments: tool_use.raw_input, }, )); } LanguageModelToolUseInput::Text(_) => { tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Custom); input_items.push(ResponseInputItem::CustomToolCall( ResponseCustomToolCallItem { id: None, call_id, name: tool_use.name.to_string(), input: tool_use.raw_input, }, )); } } } MessageContent::ToolResult(tool_result) => { flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, ); let output = match tool_result.content.as_slice() { [LanguageModelToolResultContent::Text(text)] => { ResponseFunctionCallOutputContent::Text(text.to_string()) } _ => { let parts = tool_result .content .into_iter() .map(|part| match part { LanguageModelToolResultContent::Text(text) => { ResponseInputContent::Text { text: text.to_string(), } } LanguageModelToolResultContent::Image(image) => { ResponseInputContent::Image { image_url: image.to_base64_url(), } } }) .collect(); ResponseFunctionCallOutputContent::List(parts) } }; match tool_use_kinds_by_id.get(&tool_result.tool_use_id) { Some(ReplayToolKind::Custom) => { input_items.push(ResponseInputItem::CustomToolCallOutput( ResponseCustomToolCallOutputItem { call_id: tool_result.tool_use_id.to_string(), output, }, )); } Some(ReplayToolKind::Function) | None => { input_items.push(ResponseInputItem::FunctionCallOutput( ResponseFunctionCallOutputItem { call_id: tool_result.tool_use_id.to_string(),