use anyhow::{Result, anyhow};use collections::HashMap;use futures::{Stream, StreamExt};use language_model_core::{ LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason, TokenUsage, util::{fix_streamed_json, parse_tool_arguments},};use std::pin::Pin;use std::sync::Arc; use crate::responses::{ Request as ResponseRequest, 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, }} 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>, reasoning_effort: Option<ReasoningEffort>, interleaved_reasoning: bool,) -> crate::Request { 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::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 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(&tool_use.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(), }); } } } } 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: 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| crate::ToolDefinition::Function { function: FunctionDefinition { name: tool.name, description: Some(tool.description), parameters: Some(tool.input_schema), }, }) .collect(), 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, } = request; let service_tier = service_tier_for(speed); let mut input_items = Vec::new(); let mut replayed_reasoning_item_indexes = HashMap::default(); for (index, message) in messages.into_iter().enumerate() { append_message_to_response_items( message, index, &mut replayed_reasoning_item_indexes, &mut input_items, ); } let tools: Vec<_> = tools .into_iter() .map(|tool| crate::responses::ToolDefinition::Function { name: tool.name, description: Some(tool.description), parameters: Some(tool.input_schema), strict: None, }) .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, }} fn append_message_to_response_items( message: LanguageModelRequestMessage, index: usize, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, 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::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(); input_items.push(ResponseInputItem::FunctionCall(ResponseFunctionCallItem { call_id, name: tool_use.name.to_string(), arguments: 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) } }; input_items.push(ResponseInputItem::FunctionCallOutput( ResponseFunctionCallOutputItem { call_id: tool_result.tool_use_id.to_string(), output, }, )); } } } flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, );} fn append_reasoning_details_to_response_items( reasoning_details: Option<&serde_json::Value>, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, input_items: &mut Vec<ResponseInputItem>,) { let Some(reasoning_details) = reasoning_details else { return; }; let Some(metadata) = response_message_metadata_from_details(reasoning_details) else { return; }; for reasoning_item in metadata.reasoning_items { push_replayed_reasoning_item(reasoning_item, replayed_reasoning_item_indexes, input_items); }} fn push_replayed_reasoning_item( reasoning_item: ResponseReasoningInputItem, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, input_items: &mut Vec<ResponseInputItem>,) { if let Some(id) = reasoning_item.id.as_ref() { if let Some(index) = replayed_reasoning_item_indexes.get(id) { input_items[*index] = ResponseInputItem::Reasoning(reasoning_item); return; } replayed_reasoning_item_indexes.insert(id.clone(), input_items.len()); } input_items.push(ResponseInputItem::Reasoning(reasoning_item));} fn push_response_text_part( role: &Role, text: impl Into<String>, parts: &mut Vec<ResponseInputContent>,) { let text = text.into(); if text.trim().is_empty() { return; } match role { Role::Assistant => parts.push(ResponseInputContent::OutputText { text, annotations: Vec::new(), }), _ => parts.push(ResponseInputContent::Text { text }), }} fn push_response_image_part( role: &Role, image: LanguageModelImage, parts: &mut Vec<ResponseInputContent>,) { match role { Role::Assistant => parts.push(ResponseInputContent::OutputText { text: "[image omitted]".to_string(), annotations: Vec::new(), }), _ => parts.push(ResponseInputContent::Image { image_url: image.to_base64_url(), }), }} fn flush_response_parts( role: &Role, _index: usize, phase: Option<&str>, parts: &mut Vec<ResponseInputContent>, input_items: &mut Vec<ResponseInputItem>,) { if parts.is_empty() { return; } let item = ResponseInputItem::Message(ResponseMessageItem { role: match role { Role::User => crate::Role::User, Role::Assistant => crate::Role::Assistant, Role::System => crate::Role::System, }, content: parts.clone(), phase: match role { Role::Assistant => phase.map(str::to_string), Role::User | Role::System => None, }, });Lines 1–500 of 3274. Load more
use anyhow::{Result, anyhow}; use collections::HashMap; use futures::{Stream, StreamExt}; use language_model_core::{ LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason, TokenUsage, util::{fix_streamed_json, parse_tool_arguments}, }; use std::pin::Pin; use std::sync::Arc; use crate::responses::{ Request as ResponseRequest, 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, } } 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>, reasoning_effort: Option<ReasoningEffort>, interleaved_reasoning: bool, ) -> crate::Request { 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::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 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(&tool_use.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(), }); } } } } 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: 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| crate::ToolDefinition::Function { function: FunctionDefinition { name: tool.name, description: Some(tool.description), parameters: Some(tool.input_schema), }, }) .collect(), 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, } = request; let service_tier = service_tier_for(speed); let mut input_items = Vec::new(); let mut replayed_reasoning_item_indexes = HashMap::default(); for (index, message) in messages.into_iter().enumerate() { append_message_to_response_items( message, index, &mut replayed_reasoning_item_indexes, &mut input_items, ); } let tools: Vec<_> = tools .into_iter() .map(|tool| crate::responses::ToolDefinition::Function { name: tool.name, description: Some(tool.description), parameters: Some(tool.input_schema), strict: None, }) .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, } } fn append_message_to_response_items( message: LanguageModelRequestMessage, index: usize, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, 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::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(); input_items.push(ResponseInputItem::FunctionCall(ResponseFunctionCallItem { call_id, name: tool_use.name.to_string(), arguments: 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) } }; input_items.push(ResponseInputItem::FunctionCallOutput( ResponseFunctionCallOutputItem { call_id: tool_result.tool_use_id.to_string(), output, }, )); } } } flush_response_parts( &role, index, phase.as_deref(), &mut content_parts, input_items, ); } fn append_reasoning_details_to_response_items( reasoning_details: Option<&serde_json::Value>, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, input_items: &mut Vec<ResponseInputItem>, ) { let Some(reasoning_details) = reasoning_details else { return; }; let Some(metadata) = response_message_metadata_from_details(reasoning_details) else { return; }; for reasoning_item in metadata.reasoning_items { push_replayed_reasoning_item(reasoning_item, replayed_reasoning_item_indexes, input_items); } } fn push_replayed_reasoning_item( reasoning_item: ResponseReasoningInputItem, replayed_reasoning_item_indexes: &mut HashMap<String, usize>, input_items: &mut Vec<ResponseInputItem>, ) { if let Some(id) = reasoning_item.id.as_ref() { if let Some(index) = replayed_reasoning_item_indexes.get(id) { input_items[*index] = ResponseInputItem::Reasoning(reasoning_item); return; } replayed_reasoning_item_indexes.insert(id.clone(), input_items.len()); } input_items.push(ResponseInputItem::Reasoning(reasoning_item)); } fn push_response_text_part( role: &Role, text: impl Into<String>, parts: &mut Vec<ResponseInputContent>, ) { let text = text.into(); if text.trim().is_empty() { return; } match role { Role::Assistant => parts.push(ResponseInputContent::OutputText { text, annotations: Vec::new(), }), _ => parts.push(ResponseInputContent::Text { text }), } } fn push_response_image_part( role: &Role, image: LanguageModelImage, parts: &mut Vec<ResponseInputContent>, ) { match role { Role::Assistant => parts.push(ResponseInputContent::OutputText { text: "[image omitted]".to_string(), annotations: Vec::new(), }), _ => parts.push(ResponseInputContent::Image { image_url: image.to_base64_url(), }), } } fn flush_response_parts( role: &Role, _index: usize, phase: Option<&str>, parts: &mut Vec<ResponseInputContent>, input_items: &mut Vec<ResponseInputItem>, ) { if parts.is_empty() { return; } let item = ResponseInputItem::Message(ResponseMessageItem { role: match role { Role::User => crate::Role::User, Role::Assistant => crate::Role::Assistant, Role::System => crate::Role::System, }, content: parts.clone(), phase: match role { Role::Assistant => phase.map(str::to_string), Role::User | Role::System => None, }, });