4708 lines · 167.5 KB · Rust
RawDownload
use anyhow::{Result, anyhow};use collections::HashMap;use futures::{Stream, StreamExt};use language_model_core::{    CompactedContext, CompactionUpdate, LanguageModelCompletionError, LanguageModelCompletionEvent,    LanguageModelCustomToolFormat, LanguageModelCustomToolGrammarSyntax, LanguageModelImage,    LanguageModelProviderId, 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, ResponseCustomToolCallItem,    ResponseCustomToolCallOutputItem, ResponseError, ResponseFunctionCallItem,    ResponseFunctionCallOutputContent, ResponseFunctionCallOutputItem, ResponseIncludable,    ResponseInputContent, ResponseInputItem, ResponseMessageItem, ResponseOutputItem,    ResponseOutputMessage, ResponseReasoningInputItem, ResponseReasoningItem,    ResponseReasoningSummaryPart, ResponseSummary as ResponsesSummary,    ResponseUsage as ResponsesUsage, StreamEvent as ResponsesStreamEvent,    provider_compaction_items, provider_compaction_state_from_items,};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,    })} /// `compaction_state_owner` identifies the backend this request will be sent/// to for the purpose of replaying provider-native compaction state. Multiple/// backends share this conversion, but their encrypted compaction items are/// not interchangeable, so only state owned by this id is replayed; state/// owned by any other backend falls back to replaying the full transcript.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,    compaction_state_owner: &LanguageModelProviderId,) -> Result<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 provider_items = Vec::new();    let mut input_items = Vec::new();    let mut replayed_reasoning_item_indexes = HashMap::default();    let mut tool_use_kinds_by_id = HashMap::default();    let mut system_instructions = Vec::new();    for (index, message) in messages.into_iter().enumerate() {        // System messages go to the top-level `instructions` field rather        // than the input item list. `instructions` is applied per request        // (the Responses API documents it as a system message inserted into        // the model's context), so the current system prompt survives when        // replaying provider compaction state replaces the accumulated input        // items below. This also matches the Codex backend, which rejects        // system-role input items outright.        if message.role == Role::System {            for content in message.content {                if let MessageContent::Text(text) = content                    && !text.trim().is_empty()                {                    system_instructions.push(text);                }            }            continue;        }        append_message_to_response_items(            message,            index,            compaction_state_owner,            &mut replayed_reasoning_item_indexes,            &mut tool_use_kinds_by_id,            &mut provider_items,            &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()    };     Ok(ResponseRequest {        model: model_id.into(),        instructions: if system_instructions.is_empty() {            None        } else {            Some(system_instructions.join("\n\n"))        },        input: crate::responses::ResponseInput::new(provider_items, 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,    compaction_state_owner: &LanguageModelProviderId,    replayed_reasoning_item_indexes: &mut HashMap<String, usize>,    tool_use_kinds_by_id: &mut HashMap<LanguageModelToolUseId, ReplayToolKind>,    provider_items: &mut Vec<serde_json::Value>,    input_items: &mut Vec<ResponseInputItem>,) -> Result<()> {    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(CompactedContext::ProviderState(state)) => {                // The canonical replacement window already encodes all                // context retained at the compaction point, so everything                // accumulated before it -- earlier input items, earlier parts                // of this same message, and replay bookkeeping -- is                // superseded and must not be resent. When a transcript                // contains multiple compactions, each later window supersedes                // the previous one, so the last compaction wins. State owned                // by another backend yields `None`, in which case we fall                // back to replaying the full transcript.                if let Some(items) = provider_compaction_items(&state, compaction_state_owner)? {                    content_parts.clear();                    input_items.clear();                    replayed_reasoning_item_indexes.clear();                    tool_use_kinds_by_id.clear();                    provider_items.clear();                    provider_items.extend(items);                }            }            MessageContent::Compaction(CompactedContext::Summary { .. }) => {}            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())                    }

Lines 1–500 of 4708.