Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:36:36.843Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

completion.rs

4709 lines · 171.6 KB · rust
1use anyhow::{Result, anyhow};
2use collections::HashMap;
3use futures::{Stream, StreamExt};
4use language_model_core::{
5    CompactedContext, CompactionUpdate, LanguageModelCompletionError, LanguageModelCompletionEvent,
6    LanguageModelCustomToolFormat, LanguageModelCustomToolGrammarSyntax, LanguageModelImage,
7    LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage,
8    LanguageModelRequestToolInput, LanguageModelToolChoice, LanguageModelToolResultContent,
9    LanguageModelToolUse, LanguageModelToolUseId, LanguageModelToolUseInput, MessageContent, Role,
10    StopReason, TokenUsage,
11    util::{fix_streamed_json, is_context_window_exceeded_message, parse_tool_arguments},
12};
13use std::pin::Pin;
14use std::sync::Arc;
15
16use crate::responses::{
17    ContextManagement, Request as ResponseRequest, ResponseCustomToolCallItem,
18    ResponseCustomToolCallOutputItem, ResponseError, ResponseFunctionCallItem,
19    ResponseFunctionCallOutputContent, ResponseFunctionCallOutputItem, ResponseIncludable,
20    ResponseInputContent, ResponseInputItem, ResponseMessageItem, ResponseOutputItem,
21    ResponseOutputMessage, ResponseReasoningInputItem, ResponseReasoningItem,
22    ResponseReasoningSummaryPart, ResponseSummary as ResponsesSummary,
23    ResponseUsage as ResponsesUsage, StreamEvent as ResponsesStreamEvent,
24    provider_compaction_items, provider_compaction_state_from_items,
25};
26use crate::{
27    FunctionContent, FunctionDefinition, ImageUrl, MessagePart, ReasoningEffort,
28    ResponseStreamEvent, ServiceTier, ToolCall, ToolCallContent,
29};
30
31const RESPONSE_MESSAGE_PHASE_COMMENTARY: &str = "commentary";
32const RESPONSE_MESSAGE_PHASE_FINAL_ANSWER: &str = "final_answer";
33
34/// Translates the request's `Speed` into the corresponding OpenAI service tier.
35/// Only `Fast` produces a value; `Standard` leaves the field unset so that the
36/// project's default tier applies.
37fn service_tier_for(speed: Option<language_model_core::Speed>) -> Option<ServiceTier> {
38    match speed? {
39        language_model_core::Speed::Fast => Some(ServiceTier::Priority),
40        language_model_core::Speed::Standard => None,
41    }
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum ChatCompletionMaxTokensParameter {
46    MaxCompletionTokens,
47    MaxTokens,
48}
49
50pub fn into_open_ai(
51    request: LanguageModelRequest,
52    model_id: &str,
53    supports_parallel_tool_calls: bool,
54    supports_prompt_cache_key: bool,
55    max_output_tokens: Option<u64>,
56    max_tokens_parameter: ChatCompletionMaxTokensParameter,
57    reasoning_effort: Option<ReasoningEffort>,
58    interleaved_reasoning: bool,
59) -> Result<crate::Request> {
60    if request
61        .tools
62        .iter()
63        .any(|tool| matches!(tool.input, LanguageModelRequestToolInput::Custom { .. }))
64    {
65        return Err(anyhow!(
66            "OpenAI Chat Completions does not support custom tools; use Responses API instead"
67        ));
68    }
69
70    let stream = !model_id.starts_with("o1-");
71    let service_tier = service_tier_for(request.speed);
72
73    let mut messages = Vec::new();
74    let mut current_reasoning: Option<String> = None;
75    for message in request.messages {
76        for content in message.content {
77            match content {
78                MessageContent::Thinking { text, .. } if interleaved_reasoning => {
79                    current_reasoning.get_or_insert_default().push_str(&text);
80                }
81                MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
82                    let should_add = if message.role == Role::User {
83                        // Including whitespace-only user messages can cause error with OpenAI compatible APIs
84                        // See https://github.com/zed-industries/zed/issues/40097
85                        !text.trim().is_empty()
86                    } else {
87                        !text.is_empty()
88                    };
89                    if should_add {
90                        add_message_content_part(
91                            MessagePart::Text { text },
92                            message.role,
93                            &mut messages,
94                        );
95                        if let Some(reasoning) = current_reasoning.take() {
96                            if let Some(crate::RequestMessage::Assistant {
97                                reasoning_content,
98                                ..
99                            }) = messages.last_mut()
100                            {
101                                *reasoning_content = Some(reasoning);
102                            }
103                        }
104                    }
105                }
106                MessageContent::RedactedThinking(_) | MessageContent::Compaction(_) => {}
107                MessageContent::Image(image) => {
108                    add_message_content_part(
109                        MessagePart::Image {
110                            image_url: ImageUrl {
111                                url: image.to_base64_url(),
112                                detail: None,
113                            },
114                        },
115                        message.role,
116                        &mut messages,
117                    );
118                }
119                MessageContent::ToolUse(tool_use) => {
120                    let LanguageModelToolUseInput::Json(input) = &tool_use.input else {
121                        return Err(anyhow!(
122                            "OpenAI Chat Completions cannot replay custom tool call `{}`",
123                            tool_use.name
124                        ));
125                    };
126                    let tool_call = ToolCall {
127                        id: tool_use.id.to_string(),
128                        content: ToolCallContent::Function {
129                            function: FunctionContent {
130                                name: tool_use.name.to_string(),
131                                arguments: serde_json::to_string(input).unwrap_or_default(),
132                            },
133                        },
134                    };
135
136                    if let Some(crate::RequestMessage::Assistant { tool_calls, .. }) =
137                        messages.last_mut()
138                    {
139                        tool_calls.push(tool_call);
140                    } else {
141                        messages.push(crate::RequestMessage::Assistant {
142                            content: None,
143                            tool_calls: vec![tool_call],
144                            reasoning_content: current_reasoning.take(),
145                        });
146                    }
147                }
148                MessageContent::ToolResult(tool_result) => {
149                    let content: Vec<MessagePart> = tool_result
150                        .content
151                        .iter()
152                        .map(|part| match part {
153                            LanguageModelToolResultContent::Text(text) => MessagePart::Text {
154                                text: text.to_string(),
155                            },
156                            LanguageModelToolResultContent::Image(image) => MessagePart::Image {
157                                image_url: ImageUrl {
158                                    url: image.to_base64_url(),
159                                    detail: None,
160                                },
161                            },
162                        })
163                        .collect();
164
165                    messages.push(crate::RequestMessage::Tool {
166                        content: content.into(),
167                        tool_call_id: tool_result.tool_use_id.to_string(),
168                    });
169                }
170            }
171        }
172    }
173
174    Ok(crate::Request {
175        model: model_id.into(),
176        messages,
177        stream,
178        stream_options: if stream {
179            Some(crate::StreamOptions::default())
180        } else {
181            None
182        },
183        stop: request.stop,
184        temperature: request.temperature.or(Some(1.0)),
185        max_completion_tokens: match max_tokens_parameter {
186            ChatCompletionMaxTokensParameter::MaxCompletionTokens => max_output_tokens,
187            ChatCompletionMaxTokensParameter::MaxTokens => None,
188        },
189        max_tokens: match max_tokens_parameter {
190            ChatCompletionMaxTokensParameter::MaxCompletionTokens => None,
191            ChatCompletionMaxTokensParameter::MaxTokens => max_output_tokens,
192        },
193        parallel_tool_calls: if supports_parallel_tool_calls && !request.tools.is_empty() {
194            Some(supports_parallel_tool_calls)
195        } else {
196            None
197        },
198        prompt_cache_key: if supports_prompt_cache_key {
199            request.thread_id
200        } else {
201            None
202        },
203        tools: request
204            .tools
205            .into_iter()
206            .map(|tool| match tool.input {
207                LanguageModelRequestToolInput::Function { input_schema, .. } => {
208                    Ok(crate::ToolDefinition::Function {
209                        function: FunctionDefinition {
210                            name: tool.name,
211                            description: Some(tool.description),
212                            parameters: Some(input_schema),
213                        },
214                    })
215                }
216                LanguageModelRequestToolInput::Custom { .. } => Err(anyhow!(
217                    "OpenAI Chat Completions does not support custom tools; use Responses API instead"
218                )),
219            })
220            .collect::<Result<_>>()?,
221        tool_choice: request.tool_choice.map(|choice| match choice {
222            LanguageModelToolChoice::Auto => crate::ToolChoice::Auto,
223            LanguageModelToolChoice::Any => crate::ToolChoice::Required,
224            LanguageModelToolChoice::None => crate::ToolChoice::None,
225        }),
226        reasoning_effort,
227        service_tier,
228    })
229}
230
231/// `compaction_state_owner` identifies the backend this request will be sent
232/// to for the purpose of replaying provider-native compaction state. Multiple
233/// backends share this conversion, but their encrypted compaction items are
234/// not interchangeable, so only state owned by this id is replayed; state
235/// owned by any other backend falls back to replaying the full transcript.
236pub fn into_open_ai_response(
237    request: LanguageModelRequest,
238    model_id: &str,
239    supports_parallel_tool_calls: bool,
240    supports_prompt_cache_key: bool,
241    max_output_tokens: Option<u64>,
242    default_reasoning_effort: Option<ReasoningEffort>,
243    supports_none_reasoning_effort: bool,
244    compaction_state_owner: &LanguageModelProviderId,
245) -> Result<ResponseRequest> {
246    let stream = !model_id.starts_with("o1-");
247
248    let LanguageModelRequest {
249        thread_id,
250        prompt_id: _,
251        intent: _,
252        messages,
253        tools,
254        tool_choice,
255        stop: _,
256        temperature,
257        thinking_allowed,
258        thinking_effort,
259        speed,
260        compact_at_tokens,
261    } = request;
262
263    let service_tier = service_tier_for(speed);
264
265    let mut provider_items = Vec::new();
266    let mut input_items = Vec::new();
267    let mut replayed_reasoning_item_indexes = HashMap::default();
268    let mut tool_use_kinds_by_id = HashMap::default();
269    let mut system_instructions = Vec::new();
270    for (index, message) in messages.into_iter().enumerate() {
271        // System messages go to the top-level `instructions` field rather
272        // than the input item list. `instructions` is applied per request
273        // (the Responses API documents it as a system message inserted into
274        // the model's context), so the current system prompt survives when
275        // replaying provider compaction state replaces the accumulated input
276        // items below. This also matches the Codex backend, which rejects
277        // system-role input items outright.
278        if message.role == Role::System {
279            for content in message.content {
280                if let MessageContent::Text(text) = content
281                    && !text.trim().is_empty()
282                {
283                    system_instructions.push(text);
284                }
285            }
286            continue;
287        }
288        append_message_to_response_items(
289            message,
290            index,
291            compaction_state_owner,
292            &mut replayed_reasoning_item_indexes,
293            &mut tool_use_kinds_by_id,
294            &mut provider_items,
295            &mut input_items,
296        )?;
297    }
298
299    let tools: Vec<_> = tools
300        .into_iter()
301        .map(|tool| match tool.input {
302            LanguageModelRequestToolInput::Function { input_schema, .. } => {
303                crate::responses::ToolDefinition::Function {
304                    name: tool.name,
305                    description: Some(tool.description),
306                    parameters: Some(input_schema),
307                    strict: None,
308                }
309            }
310            LanguageModelRequestToolInput::Custom { format } => {
311                crate::responses::ToolDefinition::Custom {
312                    name: tool.name,
313                    description: if tool.description.is_empty() {
314                        None
315                    } else {
316                        Some(tool.description)
317                    },
318                    format: format.map(custom_tool_format_into_open_ai),
319                }
320            }
321        })
322        .collect();
323
324    let default_reasoning_effort =
325        default_reasoning_effort.filter(|effort| *effort != ReasoningEffort::None);
326    let reasoning_effort = if thinking_allowed {
327        thinking_effort
328            .as_deref()
329            .and_then(|effort| effort.parse::<ReasoningEffort>().ok())
330            .filter(|effort| *effort != ReasoningEffort::None)
331            .or(default_reasoning_effort)
332    } else if supports_none_reasoning_effort {
333        Some(ReasoningEffort::None)
334    } else {
335        None
336    };
337
338    let reasoning = reasoning_effort.map(|effort| crate::responses::ReasoningConfig {
339        effort,
340        summary: if effort == ReasoningEffort::None {
341            None
342        } else {
343            Some(crate::responses::ReasoningSummaryMode::Auto)
344        },
345    });
346
347    let include = if reasoning
348        .as_ref()
349        .is_some_and(|reasoning| reasoning.effort != ReasoningEffort::None)
350        || input_items
351            .iter()
352            .any(|item| matches!(item, ResponseInputItem::Reasoning(_)))
353    {
354        vec![ResponseIncludable::ReasoningEncryptedContent]
355    } else {
356        Vec::new()
357    };
358
359    Ok(ResponseRequest {
360        model: model_id.into(),
361        instructions: if system_instructions.is_empty() {
362            None
363        } else {
364            Some(system_instructions.join("\n\n"))
365        },
366        input: crate::responses::ResponseInput::new(provider_items, input_items),
367        store: Some(false),
368        include,
369        stream,
370        temperature,
371        top_p: None,
372        max_output_tokens,
373        parallel_tool_calls: if tools.is_empty() {
374            None
375        } else {
376            Some(supports_parallel_tool_calls)
377        },
378        tool_choice: tool_choice.map(|choice| match choice {
379            LanguageModelToolChoice::Auto => crate::ToolChoice::Auto,
380            LanguageModelToolChoice::Any => crate::ToolChoice::Required,
381            LanguageModelToolChoice::None => crate::ToolChoice::None,
382        }),
383        tools,
384        prompt_cache_key: if supports_prompt_cache_key {
385            thread_id
386        } else {
387            None
388        },
389        reasoning,
390        service_tier,
391        context_management: compact_at_tokens
392            .map(|compact_threshold| vec![ContextManagement::Compaction { compact_threshold }]),
393    })
394}
395
396fn append_message_to_response_items(
397    message: LanguageModelRequestMessage,
398    index: usize,
399    compaction_state_owner: &LanguageModelProviderId,
400    replayed_reasoning_item_indexes: &mut HashMap<String, usize>,
401    tool_use_kinds_by_id: &mut HashMap<LanguageModelToolUseId, ReplayToolKind>,
402    provider_items: &mut Vec<serde_json::Value>,
403    input_items: &mut Vec<ResponseInputItem>,
404) -> Result<()> {
405    let mut content_parts: Vec<ResponseInputContent> = Vec::new();
406
407    let LanguageModelRequestMessage {
408        role,
409        content,
410        reasoning_details,
411        ..
412    } = message;
413    let phase = if role == Role::Assistant {
414        response_message_phase_from_details(reasoning_details.as_deref())
415    } else {
416        None
417    };
418
419    if role == Role::Assistant {
420        append_reasoning_details_to_response_items(
421            reasoning_details.as_deref(),
422            replayed_reasoning_item_indexes,
423            input_items,
424        );
425    }
426
427    for content in content {
428        match content {
429            MessageContent::Text(text) => {
430                push_response_text_part(&role, text, &mut content_parts);
431            }
432            MessageContent::Thinking { .. } | MessageContent::RedactedThinking(_) => {}
433            MessageContent::Compaction(CompactedContext::ProviderState(state)) => {
434                // The canonical replacement window already encodes all
435                // context retained at the compaction point, so everything
436                // accumulated before it -- earlier input items, earlier parts
437                // of this same message, and replay bookkeeping -- is
438                // superseded and must not be resent. When a transcript
439                // contains multiple compactions, each later window supersedes
440                // the previous one, so the last compaction wins. State owned
441                // by another backend yields `None`, in which case we fall
442                // back to replaying the full transcript.
443                if let Some(items) = provider_compaction_items(&state, compaction_state_owner)? {
444                    content_parts.clear();
445                    input_items.clear();
446                    replayed_reasoning_item_indexes.clear();
447                    tool_use_kinds_by_id.clear();
448                    provider_items.clear();
449                    provider_items.extend(items);
450                }
451            }
452            MessageContent::Compaction(CompactedContext::Summary { .. }) => {}
453            MessageContent::Image(image) => {
454                push_response_image_part(&role, image, &mut content_parts);
455            }
456            MessageContent::ToolUse(tool_use) => {
457                flush_response_parts(
458                    &role,
459                    index,
460                    phase.as_deref(),
461                    &mut content_parts,
462                    input_items,
463                );
464                let call_id = tool_use.id.to_string();
465                match tool_use.input {
466                    LanguageModelToolUseInput::Json(_) => {
467                        tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Function);
468                        input_items.push(ResponseInputItem::FunctionCall(
469                            ResponseFunctionCallItem {
470                                call_id,
471                                name: tool_use.name.to_string(),
472                                arguments: tool_use.raw_input,
473                            },
474                        ));
475                    }
476                    LanguageModelToolUseInput::Text(_) => {
477                        tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Custom);
478                        input_items.push(ResponseInputItem::CustomToolCall(
479                            ResponseCustomToolCallItem {
480                                id: None,
481                                call_id,
482                                name: tool_use.name.to_string(),
483                                input: tool_use.raw_input,
484                            },
485                        ));
486                    }
487                }
488            }
489            MessageContent::ToolResult(tool_result) => {
490                flush_response_parts(
491                    &role,
492                    index,
493                    phase.as_deref(),
494                    &mut content_parts,
495                    input_items,
496                );
497                let output = match tool_result.content.as_slice() {
498                    [LanguageModelToolResultContent::Text(text)] => {
499                        ResponseFunctionCallOutputContent::Text(text.to_string())
500                    }
501                    _ => {
502                        let parts = tool_result
503                            .content
504                            .into_iter()
505                            .map(|part| match part {
506                                LanguageModelToolResultContent::Text(text) => {
507                                    ResponseInputContent::Text {
508                                        text: text.to_string(),
509                                    }
510                                }
511                                LanguageModelToolResultContent::Image(image) => {
512                                    ResponseInputContent::Image {
513                                        image_url: image.to_base64_url(),
514                                    }
515                                }
516                            })
517                            .collect();
518                        ResponseFunctionCallOutputContent::List(parts)
519                    }
520                };
521                match tool_use_kinds_by_id.get(&tool_result.tool_use_id) {
522                    Some(ReplayToolKind::Custom) => {
523                        input_items.push(ResponseInputItem::CustomToolCallOutput(
524                            ResponseCustomToolCallOutputItem {
525                                call_id: tool_result.tool_use_id.to_string(),
526                                output,
527                            },
528                        ));
529                    }
530                    Some(ReplayToolKind::Function) | None => {
531                        input_items.push(ResponseInputItem::FunctionCallOutput(
532                            ResponseFunctionCallOutputItem {
533                                call_id: tool_result.tool_use_id.to_string(),
534                                output,
535                            },
536                        ));
537                    }
538                }
539            }
540        }
541    }
542
543    flush_response_parts(
544        &role,
545        index,
546        phase.as_deref(),
547        &mut content_parts,
548        input_items,
549    );
550    Ok(())
551}
552
553#[derive(Clone, Copy)]
554enum ReplayToolKind {
555    Function,
556    Custom,
557}
558
559fn custom_tool_format_into_open_ai(
560    format: LanguageModelCustomToolFormat,
561) -> crate::responses::CustomToolFormat {
562    match format {
563        LanguageModelCustomToolFormat::Text => crate::responses::CustomToolFormat::Text,
564        LanguageModelCustomToolFormat::Grammar { syntax, definition } => {
565            crate::responses::CustomToolFormat::Grammar {
566                syntax: match syntax {
567                    LanguageModelCustomToolGrammarSyntax::Lark => {
568                        crate::responses::CustomToolGrammarSyntax::Lark
569                    }
570                    LanguageModelCustomToolGrammarSyntax::Regex => {
571                        crate::responses::CustomToolGrammarSyntax::Regex
572                    }
573                },
574                definition,
575            }
576        }
577    }
578}
579
580fn append_reasoning_details_to_response_items(
581    reasoning_details: Option<&serde_json::Value>,
582    replayed_reasoning_item_indexes: &mut HashMap<String, usize>,
583    input_items: &mut Vec<ResponseInputItem>,
584) {
585    let Some(reasoning_details) = reasoning_details else {
586        return;
587    };
588
589    let Some(metadata) = response_message_metadata_from_details(reasoning_details) else {
590        return;
591    };
592
593    for reasoning_item in metadata.reasoning_items {
594        push_replayed_reasoning_item(reasoning_item, replayed_reasoning_item_indexes, input_items);
595    }
596}
597
598fn push_replayed_reasoning_item(
599    reasoning_item: ResponseReasoningInputItem,
600    replayed_reasoning_item_indexes: &mut HashMap<String, usize>,
601    input_items: &mut Vec<ResponseInputItem>,
602) {
603    if let Some(id) = reasoning_item.id.as_ref() {
604        if let Some(index) = replayed_reasoning_item_indexes.get(id) {
605            input_items[*index] = ResponseInputItem::Reasoning(reasoning_item);
606            return;
607        }
608
609        replayed_reasoning_item_indexes.insert(id.clone(), input_items.len());
610    }
611
612    input_items.push(ResponseInputItem::Reasoning(reasoning_item));
613}
614
615fn push_response_text_part(
616    role: &Role,
617    text: impl Into<String>,
618    parts: &mut Vec<ResponseInputContent>,
619) {
620    let text = text.into();
621    if text.trim().is_empty() {
622        return;
623    }
624
625    match role {
626        Role::Assistant => parts.push(ResponseInputContent::OutputText {
627            text,
628            annotations: Vec::new(),
629        }),
630        _ => parts.push(ResponseInputContent::Text { text }),
631    }
632}
633
634fn push_response_image_part(
635    role: &Role,
636    image: LanguageModelImage,
637    parts: &mut Vec<ResponseInputContent>,
638) {
639    match role {
640        Role::Assistant => parts.push(ResponseInputContent::OutputText {
641            text: "[image omitted]".to_string(),
642            annotations: Vec::new(),
643        }),
644        _ => parts.push(ResponseInputContent::Image {
645            image_url: image.to_base64_url(),
646        }),
647    }
648}
649
650fn flush_response_parts(
651    role: &Role,
652    _index: usize,
653    phase: Option<&str>,
654    parts: &mut Vec<ResponseInputContent>,
655    input_items: &mut Vec<ResponseInputItem>,
656) {
657    if parts.is_empty() {
658        return;
659    }
660
661    let item = ResponseInputItem::Message(ResponseMessageItem {
662        role: match role {
663            Role::User => crate::Role::User,
664            Role::Assistant => crate::Role::Assistant,
665            Role::System => crate::Role::System,
666        },
667        content: parts.clone(),
668        phase: match role {
669            Role::Assistant => phase.map(str::to_string),
670            Role::User | Role::System => None,
671        },
672    });
673
674    input_items.push(item);
675    parts.clear();
676}
677
678fn add_message_content_part(
679    new_part: MessagePart,
680    role: Role,
681    messages: &mut Vec<crate::RequestMessage>,
682) {
683    match (role, messages.last_mut()) {
684        (Role::User, Some(crate::RequestMessage::User { content }))
685        | (
686            Role::Assistant,
687            Some(crate::RequestMessage::Assistant {
688                content: Some(content),
689                ..
690            }),
691        )
692        | (Role::System, Some(crate::RequestMessage::System { content, .. })) => {
693            content.push_part(new_part);
694        }
695        _ => {
696            messages.push(match role {
697                Role::User => crate::RequestMessage::User {
698                    content: crate::MessageContent::from(vec![new_part]),
699                },
700                Role::Assistant => crate::RequestMessage::Assistant {
701                    content: Some(crate::MessageContent::from(vec![new_part])),
702                    tool_calls: Vec::new(),
703                    reasoning_content: None,
704                },
705                Role::System => crate::RequestMessage::System {
706                    content: crate::MessageContent::from(vec![new_part]),
707                },
708            });
709        }
710    }
711}
712
713pub struct OpenAiEventMapper {
714    tool_calls_by_index: HashMap<usize, RawToolCall>,
715}
716
717impl OpenAiEventMapper {
718    pub fn new() -> Self {
719        Self {
720            tool_calls_by_index: HashMap::default(),
721        }
722    }
723
724    pub fn map_stream(
725        mut self,
726        events: Pin<Box<dyn Send + Stream<Item = Result<ResponseStreamEvent>>>>,
727    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
728    {
729        events.flat_map(move |event| {
730            futures::stream::iter(match event {
731                Ok(event) => self.map_event(event),
732                Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
733            })
734        })
735    }
736
737    pub fn map_event(
738        &mut self,
739        event: ResponseStreamEvent,
740    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
741        let mut events = Vec::new();
742        if let Some(usage) = event.usage
743            && let Some(prompt_tokens) = usage.prompt_tokens
744            && let Some(completion_tokens) = usage.completion_tokens
745        {
746            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
747                input_tokens: prompt_tokens,
748                output_tokens: completion_tokens,
749                cache_creation_input_tokens: 0,
750                cache_read_input_tokens: 0,
751            })));
752        }
753
754        let Some(choice) = event.choices.first() else {
755            return events;
756        };
757
758        if let Some(delta) = choice.delta.as_ref() {
759            if let Some(reasoning) = delta.reasoning.clone() {
760                push_thinking_event(reasoning, &mut events);
761            }
762            if let Some(reasoning_content) = delta.reasoning_content.clone() {
763                push_thinking_event(reasoning_content, &mut events);
764            }
765            if let Some(content) = delta.content.clone() {
766                if !content.is_empty() {
767                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
768                }
769            }
770
771            if let Some(tool_calls) = delta.tool_calls.as_ref() {
772                for tool_call in tool_calls {
773                    let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
774
775                    if let Some(tool_id) = tool_call.id.clone()
776                        && !tool_id.is_empty()
777                    {
778                        entry.id = tool_id;
779                    }
780
781                    if let Some(function) = tool_call.function.as_ref() {
782                        if let Some(name) = function.name.clone()
783                            && !name.is_empty()
784                        {
785                            entry.name = name;
786                        }
787
788                        if let Some(arguments) = function.arguments.clone() {
789                            entry.arguments.push_str(&arguments);
790                        }
791                    }
792
793                    if !entry.id.is_empty() && !entry.name.is_empty() {
794                        if let Ok(input) = serde_json::from_str::<serde_json::Value>(
795                            &fix_streamed_json(&entry.arguments),
796                        ) {
797                            events.push(Ok(LanguageModelCompletionEvent::ToolUse(
798                                LanguageModelToolUse {
799                                    id: entry.id.clone().into(),
800                                    name: entry.name.as_str().into(),
801                                    is_input_complete: false,
802                                    input: LanguageModelToolUseInput::Json(input),
803                                    raw_input: entry.arguments.clone(),
804                                    thought_signature: None,
805                                },
806                            )));
807                        }
808                    }
809                }
810            }
811        }
812
813        match choice.finish_reason.as_deref() {
814            Some("stop") => {
815                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
816            }
817            Some("tool_calls") => {
818                events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
819                    match parse_tool_arguments(&tool_call.arguments) {
820                        Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
821                            LanguageModelToolUse {
822                                id: tool_call.id.clone().into(),
823                                name: tool_call.name.as_str().into(),
824                                is_input_complete: true,
825                                input: LanguageModelToolUseInput::Json(input),
826                                raw_input: tool_call.arguments.clone(),
827                                thought_signature: None,
828                            },
829                        )),
830                        Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
831                            id: tool_call.id.into(),
832                            tool_name: tool_call.name.into(),
833                            raw_input: tool_call.arguments.clone().into(),
834                            json_parse_error: error.to_string(),
835                        }),
836                    }
837                }));
838
839                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
840            }
841            Some(stop_reason) => {
842                log::error!("Unexpected OpenAI stop_reason: {stop_reason:?}",);
843                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
844            }
845            None => {}
846        }
847
848        events
849    }
850}
851
852fn push_thinking_event(
853    text: String,
854    events: &mut Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
855) {
856    if !text.is_empty() {
857        events.push(Ok(LanguageModelCompletionEvent::Thinking {
858            text,
859            signature: None,
860        }));
861    }
862}
863
864#[derive(Default)]
865struct RawToolCall {
866    id: String,
867    name: String,
868    arguments: String,
869}
870
871pub struct OpenAiResponseEventMapper {
872    /// The backend whose infrastructure produced this stream; stamped on any
873    /// compaction state it emits so replay is limited to the same backend.
874    compaction_state_owner: LanguageModelProviderId,
875    function_calls_by_item: HashMap<String, PendingResponseFunctionCall>,
876    custom_tool_calls_by_item: HashMap<String, PendingResponseCustomToolCall>,
877    reasoning_items: Vec<ResponseReasoningInputItem>,
878    current_message_phase: Option<String>,
879    pending_stop_reason: Option<StopReason>,
880    pending_compaction_items: usize,
881}
882
883#[derive(Default)]
884struct PendingResponseFunctionCall {
885    call_id: String,
886    name: Arc<str>,
887    arguments: String,
888}
889
890struct PendingResponseCustomToolCall {
891    call_id: String,
892    name: Arc<str>,
893    input: String,
894}
895
896impl OpenAiResponseEventMapper {
897    pub fn new(compaction_state_owner: LanguageModelProviderId) -> Self {
898        Self {
899            compaction_state_owner,
900            function_calls_by_item: HashMap::default(),
901            custom_tool_calls_by_item: HashMap::default(),
902            reasoning_items: Vec::new(),
903            current_message_phase: None,
904            pending_stop_reason: None,
905            pending_compaction_items: 0,
906        }
907    }
908
909    pub fn map_stream(
910        mut self,
911        events: Pin<Box<dyn Send + Stream<Item = Result<ResponsesStreamEvent>>>>,
912    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
913    {
914        events.flat_map(move |event| {
915            futures::stream::iter(match event {
916                Ok(event) => self.map_event(event),
917                Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
918            })
919        })
920    }
921
922    pub fn map_event(
923        &mut self,
924        event: ResponsesStreamEvent,
925    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
926        match event {
927            ResponsesStreamEvent::OutputItemAdded { item, .. } => {
928                let mut events = Vec::new();
929
930                match &item {
931                    ResponseOutputItem::Message(message) => {
932                        if let Some(id) = &message.id {
933                            events.push(Ok(LanguageModelCompletionEvent::StartMessage {
934                                message_id: id.clone(),
935                            }));
936                        }
937                        events.extend(self.capture_message_phase(message));
938                    }
939                    ResponseOutputItem::FunctionCall(function_call) => {
940                        if let Some(item_id) = function_call.id.clone() {
941                            let call_id = function_call
942                                .call_id
943                                .clone()
944                                .or_else(|| function_call.id.clone())
945                                .unwrap_or_else(|| item_id.clone());
946                            let entry = PendingResponseFunctionCall {
947                                call_id,
948                                name: Arc::<str>::from(
949                                    function_call.name.clone().unwrap_or_default(),
950                                ),
951                                arguments: function_call.arguments.clone(),
952                            };
953                            self.function_calls_by_item.insert(item_id, entry);
954                        }
955                    }
956                    ResponseOutputItem::CustomToolCall(custom_tool_call) => {
957                        if let Some(item_id) = custom_tool_call.id.clone() {
958                            let call_id = custom_tool_call
959                                .call_id
960                                .clone()
961                                .or_else(|| custom_tool_call.id.clone())
962                                .unwrap_or_else(|| item_id.clone());
963                            let entry = PendingResponseCustomToolCall {
964                                call_id,
965                                name: Arc::<str>::from(
966                                    custom_tool_call.name.clone().unwrap_or_default(),
967                                ),
968                                input: custom_tool_call.input.clone(),
969                            };
970                            self.custom_tool_calls_by_item.insert(item_id, entry);
971                        }
972                    }
973                    ResponseOutputItem::Compaction(_) => {
974                        self.pending_compaction_items += 1;
975                        events.push(Ok(LanguageModelCompletionEvent::Compaction(
976                            CompactionUpdate::Started,
977                        )));
978                    }
979                    ResponseOutputItem::Reasoning(_) | ResponseOutputItem::Unknown => {}
980                }
981                events
982            }
983            ResponsesStreamEvent::ReasoningSummaryTextDelta { delta, .. }
984            | ResponsesStreamEvent::ReasoningDelta { delta, .. } => {
985                if delta.is_empty() {
986                    Vec::new()
987                } else {
988                    vec![Ok(LanguageModelCompletionEvent::Thinking {
989                        text: delta,
990                        signature: None,
991                    })]
992                }
993            }
994            ResponsesStreamEvent::OutputTextDelta { delta, .. } => {
995                if delta.is_empty() {
996                    Vec::new()
997                } else {
998                    vec![Ok(LanguageModelCompletionEvent::Text(delta))]
999                }
1000            }
1001            ResponsesStreamEvent::RefusalDelta { .. }
1002            | ResponsesStreamEvent::RefusalDone { .. } => {
1003                self.pending_stop_reason = Some(StopReason::Refusal);
1004                Vec::new()
1005            }
1006            ResponsesStreamEvent::FunctionCallArgumentsDelta { item_id, delta, .. } => {
1007                if let Some(entry) = self.function_calls_by_item.get_mut(&item_id) {
1008                    entry.arguments.push_str(&delta);
1009                    if let Ok(input) = serde_json::from_str::<serde_json::Value>(
1010                        &fix_streamed_json(&entry.arguments),
1011                    ) {
1012                        return vec![Ok(LanguageModelCompletionEvent::ToolUse(
1013                            LanguageModelToolUse {
1014                                id: LanguageModelToolUseId::from(entry.call_id.clone()),
1015                                name: entry.name.clone(),
1016                                is_input_complete: false,
1017                                input: LanguageModelToolUseInput::Json(input),
1018                                raw_input: entry.arguments.clone(),
1019                                thought_signature: None,
1020                            },
1021                        ))];
1022                    }
1023                }
1024                Vec::new()
1025            }
1026            ResponsesStreamEvent::FunctionCallArgumentsDone {
1027                item_id, arguments, ..
1028            } => {
1029                if let Some(mut entry) = self.function_calls_by_item.remove(&item_id) {
1030                    if !arguments.is_empty() {
1031                        entry.arguments = arguments;
1032                    }
1033                    let raw_input = entry.arguments.clone();
1034                    self.pending_stop_reason = Some(StopReason::ToolUse);
1035                    match parse_tool_arguments(&entry.arguments) {
1036                        Ok(input) => {
1037                            vec![Ok(LanguageModelCompletionEvent::ToolUse(
1038                                LanguageModelToolUse {
1039                                    id: LanguageModelToolUseId::from(entry.call_id.clone()),
1040                                    name: entry.name.clone(),
1041                                    is_input_complete: true,
1042                                    input: LanguageModelToolUseInput::Json(input),
1043                                    raw_input,
1044                                    thought_signature: None,
1045                                },
1046                            ))]
1047                        }
1048                        Err(error) => {
1049                            vec![Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
1050                                id: LanguageModelToolUseId::from(entry.call_id.clone()),
1051                                tool_name: entry.name.clone(),
1052                                raw_input: Arc::<str>::from(raw_input),
1053                                json_parse_error: error.to_string(),
1054                            })]
1055                        }
1056                    }
1057                } else {
1058                    Vec::new()
1059                }
1060            }
1061            ResponsesStreamEvent::CustomToolCallInputDelta { item_id, delta, .. } => {
1062                if let Some(entry) = self.custom_tool_calls_by_item.get_mut(&item_id) {
1063                    entry.input.push_str(&delta);
1064                    return vec![Ok(LanguageModelCompletionEvent::ToolUse(
1065                        LanguageModelToolUse {
1066                            id: LanguageModelToolUseId::from(entry.call_id.clone()),
1067                            name: entry.name.clone(),
1068                            is_input_complete: false,
1069                            input: LanguageModelToolUseInput::Text(entry.input.clone()),
1070                            raw_input: entry.input.clone(),
1071                            thought_signature: None,
1072                        },
1073                    ))];
1074                }
1075                Vec::new()
1076            }
1077            ResponsesStreamEvent::CustomToolCallInputDone { item_id, input, .. } => {
1078                if let Some(entry) = self.custom_tool_calls_by_item.get_mut(&item_id)
1079                    && !input.is_empty()
1080                {
1081                    entry.input = input;
1082                }
1083                self.finish_pending_custom_tool_call(&item_id, None)
1084            }
1085            ResponsesStreamEvent::Completed { response } => {
1086                self.handle_completion(response, StopReason::EndTurn)
1087            }
1088            ResponsesStreamEvent::Incomplete { response } => {
1089                let reason = response
1090                    .incomplete_details
1091                    .as_ref()
1092                    .and_then(|details| details.reason.as_deref());
1093                let mut stop_reason = match reason {
1094                    Some("max_tokens" | "max_output_tokens") => StopReason::MaxTokens,
1095                    Some("content_filter") => {
1096                        self.pending_stop_reason = Some(StopReason::Refusal);
1097                        StopReason::Refusal
1098                    }
1099                    _ => self
1100                        .pending_stop_reason
1101                        .take()
1102                        .unwrap_or(StopReason::EndTurn),
1103                };
1104
1105                let mut events = Vec::new();
1106                events.extend(self.capture_reasoning_items_from_output(&response.output));
1107                if response_output_contains_refusal(&response.output)
1108                    && !matches!(stop_reason, StopReason::MaxTokens)
1109                {
1110                    self.pending_stop_reason = Some(StopReason::Refusal);
1111                    stop_reason = StopReason::Refusal;
1112                }
1113                if self.pending_stop_reason.is_none() {
1114                    events.extend(self.emit_tool_calls_from_output(&response.output));
1115                }
1116                if let Some(usage) = response.usage.as_ref() {
1117                    events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
1118                        token_usage_from_response_usage(usage),
1119                    )));
1120                }
1121                events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
1122                events
1123            }
1124            ResponsesStreamEvent::Failed { response } => match response.error.as_ref() {
1125                Some(error) => vec![Err(completion_error_from_response_error(error))],
1126                None => vec![Err(LanguageModelCompletionError::Other(anyhow!(
1127                    response_failure_message(&response)
1128                )))],
1129            },
1130            ResponsesStreamEvent::Error { error } => {
1131                vec![Err(completion_error_from_response_error(&error))]
1132            }
1133            ResponsesStreamEvent::GenericError { error } => {
1134                let error = error.into_response_error();
1135                vec![Err(completion_error_from_response_error(&error))]
1136            }
1137            ResponsesStreamEvent::ReasoningSummaryPartAdded { summary_index, .. } => {
1138                if summary_index > 0 {
1139                    vec![Ok(LanguageModelCompletionEvent::Thinking {
1140                        text: "\n\n".to_string(),
1141                        signature: None,
1142                    })]
1143                } else {
1144                    Vec::new()
1145                }
1146            }
1147            ResponsesStreamEvent::OutputItemDone { item, .. } => match item {
1148                ResponseOutputItem::Reasoning(reasoning) => self.capture_reasoning_item(&reasoning),
1149                ResponseOutputItem::Message(message) => self.capture_message_phase(&message),
1150                ResponseOutputItem::CustomToolCall(custom_tool_call) => {
1151                    if let Some(item_id) = custom_tool_call.id.as_ref() {
1152                        self.finish_pending_custom_tool_call(item_id, Some(&custom_tool_call))
1153                    } else {
1154                        Vec::new()
1155                    }
1156                }
1157                ResponseOutputItem::Compaction(compaction) => {
1158                    self.pending_compaction_items = self.pending_compaction_items.saturating_sub(1);
1159                    match serde_json::to_value(ResponseInputItem::Compaction(compaction))
1160                        .map_err(anyhow::Error::from)
1161                        .and_then(|item| {
1162                            provider_compaction_state_from_items(
1163                                self.compaction_state_owner.clone(),
1164                                vec![item],
1165                            )
1166                        }) {
1167                        Ok(state) => vec![Ok(LanguageModelCompletionEvent::Compaction(
1168                            CompactionUpdate::Finished(CompactedContext::ProviderState(state)),
1169                        ))],
1170                        Err(error) => vec![Err(LanguageModelCompletionError::Other(error))],
1171                    }
1172                }
1173                ResponseOutputItem::FunctionCall(_) | ResponseOutputItem::Unknown => Vec::new(),
1174            },
1175            ResponsesStreamEvent::OutputTextDone { .. }
1176            | ResponsesStreamEvent::ContentPartAdded { .. }
1177            | ResponsesStreamEvent::ContentPartDone { .. }
1178            | ResponsesStreamEvent::ReasoningSummaryTextDone { .. }
1179            | ResponsesStreamEvent::ReasoningSummaryPartDone { .. }
1180            | ResponsesStreamEvent::ReasoningDone { .. }
1181            | ResponsesStreamEvent::Created { .. }
1182            | ResponsesStreamEvent::InProgress { .. }
1183            | ResponsesStreamEvent::Unknown => Vec::new(),
1184        }
1185    }
1186
1187    fn handle_completion(
1188        &mut self,
1189        response: ResponsesSummary,
1190        default_reason: StopReason,
1191    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1192        // A compaction item that was added but never done means the server
1193        // already pruned its context, but we never received the canonical
1194        // replacement window. Continuing as if the turn succeeded would
1195        // leave the conversation unable to continue coherently.
1196        if self.pending_compaction_items > 0 {
1197            return vec![Err(LanguageModelCompletionError::Other(anyhow!(
1198                "response completed with an unfinished compaction item"
1199            )))];
1200        }
1201
1202        let mut events = Vec::new();
1203
1204        events.extend(self.capture_reasoning_items_from_output(&response.output));
1205
1206        if response_output_contains_refusal(&response.output) {
1207            self.pending_stop_reason = Some(StopReason::Refusal);
1208        }
1209
1210        if self.pending_stop_reason.is_none() {
1211            events.extend(self.emit_tool_calls_from_output(&response.output));
1212        }
1213
1214        if let Some(usage) = response.usage.as_ref() {
1215            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
1216                token_usage_from_response_usage(usage),
1217            )));
1218        }
1219
1220        let stop_reason = self.pending_stop_reason.take().unwrap_or(default_reason);
1221        events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
1222        events
1223    }
1224
1225    fn emit_tool_calls_from_output(
1226        &mut self,
1227        output: &[ResponseOutputItem],
1228    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1229        let mut events = Vec::new();
1230        for item in output {
1231            match item {
1232                ResponseOutputItem::FunctionCall(function_call) => {
1233                    let Some(call_id) = function_call
1234                        .call_id
1235                        .clone()
1236                        .or_else(|| function_call.id.clone())
1237                    else {
1238                        log::error!(
1239                            "Function call item missing both call_id and id: {:?}",
1240                            function_call
1241                        );
1242                        continue;
1243                    };
1244                    let name: Arc<str> = Arc::from(function_call.name.clone().unwrap_or_default());
1245                    let arguments = &function_call.arguments;
1246                    self.pending_stop_reason = Some(StopReason::ToolUse);
1247                    match parse_tool_arguments(arguments) {
1248                        Ok(input) => {
1249                            events.push(Ok(LanguageModelCompletionEvent::ToolUse(
1250                                LanguageModelToolUse {
1251                                    id: LanguageModelToolUseId::from(call_id.clone()),
1252                                    name: name.clone(),
1253                                    is_input_complete: true,
1254                                    input: LanguageModelToolUseInput::Json(input),
1255                                    raw_input: arguments.clone(),
1256                                    thought_signature: None,
1257                                },
1258                            )));
1259                        }
1260                        Err(error) => {
1261                            events.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
1262                                id: LanguageModelToolUseId::from(call_id.clone()),
1263                                tool_name: name.clone(),
1264                                raw_input: Arc::<str>::from(arguments.clone()),
1265                                json_parse_error: error.to_string(),
1266                            }));
1267                        }
1268                    }
1269                }
1270                ResponseOutputItem::CustomToolCall(custom_tool_call) => {
1271                    events.extend(self.emit_custom_tool_call(custom_tool_call));
1272                }
1273                _ => {}
1274            }
1275        }
1276        events
1277    }
1278
1279    fn emit_custom_tool_call(
1280        &mut self,
1281        custom_tool_call: &crate::responses::ResponseCustomToolCall,
1282    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1283        let Some(call_id) = custom_tool_call
1284            .call_id
1285            .clone()
1286            .or_else(|| custom_tool_call.id.clone())
1287        else {
1288            log::error!(
1289                "Custom tool call item missing both call_id and id: {:?}",
1290                custom_tool_call
1291            );
1292            return Vec::new();
1293        };
1294        self.pending_stop_reason = Some(StopReason::ToolUse);
1295        let input = custom_tool_call.input.clone();
1296        vec![Ok(LanguageModelCompletionEvent::ToolUse(
1297            LanguageModelToolUse {
1298                id: LanguageModelToolUseId::from(call_id),
1299                name: Arc::from(custom_tool_call.name.clone().unwrap_or_default()),
1300                is_input_complete: true,
1301                input: LanguageModelToolUseInput::Text(input.clone()),
1302                raw_input: input,
1303                thought_signature: None,
1304            },
1305        ))]
1306    }
1307
1308    fn finish_pending_custom_tool_call(
1309        &mut self,
1310        item_id: &str,
1311        fallback: Option<&crate::responses::ResponseCustomToolCall>,
1312    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1313        let Some(mut entry) = self.custom_tool_calls_by_item.remove(item_id) else {
1314            return Vec::new();
1315        };
1316        if let Some(fallback) = fallback
1317            && !fallback.input.is_empty()
1318        {
1319            entry.input = fallback.input.clone();
1320        }
1321        self.pending_stop_reason = Some(StopReason::ToolUse);
1322        vec![Ok(LanguageModelCompletionEvent::ToolUse(
1323            LanguageModelToolUse {
1324                id: LanguageModelToolUseId::from(entry.call_id),
1325                name: entry.name,
1326                is_input_complete: true,
1327                input: LanguageModelToolUseInput::Text(entry.input.clone()),
1328                raw_input: entry.input,
1329                thought_signature: None,
1330            },
1331        ))]
1332    }
1333
1334    fn capture_reasoning_items_from_output(
1335        &mut self,
1336        output: &[ResponseOutputItem],
1337    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1338        let mut events = Vec::new();
1339        for item in output {
1340            if let ResponseOutputItem::Reasoning(reasoning) = item {
1341                events.extend(self.capture_reasoning_item(reasoning));
1342            }
1343        }
1344        events
1345    }
1346
1347    fn capture_message_phase(
1348        &mut self,
1349        message: &ResponseOutputMessage,
1350    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1351        self.current_message_phase = message
1352            .phase
1353            .as_deref()
1354            .and_then(normalize_response_message_phase)
1355            .map(str::to_string);
1356
1357        if self.current_message_phase.is_none() && self.reasoning_items.is_empty() {
1358            return Vec::new();
1359        }
1360
1361        self.emit_response_message_metadata()
1362    }
1363
1364    fn capture_reasoning_item(
1365        &mut self,
1366        reasoning: &ResponseReasoningItem,
1367    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1368        let reasoning_item = response_reasoning_input_item_from_output(reasoning);
1369
1370        if self.reasoning_items.contains(&reasoning_item) {
1371            return Vec::new();
1372        }
1373
1374        if let Some(id) = reasoning_item.id.as_ref()
1375            && let Some(existing_reasoning_item) = self
1376                .reasoning_items
1377                .iter_mut()
1378                .find(|existing_reasoning_item| existing_reasoning_item.id.as_ref() == Some(id))
1379        {
1380            *existing_reasoning_item = reasoning_item;
1381        } else {
1382            self.reasoning_items.push(reasoning_item);
1383        }
1384
1385        self.emit_response_message_metadata()
1386    }
1387
1388    fn emit_response_message_metadata(
1389        &self,
1390    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1391        let details = serde_json::to_value(ResponseMessageMetadata {
1392            phase: self.current_message_phase.clone(),
1393            reasoning_items: self.reasoning_items.clone(),
1394        });
1395
1396        match details {
1397            Ok(details) => vec![Ok(LanguageModelCompletionEvent::ReasoningDetails(details))],
1398            Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
1399        }
1400    }
1401}
1402
1403#[derive(serde::Serialize, serde::Deserialize)]
1404struct ResponseMessageMetadata {
1405    #[serde(default, skip_serializing_if = "Option::is_none")]
1406    phase: Option<String>,
1407    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1408    reasoning_items: Vec<ResponseReasoningInputItem>,
1409}
1410
1411fn response_message_metadata_from_details(
1412    details: &serde_json::Value,
1413) -> Option<ResponseMessageMetadata> {
1414    serde_json::from_value::<ResponseMessageMetadata>(details.clone()).ok()
1415}
1416
1417fn response_message_phase_from_details(details: Option<&serde_json::Value>) -> Option<String> {
1418    let metadata = response_message_metadata_from_details(details?)?;
1419    metadata
1420        .phase
1421        .as_deref()
1422        .and_then(normalize_response_message_phase)
1423        .map(str::to_string)
1424}
1425
1426fn normalize_response_message_phase(phase: &str) -> Option<&'static str> {
1427    match phase {
1428        RESPONSE_MESSAGE_PHASE_COMMENTARY => Some(RESPONSE_MESSAGE_PHASE_COMMENTARY),
1429        RESPONSE_MESSAGE_PHASE_FINAL_ANSWER => Some(RESPONSE_MESSAGE_PHASE_FINAL_ANSWER),
1430        _ => None,
1431    }
1432}
1433
1434fn response_failure_message(response: &ResponsesSummary) -> String {
1435    if let Some(error) = response.error.as_ref() {
1436        return response_error_message(error);
1437    }
1438
1439    response
1440        .status
1441        .as_deref()
1442        .map(|status| format!("response.{status}"))
1443        .unwrap_or_else(|| "response.failed".to_string())
1444}
1445
1446fn completion_error_from_response_error(error: &ResponseError) -> LanguageModelCompletionError {
1447    let message = response_error_message(error);
1448    if is_context_window_exceeded_message(&message) {
1449        LanguageModelCompletionError::PromptTooLarge { tokens: None }
1450    } else {
1451        LanguageModelCompletionError::Other(anyhow!(message))
1452    }
1453}
1454
1455fn response_error_message(error: &ResponseError) -> String {
1456    let code = error.code.as_deref().filter(|code| !code.trim().is_empty());
1457    let message = error.message.trim();
1458
1459    match (code, message.is_empty()) {
1460        (Some(code), false) => format!("{code}: {message}"),
1461        (Some(code), true) => code.to_string(),
1462        (None, false) => message.to_string(),
1463        (None, true) => "response error".to_string(),
1464    }
1465}
1466
1467fn response_output_contains_refusal(output: &[ResponseOutputItem]) -> bool {
1468    output.iter().any(|item| {
1469        if let ResponseOutputItem::Message(message) = item {
1470            message.content.iter().any(response_content_is_refusal)
1471        } else {
1472            false
1473        }
1474    })
1475}
1476
1477fn response_content_is_refusal(content: &serde_json::Value) -> bool {
1478    let content_type = content
1479        .get("type")
1480        .and_then(|content_type| content_type.as_str());
1481    let refusal = content
1482        .get("refusal")
1483        .and_then(|refusal| refusal.as_str())
1484        .unwrap_or_default();
1485
1486    content_type == Some("refusal") || !refusal.is_empty()
1487}
1488
1489pub fn token_usage_from_response_usage(usage: &ResponsesUsage) -> TokenUsage {
1490    let cache_read_input_tokens = usage.input_tokens_details.cached_tokens;
1491
1492    TokenUsage {
1493        input_tokens: usage
1494            .input_tokens
1495            .unwrap_or_default()
1496            .saturating_sub(cache_read_input_tokens),
1497        output_tokens: usage.output_tokens.unwrap_or_default(),
1498        cache_creation_input_tokens: 0,
1499        cache_read_input_tokens,
1500    }
1501}
1502
1503fn response_reasoning_input_item_from_output(
1504    reasoning: &ResponseReasoningItem,
1505) -> ResponseReasoningInputItem {
1506    let encrypted_content = reasoning.encrypted_content.clone();
1507
1508    let summary = reasoning
1509        .summary
1510        .iter()
1511        .filter_map(|part| match part {
1512            crate::responses::ReasoningSummaryPart::SummaryText { text } => {
1513                Some(ResponseReasoningSummaryPart::SummaryText { text: text.clone() })
1514            }
1515            crate::responses::ReasoningSummaryPart::Unknown => None,
1516        })
1517        .collect();
1518
1519    ResponseReasoningInputItem {
1520        id: reasoning.id.clone(),
1521        summary,
1522        content: reasoning.content.clone(),
1523        encrypted_content,
1524        status: reasoning.status.clone(),
1525    }
1526}
1527
1528#[cfg(test)]
1529mod tests {
1530    use crate::responses::{
1531        ReasoningSummaryPart, ResponseCustomToolCall, ResponseError, ResponseFunctionToolCall,
1532        ResponseIncompleteDetails, ResponseInputItem, ResponseInputTokensDetails,
1533        ResponseOutputItem, ResponseOutputMessage, ResponseReasoningItem, ResponseSummary,
1534        ResponseUsage, StreamEvent as ResponsesStreamEvent, ToolDefinition,
1535    };
1536    use futures::{StreamExt, executor::block_on};
1537    use language_model_core::{
1538        LanguageModelCustomToolFormat, LanguageModelCustomToolGrammarSyntax, LanguageModelImage,
1539        LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelRequestToolInput,
1540        LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
1541        LanguageModelToolUseId, LanguageModelToolUseInput, OPEN_AI_PROVIDER_ID, SharedString,
1542        Speed,
1543    };
1544    use pretty_assertions::assert_eq;
1545    use serde_json::json;
1546
1547    use super::*;
1548    use crate::{
1549        ChoiceDelta, FunctionChunk, ResponseMessageDelta, ResponseStreamEvent, ToolCallChunk,
1550    };
1551
1552    fn map_response_events(events: Vec<ResponsesStreamEvent>) -> Vec<LanguageModelCompletionEvent> {
1553        block_on(async {
1554            OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID)
1555                .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
1556                .collect::<Vec<_>>()
1557                .await
1558                .into_iter()
1559                .map(Result::unwrap)
1560                .collect()
1561        })
1562    }
1563
1564    fn map_completion_events(
1565        events: Vec<ResponseStreamEvent>,
1566    ) -> Vec<LanguageModelCompletionEvent> {
1567        let mut mapper = OpenAiEventMapper::new();
1568        let mut all_events = Vec::new();
1569        for event in events {
1570            all_events.extend(mapper.map_event(event));
1571        }
1572        all_events.into_iter().filter_map(|e| e.ok()).collect()
1573    }
1574
1575    fn response_item_message(id: &str) -> ResponseOutputItem {
1576        ResponseOutputItem::Message(ResponseOutputMessage {
1577            id: Some(id.to_string()),
1578            role: Some("assistant".to_string()),
1579            status: Some("in_progress".to_string()),
1580            content: vec![],
1581            phase: None,
1582        })
1583    }
1584
1585    fn response_item_function_call(id: &str, args: Option<&str>) -> ResponseOutputItem {
1586        ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
1587            id: Some(id.to_string()),
1588            status: Some("in_progress".to_string()),
1589            name: Some("get_weather".to_string()),
1590            call_id: Some("call_123".to_string()),
1591            arguments: args.map(|s| s.to_string()).unwrap_or_default(),
1592        })
1593    }
1594
1595    fn response_item_custom_tool_call(id: &str, input: &str) -> ResponseOutputItem {
1596        ResponseOutputItem::CustomToolCall(ResponseCustomToolCall {
1597            id: Some(id.to_string()),
1598            status: Some("in_progress".to_string()),
1599            name: Some("apply_patch".to_string()),
1600            call_id: Some("call_abc".to_string()),
1601            input: input.to_string(),
1602        })
1603    }
1604
1605    fn response_reasoning_item(
1606        id: &str,
1607        summary: Vec<ReasoningSummaryPart>,
1608        encrypted_content: Option<&str>,
1609        status: Option<String>,
1610    ) -> ResponseReasoningItem {
1611        ResponseReasoningItem {
1612            id: Some(id.to_string()),
1613            summary,
1614            content: Vec::new(),
1615            encrypted_content: encrypted_content.map(str::to_string),
1616            status,
1617        }
1618    }
1619
1620    #[test]
1621    fn responses_stream_maps_text_and_usage() {
1622        let events = vec![
1623            ResponsesStreamEvent::OutputItemAdded {
1624                output_index: 0,
1625                sequence_number: None,
1626                item: response_item_message("msg_123"),
1627            },
1628            ResponsesStreamEvent::OutputTextDelta {
1629                item_id: "msg_123".into(),
1630                output_index: 0,
1631                content_index: Some(0),
1632                delta: "Hello".into(),
1633            },
1634            ResponsesStreamEvent::Completed {
1635                response: ResponseSummary {
1636                    usage: Some(ResponseUsage {
1637                        input_tokens: Some(5),
1638                        input_tokens_details: ResponseInputTokensDetails { cached_tokens: 2 },
1639                        output_tokens: Some(3),
1640                        total_tokens: Some(8),
1641                        ..Default::default()
1642                    }),
1643                    ..Default::default()
1644                },
1645            },
1646        ];
1647
1648        let mapped = map_response_events(events);
1649        assert!(matches!(
1650            mapped[0],
1651            LanguageModelCompletionEvent::StartMessage { ref message_id } if message_id == "msg_123"
1652        ));
1653        assert!(matches!(
1654            mapped[1],
1655            LanguageModelCompletionEvent::Text(ref text) if text == "Hello"
1656        ));
1657        assert!(matches!(
1658            mapped[2],
1659            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1660                input_tokens: 3,
1661                output_tokens: 3,
1662                cache_read_input_tokens: 2,
1663                ..
1664            })
1665        ));
1666        assert!(matches!(
1667            mapped[3],
1668            LanguageModelCompletionEvent::Stop(StopReason::EndTurn)
1669        ));
1670    }
1671
1672    #[test]
1673    fn responses_stream_maps_mantle_reasoning_delta() {
1674        let event = serde_json::from_value::<ResponsesStreamEvent>(json!({
1675            "type": "response.reasoning.delta",
1676            "delta": "checking the contract terms"
1677        }))
1678        .unwrap();
1679
1680        let mapped = map_response_events(vec![event]);
1681        assert!(matches!(
1682            &mapped[0],
1683            LanguageModelCompletionEvent::Thinking { text, signature: None }
1684                if text == "checking the contract terms"
1685        ));
1686    }
1687
1688    #[test]
1689    fn response_usage_deserializes_cached_tokens() -> Result<()> {
1690        let usage: ResponseUsage = serde_json::from_value(json!({
1691            "input_tokens": 5,
1692            "input_tokens_details": {
1693                "cached_tokens": 2,
1694            },
1695            "output_tokens": 3,
1696            "output_tokens_details": {
1697                "reasoning_tokens": 1,
1698            },
1699            "total_tokens": 8,
1700        }))?;
1701
1702        assert_eq!(usage.output_tokens_details.reasoning_tokens, 1);
1703        assert_eq!(
1704            token_usage_from_response_usage(&usage),
1705            TokenUsage {
1706                input_tokens: 3,
1707                output_tokens: 3,
1708                cache_creation_input_tokens: 0,
1709                cache_read_input_tokens: 2,
1710            }
1711        );
1712
1713        Ok(())
1714    }
1715
1716    #[test]
1717    fn responses_custom_tool_wire_types_round_trip() -> Result<()> {
1718        let tool_json = json!({
1719            "type": "custom",
1720            "name": "apply_patch",
1721            "description": "Apply a patch",
1722            "format": {
1723                "type": "grammar",
1724                "syntax": "lark",
1725                "definition": "start: /.+/"
1726            }
1727        });
1728        let tool: ToolDefinition = serde_json::from_value(tool_json.clone())?;
1729        assert_eq!(serde_json::to_value(tool)?, tool_json);
1730
1731        let text_tool_json = json!({
1732            "type": "custom",
1733            "name": "write_text",
1734            "format": { "type": "text" }
1735        });
1736        let text_tool: ToolDefinition = serde_json::from_value(text_tool_json.clone())?;
1737        assert_eq!(serde_json::to_value(text_tool)?, text_tool_json);
1738
1739        let input_json = json!({
1740            "type": "custom_tool_call",
1741            "id": "ctc_1",
1742            "call_id": "call_abc",
1743            "name": "apply_patch",
1744            "input": "*** Begin Patch\n*** End Patch"
1745        });
1746        let input: ResponseInputItem = serde_json::from_value(input_json.clone())?;
1747        assert_eq!(serde_json::to_value(input)?, input_json);
1748
1749        let output_json = json!({
1750            "type": "custom_tool_call_output",
1751            "call_id": "call_abc",
1752            "output": "ok"
1753        });
1754        let output: ResponseInputItem = serde_json::from_value(output_json.clone())?;
1755        assert_eq!(serde_json::to_value(output)?, output_json);
1756
1757        let output_item_json = json!({
1758            "id": "ctc_1",
1759            "type": "custom_tool_call",
1760            "status": "completed",
1761            "call_id": "call_abc",
1762            "name": "apply_patch",
1763            "input": "*** Begin Patch\n*** End Patch"
1764        });
1765        let output_item: ResponseOutputItem = serde_json::from_value(output_item_json.clone())?;
1766        assert_eq!(serde_json::to_value(output_item)?, output_item_json);
1767
1768        let delta_json = json!({
1769            "type": "response.custom_tool_call_input.delta",
1770            "output_index": 0,
1771            "item_id": "ctc_1",
1772            "sequence_number": 5,
1773            "delta": "chunk"
1774        });
1775        let delta: ResponsesStreamEvent = serde_json::from_value(delta_json)?;
1776        assert!(matches!(
1777            delta,
1778            ResponsesStreamEvent::CustomToolCallInputDelta {
1779                output_index: 0,
1780                sequence_number: Some(5),
1781                ref item_id,
1782                ref delta,
1783            } if item_id == "ctc_1" && delta == "chunk"
1784        ));
1785
1786        let done_json = json!({
1787            "type": "response.custom_tool_call_input.done",
1788            "output_index": 0,
1789            "item_id": "ctc_1",
1790            "sequence_number": 6,
1791            "input": "full text"
1792        });
1793        let done: ResponsesStreamEvent = serde_json::from_value(done_json)?;
1794        assert!(matches!(
1795            done,
1796            ResponsesStreamEvent::CustomToolCallInputDone {
1797                output_index: 0,
1798                sequence_number: Some(6),
1799                ref item_id,
1800                ref input,
1801            } if item_id == "ctc_1" && input == "full text"
1802        ));
1803
1804        Ok(())
1805    }
1806
1807    #[test]
1808    fn into_open_ai_response_builds_complete_payload() {
1809        let tool_call_id = LanguageModelToolUseId::from("call-42");
1810        let tool_input = json!({ "city": "Boston" });
1811        let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1812        let tool_use = LanguageModelToolUse {
1813            id: tool_call_id.clone(),
1814            name: Arc::from("get_weather"),
1815            raw_input: tool_arguments.clone(),
1816            input: LanguageModelToolUseInput::Json(tool_input),
1817            is_input_complete: true,
1818            thought_signature: None,
1819        };
1820        let tool_result = LanguageModelToolResult {
1821            tool_use_id: tool_call_id,
1822            tool_name: Arc::from("get_weather"),
1823            is_error: false,
1824            content: vec![LanguageModelToolResultContent::Text(Arc::from("Sunny"))],
1825            output: Some(json!({ "forecast": "Sunny" })),
1826        };
1827        let user_image = LanguageModelImage {
1828            source: SharedString::from("aGVsbG8="),
1829        };
1830        let expected_image_url = user_image.to_base64_url();
1831
1832        let request = LanguageModelRequest {
1833            thread_id: Some("thread-123".into()),
1834            prompt_id: None,
1835            intent: None,
1836            messages: vec![
1837                LanguageModelRequestMessage {
1838                    role: Role::System,
1839                    content: vec![MessageContent::Text("System context".into())],
1840                    cache: false,
1841                    reasoning_details: None,
1842                },
1843                LanguageModelRequestMessage {
1844                    role: Role::User,
1845                    content: vec![
1846                        MessageContent::Text("Please check the weather.".into()),
1847                        MessageContent::Image(user_image),
1848                    ],
1849                    cache: false,
1850                    reasoning_details: None,
1851                },
1852                LanguageModelRequestMessage {
1853                    role: Role::Assistant,
1854                    content: vec![
1855                        MessageContent::Text("Looking that up.".into()),
1856                        MessageContent::ToolUse(tool_use),
1857                    ],
1858                    cache: false,
1859                    reasoning_details: None,
1860                },
1861                LanguageModelRequestMessage {
1862                    role: Role::Assistant,
1863                    content: vec![MessageContent::ToolResult(tool_result)],
1864                    cache: false,
1865                    reasoning_details: None,
1866                },
1867            ],
1868            tools: vec![LanguageModelRequestTool::function(
1869                "get_weather".into(),
1870                "Fetches the weather".into(),
1871                json!({ "type": "object" }),
1872                false,
1873            )],
1874            tool_choice: Some(LanguageModelToolChoice::Any),
1875            stop: vec!["<STOP>".into()],
1876            temperature: None,
1877            thinking_allowed: true,
1878            thinking_effort: Some("high".into()),
1879            speed: None,
1880            compact_at_tokens: None,
1881        };
1882
1883        let response = into_open_ai_response(
1884            request,
1885            "custom-model",
1886            true,
1887            true,
1888            Some(2048),
1889            Some(ReasoningEffort::Low),
1890            false,
1891            &OPEN_AI_PROVIDER_ID,
1892        )
1893        .unwrap();
1894
1895        let serialized = serde_json::to_value(&response).unwrap();
1896        let expected = json!({
1897            "model": "custom-model",
1898            "instructions": "System context",
1899            "input": [
1900                {
1901                    "type": "message",
1902                    "role": "user",
1903                    "content": [
1904                        { "type": "input_text", "text": "Please check the weather." },
1905                        { "type": "input_image", "image_url": expected_image_url }
1906                    ]
1907                },
1908                {
1909                    "type": "message",
1910                    "role": "assistant",
1911                    "content": [
1912                        { "type": "output_text", "text": "Looking that up.", "annotations": [] }
1913                    ]
1914                },
1915                {
1916                    "type": "function_call",
1917                    "call_id": "call-42",
1918                    "name": "get_weather",
1919                    "arguments": tool_arguments
1920                },
1921                {
1922                    "type": "function_call_output",
1923                    "call_id": "call-42",
1924                    "output": "Sunny"
1925                }
1926            ],
1927            "store": false,
1928            "include": ["reasoning.encrypted_content"],
1929            "stream": true,
1930            "max_output_tokens": 2048,
1931            "parallel_tool_calls": true,
1932            "tool_choice": "required",
1933            "tools": [
1934                {
1935                    "type": "function",
1936                    "name": "get_weather",
1937                    "description": "Fetches the weather",
1938                    "parameters": { "type": "object" }
1939                }
1940            ],
1941            "prompt_cache_key": "thread-123",
1942            "reasoning": { "effort": "high", "summary": "auto" }
1943        });
1944
1945        assert_eq!(serialized, expected);
1946    }
1947
1948    #[test]
1949    fn responses_stream_maps_custom_tool_input() {
1950        let events = vec![
1951            ResponsesStreamEvent::OutputItemAdded {
1952                output_index: 0,
1953                sequence_number: None,
1954                item: response_item_custom_tool_call("ctc_1", ""),
1955            },
1956            ResponsesStreamEvent::CustomToolCallInputDelta {
1957                item_id: "ctc_1".into(),
1958                output_index: 0,
1959                delta: "*** Begin".into(),
1960                sequence_number: Some(1),
1961            },
1962            ResponsesStreamEvent::CustomToolCallInputDelta {
1963                item_id: "ctc_1".into(),
1964                output_index: 0,
1965                delta: " Patch".into(),
1966                sequence_number: Some(2),
1967            },
1968            ResponsesStreamEvent::CustomToolCallInputDone {
1969                item_id: "ctc_1".into(),
1970                output_index: 0,
1971                input: "*** Begin Patch".into(),
1972                sequence_number: Some(3),
1973            },
1974            ResponsesStreamEvent::OutputItemDone {
1975                output_index: 0,
1976                sequence_number: None,
1977                item: response_item_custom_tool_call("ctc_1", "*** Begin Patch"),
1978            },
1979            ResponsesStreamEvent::Completed {
1980                response: ResponseSummary::default(),
1981            },
1982        ];
1983
1984        let mapped = map_response_events(events);
1985        assert_eq!(
1986            mapped,
1987            vec![
1988                LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1989                    id: LanguageModelToolUseId::from("call_abc"),
1990                    name: Arc::from("apply_patch"),
1991                    raw_input: "*** Begin".into(),
1992                    input: LanguageModelToolUseInput::Text("*** Begin".into()),
1993                    is_input_complete: false,
1994                    thought_signature: None,
1995                }),
1996                LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1997                    id: LanguageModelToolUseId::from("call_abc"),
1998                    name: Arc::from("apply_patch"),
1999                    raw_input: "*** Begin Patch".into(),
2000                    input: LanguageModelToolUseInput::Text("*** Begin Patch".into()),
2001                    is_input_complete: false,
2002                    thought_signature: None,
2003                }),
2004                LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
2005                    id: LanguageModelToolUseId::from("call_abc"),
2006                    name: Arc::from("apply_patch"),
2007                    raw_input: "*** Begin Patch".into(),
2008                    input: LanguageModelToolUseInput::Text("*** Begin Patch".into()),
2009                    is_input_complete: true,
2010                    thought_signature: None,
2011                }),
2012                LanguageModelCompletionEvent::Stop(StopReason::ToolUse),
2013            ]
2014        );
2015    }
2016
2017    #[test]
2018    fn into_open_ai_response_replays_custom_tool_calls() {
2019        let tool_call_id = LanguageModelToolUseId::from("call_abc");
2020        let raw_input = "*** Begin Patch\n*** End Patch".to_string();
2021        let tool_use = LanguageModelToolUse {
2022            id: tool_call_id.clone(),
2023            name: Arc::from("apply_patch"),
2024            raw_input: raw_input.clone(),
2025            input: LanguageModelToolUseInput::Text(raw_input.clone()),
2026            is_input_complete: true,
2027            thought_signature: None,
2028        };
2029        let tool_result = LanguageModelToolResult {
2030            tool_use_id: tool_call_id,
2031            tool_name: Arc::from("apply_patch"),
2032            is_error: false,
2033            content: vec![LanguageModelToolResultContent::Text(Arc::from("ok"))],
2034            output: None,
2035        };
2036
2037        let request = LanguageModelRequest {
2038            thread_id: None,
2039            prompt_id: None,
2040            intent: None,
2041            messages: vec![LanguageModelRequestMessage {
2042                role: Role::Assistant,
2043                content: vec![
2044                    MessageContent::ToolUse(tool_use),
2045                    MessageContent::ToolResult(tool_result),
2046                ],
2047                cache: false,
2048                reasoning_details: None,
2049            }],
2050            tools: vec![LanguageModelRequestTool {
2051                name: "apply_patch".into(),
2052                description: "Apply a patch".into(),
2053                input: LanguageModelRequestToolInput::Custom {
2054                    format: Some(LanguageModelCustomToolFormat::Grammar {
2055                        syntax: LanguageModelCustomToolGrammarSyntax::Lark,
2056                        definition: "start: /.+/".into(),
2057                    }),
2058                },
2059            }],
2060            tool_choice: None,
2061            stop: Vec::new(),
2062            temperature: None,
2063            thinking_allowed: false,
2064            thinking_effort: None,
2065            speed: None,
2066            compact_at_tokens: None,
2067        };
2068
2069        let response = into_open_ai_response(
2070            request,
2071            "custom-model",
2072            false,
2073            false,
2074            None,
2075            None,
2076            false,
2077            &OPEN_AI_PROVIDER_ID,
2078        )
2079        .unwrap();
2080        let serialized = serde_json::to_value(response).unwrap();
2081        assert_eq!(
2082            serialized,
2083            json!({
2084                "model": "custom-model",
2085                "input": [
2086                    {
2087                        "type": "custom_tool_call",
2088                        "call_id": "call_abc",
2089                        "name": "apply_patch",
2090                        "input": raw_input
2091                    },
2092                    {
2093                        "type": "custom_tool_call_output",
2094                        "call_id": "call_abc",
2095                        "output": "ok"
2096                    }
2097                ],
2098                "store": false,
2099                "stream": true,
2100                "parallel_tool_calls": false,
2101                "tools": [
2102                    {
2103                        "type": "custom",
2104                        "name": "apply_patch",
2105                        "description": "Apply a patch",
2106                        "format": {
2107                            "type": "grammar",
2108                            "syntax": "lark",
2109                            "definition": "start: /.+/"
2110                        }
2111                    }
2112                ]
2113            })
2114        );
2115    }
2116
2117    #[test]
2118    fn into_open_ai_response_replays_encrypted_reasoning_details() {
2119        let tool_call_id = LanguageModelToolUseId::from("call-42");
2120        let tool_arguments = "{\"city\":\"Boston\"}".to_string();
2121        let tool_use = LanguageModelToolUse {
2122            id: tool_call_id,
2123            name: Arc::from("get_weather"),
2124            raw_input: tool_arguments.clone(),
2125            input: LanguageModelToolUseInput::Json(json!({ "city": "Boston" })),
2126            is_input_complete: true,
2127            thought_signature: None,
2128        };
2129
2130        let request = LanguageModelRequest {
2131            thread_id: None,
2132            prompt_id: None,
2133            intent: None,
2134            messages: vec![LanguageModelRequestMessage {
2135                role: Role::Assistant,
2136                content: vec![MessageContent::ToolUse(tool_use)],
2137                cache: false,
2138                reasoning_details: Some(Arc::new(json!({
2139                    "reasoning_items": [
2140                        {
2141                            "id": "rs_123",
2142                            "summary": [
2143                                {
2144                                    "type": "summary_text",
2145                                    "text": "Checked what information is needed."
2146                                }
2147                            ],
2148                            "content": [
2149                                {
2150                                    "type": "reasoning_text",
2151                                    "text": "Internal reasoning text."
2152                                }
2153                            ],
2154                            "encrypted_content": "ENC",
2155                            "status": "completed",
2156                        }
2157                    ]
2158                }))),
2159            }],
2160            tools: Vec::new(),
2161            tool_choice: None,
2162            stop: Vec::new(),
2163            temperature: None,
2164            thinking_allowed: false,
2165            thinking_effort: None,
2166            speed: None,
2167            compact_at_tokens: None,
2168        };
2169
2170        let response = into_open_ai_response(
2171            request,
2172            "gpt-5",
2173            true,
2174            true,
2175            None,
2176            Some(ReasoningEffort::Low),
2177            false,
2178            &OPEN_AI_PROVIDER_ID,
2179        )
2180        .unwrap();
2181
2182        let serialized = serde_json::to_value(&response).unwrap();
2183        assert_eq!(
2184            serialized["input"],
2185            json!([
2186                {
2187                    "type": "reasoning",
2188                    "id": "rs_123",
2189                    "summary": [
2190                        {
2191                            "type": "summary_text",
2192                            "text": "Checked what information is needed."
2193                        }
2194                    ],
2195                    "content": [
2196                        {
2197                            "type": "reasoning_text",
2198                            "text": "Internal reasoning text."
2199                        }
2200                    ],
2201                    "encrypted_content": "ENC",
2202                    "status": "completed"
2203                },
2204                {
2205                    "type": "function_call",
2206                    "call_id": "call-42",
2207                    "name": "get_weather",
2208                    "arguments": tool_arguments
2209                }
2210            ])
2211        );
2212        assert_eq!(
2213            serialized["include"],
2214            json!(["reasoning.encrypted_content"])
2215        );
2216        assert_eq!(serialized.get("reasoning"), None);
2217    }
2218
2219    #[test]
2220    fn into_open_ai_response_replays_reasoning_without_encrypted_content() {
2221        let request = LanguageModelRequest {
2222            thread_id: None,
2223            prompt_id: None,
2224            intent: None,
2225            messages: vec![LanguageModelRequestMessage {
2226                role: Role::Assistant,
2227                content: vec![MessageContent::Text("Done.".into())],
2228                cache: false,
2229                reasoning_details: Some(Arc::new(json!({
2230                    "reasoning_items": [
2231                        {
2232                            "id": "rs_123",
2233                            "summary": [],
2234                            "status": "completed"
2235                        },
2236                        {
2237                            "id": "rs_456",
2238                            "summary": [],
2239                            "encrypted_content": "",
2240                            "status": "completed"
2241                        }
2242                    ]
2243                }))),
2244            }],
2245            tools: Vec::new(),
2246            tool_choice: None,
2247            stop: Vec::new(),
2248            temperature: None,
2249            thinking_allowed: false,
2250            thinking_effort: None,
2251            speed: None,
2252            compact_at_tokens: None,
2253        };
2254
2255        let response = into_open_ai_response(
2256            request,
2257            "custom-model",
2258            false,
2259            false,
2260            None,
2261            None,
2262            false,
2263            &OPEN_AI_PROVIDER_ID,
2264        )
2265        .unwrap();
2266        let serialized = serde_json::to_value(&response).unwrap();
2267
2268        assert_eq!(
2269            serialized["input"],
2270            json!([
2271                {
2272                    "type": "reasoning",
2273                    "id": "rs_123",
2274                    "summary": [],
2275                    "status": "completed"
2276                },
2277                {
2278                    "type": "reasoning",
2279                    "id": "rs_456",
2280                    "summary": [],
2281                    "encrypted_content": "",
2282                    "status": "completed"
2283                },
2284                {
2285                    "type": "message",
2286                    "role": "assistant",
2287                    "content": [
2288                        {
2289                            "type": "output_text",
2290                            "text": "Done.",
2291                            "annotations": []
2292                        }
2293                    ]
2294                }
2295            ])
2296        );
2297    }
2298
2299    #[test]
2300    fn into_open_ai_response_omits_reasoning_when_thinking_is_disabled_and_none_is_unsupported() {
2301        let request = LanguageModelRequest {
2302            thread_id: None,
2303            prompt_id: None,
2304            intent: None,
2305            messages: vec![LanguageModelRequestMessage {
2306                role: Role::User,
2307                content: vec![MessageContent::Text("Hello".into())],
2308                cache: false,
2309                reasoning_details: None,
2310            }],
2311            tools: Vec::new(),
2312            tool_choice: None,
2313            stop: Vec::new(),
2314            temperature: None,
2315            thinking_allowed: false,
2316            thinking_effort: Some("high".into()),
2317            speed: None,
2318            compact_at_tokens: None,
2319        };
2320
2321        let response = into_open_ai_response(
2322            request,
2323            "gpt-5",
2324            true,
2325            true,
2326            None,
2327            Some(ReasoningEffort::Medium),
2328            false,
2329            &OPEN_AI_PROVIDER_ID,
2330        )
2331        .unwrap();
2332
2333        let serialized = serde_json::to_value(&response).unwrap();
2334        assert_eq!(serialized.get("reasoning"), None);
2335    }
2336
2337    /// `Speed::Fast` should translate to `service_tier: "priority"` on the
2338    /// outgoing Responses request, while `Standard` / `None` should leave the
2339    /// field unset so the project's default tier wins.
2340    #[test]
2341    fn into_open_ai_response_sets_service_tier_for_fast_speed() -> Result<()> {
2342        for (speed, expected) in [
2343            (None, None),
2344            (Some(Speed::Standard), None),
2345            (Some(Speed::Fast), Some("priority")),
2346        ] {
2347            let request = LanguageModelRequest {
2348                thread_id: None,
2349                prompt_id: None,
2350                intent: None,
2351                messages: vec![LanguageModelRequestMessage {
2352                    role: Role::User,
2353                    content: vec![MessageContent::Text("Hello".into())],
2354                    cache: false,
2355                    reasoning_details: None,
2356                }],
2357                tools: Vec::new(),
2358                tool_choice: None,
2359                stop: Vec::new(),
2360                temperature: None,
2361                thinking_allowed: false,
2362                thinking_effort: None,
2363                speed,
2364                compact_at_tokens: None,
2365            };
2366
2367            let response = into_open_ai_response(
2368                request,
2369                "gpt-5.4",
2370                true,
2371                true,
2372                None,
2373                None,
2374                true,
2375                &OPEN_AI_PROVIDER_ID,
2376            )
2377            .unwrap();
2378
2379            let serialized = serde_json::to_value(&response)?;
2380            assert_eq!(
2381                serialized
2382                    .get("service_tier")
2383                    .and_then(|value| value.as_str()),
2384                expected,
2385                "speed = {speed:?} should produce service_tier = {expected:?}",
2386            );
2387        }
2388        Ok(())
2389    }
2390
2391    /// Same as above but for the Chat Completions code path.
2392    #[test]
2393    fn into_open_ai_sets_service_tier_for_fast_speed() -> Result<()> {
2394        for (speed, expected) in [
2395            (None, None),
2396            (Some(Speed::Standard), None),
2397            (Some(Speed::Fast), Some("priority")),
2398        ] {
2399            let request = LanguageModelRequest {
2400                thread_id: None,
2401                prompt_id: None,
2402                intent: None,
2403                messages: vec![LanguageModelRequestMessage {
2404                    role: Role::User,
2405                    content: vec![MessageContent::Text("Hello".into())],
2406                    cache: false,
2407                    reasoning_details: None,
2408                }],
2409                tools: Vec::new(),
2410                tool_choice: None,
2411                stop: Vec::new(),
2412                temperature: None,
2413                thinking_allowed: false,
2414                thinking_effort: None,
2415                speed,
2416                compact_at_tokens: None,
2417            };
2418
2419            let chat = into_open_ai(
2420                request,
2421                "gpt-5.4",
2422                true,
2423                true,
2424                None,
2425                ChatCompletionMaxTokensParameter::MaxCompletionTokens,
2426                None,
2427                false,
2428            )?;
2429
2430            let serialized = serde_json::to_value(&chat)?;
2431            assert_eq!(
2432                serialized
2433                    .get("service_tier")
2434                    .and_then(|value| value.as_str()),
2435                expected,
2436                "speed = {speed:?} should produce service_tier = {expected:?}",
2437            );
2438        }
2439        Ok(())
2440    }
2441
2442    #[test]
2443    fn into_open_ai_can_send_max_tokens_parameter() -> Result<()> {
2444        let request = LanguageModelRequest {
2445            thread_id: None,
2446            prompt_id: None,
2447            intent: None,
2448            messages: vec![LanguageModelRequestMessage {
2449                role: Role::User,
2450                content: vec![MessageContent::Text("Hello".into())],
2451                cache: false,
2452                reasoning_details: None,
2453            }],
2454            tools: Vec::new(),
2455            tool_choice: None,
2456            stop: Vec::new(),
2457            temperature: None,
2458            thinking_allowed: false,
2459            thinking_effort: None,
2460            speed: None,
2461            compact_at_tokens: None,
2462        };
2463
2464        let chat = into_open_ai(
2465            request,
2466            "compatible-model",
2467            false,
2468            false,
2469            Some(4096),
2470            ChatCompletionMaxTokensParameter::MaxTokens,
2471            None,
2472            false,
2473        )?;
2474
2475        let serialized = serde_json::to_value(&chat)?;
2476        assert_eq!(serialized.get("max_completion_tokens"), None);
2477        assert_eq!(serialized["max_tokens"], json!(4096));
2478        Ok(())
2479    }
2480
2481    #[test]
2482    fn into_open_ai_response_sends_none_reasoning_when_thinking_is_disabled() -> Result<()> {
2483        let request = LanguageModelRequest {
2484            thread_id: None,
2485            prompt_id: None,
2486            intent: None,
2487            messages: vec![LanguageModelRequestMessage {
2488                role: Role::User,
2489                content: vec![MessageContent::Text("Hello".into())],
2490                cache: false,
2491                reasoning_details: None,
2492            }],
2493            tools: Vec::new(),
2494            tool_choice: None,
2495            stop: Vec::new(),
2496            temperature: None,
2497            thinking_allowed: false,
2498            thinking_effort: Some("high".into()),
2499            speed: None,
2500            compact_at_tokens: None,
2501        };
2502
2503        let response = into_open_ai_response(
2504            request,
2505            "gpt-5.1",
2506            true,
2507            true,
2508            None,
2509            Some(ReasoningEffort::Medium),
2510            true,
2511            &OPEN_AI_PROVIDER_ID,
2512        )
2513        .unwrap();
2514
2515        let serialized = serde_json::to_value(&response)?;
2516        assert_eq!(serialized["reasoning"], json!({ "effort": "none" }));
2517        assert_eq!(serialized.get("include"), None);
2518
2519        Ok(())
2520    }
2521
2522    #[test]
2523    fn into_open_ai_response_uses_default_effort_when_selected_effort_is_none() -> Result<()> {
2524        let request = LanguageModelRequest {
2525            thread_id: None,
2526            prompt_id: None,
2527            intent: None,
2528            messages: vec![LanguageModelRequestMessage {
2529                role: Role::User,
2530                content: vec![MessageContent::Text("Hello".into())],
2531                cache: false,
2532                reasoning_details: None,
2533            }],
2534            tools: Vec::new(),
2535            tool_choice: None,
2536            stop: Vec::new(),
2537            temperature: None,
2538            thinking_allowed: true,
2539            thinking_effort: Some("none".into()),
2540            speed: None,
2541            compact_at_tokens: None,
2542        };
2543
2544        let response = into_open_ai_response(
2545            request,
2546            "gpt-5.1",
2547            true,
2548            true,
2549            None,
2550            Some(ReasoningEffort::Medium),
2551            true,
2552            &OPEN_AI_PROVIDER_ID,
2553        )
2554        .unwrap();
2555
2556        let serialized = serde_json::to_value(&response)?;
2557        assert_eq!(
2558            serialized["reasoning"],
2559            json!({ "effort": "medium", "summary": "auto" })
2560        );
2561
2562        Ok(())
2563    }
2564
2565    #[test]
2566    fn into_open_ai_response_replays_assistant_phase() {
2567        let request = LanguageModelRequest {
2568            thread_id: None,
2569            prompt_id: None,
2570            intent: None,
2571            messages: vec![LanguageModelRequestMessage {
2572                role: Role::Assistant,
2573                content: vec![MessageContent::Text("Done.".into())],
2574                cache: false,
2575                reasoning_details: Some(Arc::new(json!({
2576                    "phase": "final_answer",
2577                    "reasoning_items": [
2578                        {
2579                            "id": "rs_123",
2580                            "summary": [],
2581                            "encrypted_content": "ENC",
2582                            "status": "completed"
2583                        }
2584                    ]
2585                }))),
2586            }],
2587            tools: Vec::new(),
2588            tool_choice: None,
2589            stop: Vec::new(),
2590            temperature: None,
2591            thinking_allowed: true,
2592            thinking_effort: None,
2593            speed: None,
2594            compact_at_tokens: None,
2595        };
2596
2597        let response = into_open_ai_response(
2598            request,
2599            "gpt-5.3-codex",
2600            true,
2601            true,
2602            None,
2603            Some(ReasoningEffort::Medium),
2604            false,
2605            &OPEN_AI_PROVIDER_ID,
2606        )
2607        .unwrap();
2608
2609        let serialized = serde_json::to_value(&response).unwrap();
2610        assert_eq!(
2611            serialized["input"],
2612            json!([
2613                {
2614                    "type": "reasoning",
2615                    "id": "rs_123",
2616                    "summary": [],
2617                    "encrypted_content": "ENC",
2618                    "status": "completed"
2619                },
2620                {
2621                    "type": "message",
2622                    "role": "assistant",
2623                    "content": [
2624                        { "type": "output_text", "text": "Done.", "annotations": [] }
2625                    ],
2626                    "phase": "final_answer"
2627                }
2628            ])
2629        );
2630    }
2631
2632    #[test]
2633    fn into_open_ai_response_deduplicates_replayed_reasoning_items() {
2634        let first_reasoning_details = json!({
2635            "phase": "final_answer",
2636            "reasoning_items": [
2637                {
2638                    "id": "rs_123",
2639                    "summary": [],
2640                    "encrypted_content": "ENC_OLD",
2641                    "status": "in_progress"
2642                }
2643            ]
2644        });
2645        let second_reasoning_details = json!({
2646            "phase": "final_answer",
2647            "reasoning_items": [
2648                {
2649                    "id": "rs_123",
2650                    "summary": [
2651                        {
2652                            "type": "summary_text",
2653                            "text": "Later metadata has the complete summary."
2654                        }
2655                    ],
2656                    "encrypted_content": "ENC_NEW",
2657                    "status": "completed"
2658                }
2659            ]
2660        });
2661        let request = LanguageModelRequest {
2662            thread_id: None,
2663            prompt_id: None,
2664            intent: None,
2665            messages: vec![
2666                LanguageModelRequestMessage {
2667                    role: Role::Assistant,
2668                    content: vec![MessageContent::Text("First.".into())],
2669                    cache: false,
2670                    reasoning_details: Some(Arc::new(first_reasoning_details)),
2671                },
2672                LanguageModelRequestMessage {
2673                    role: Role::Assistant,
2674                    content: vec![MessageContent::Text("Second.".into())],
2675                    cache: false,
2676                    reasoning_details: Some(Arc::new(second_reasoning_details)),
2677                },
2678            ],
2679            tools: Vec::new(),
2680            tool_choice: None,
2681            stop: Vec::new(),
2682            temperature: None,
2683            thinking_allowed: true,
2684            thinking_effort: None,
2685            speed: None,
2686            compact_at_tokens: None,
2687        };
2688
2689        let response = into_open_ai_response(
2690            request,
2691            "gpt-5.3-codex",
2692            true,
2693            true,
2694            None,
2695            Some(ReasoningEffort::Medium),
2696            false,
2697            &OPEN_AI_PROVIDER_ID,
2698        )
2699        .unwrap();
2700
2701        let serialized = serde_json::to_value(&response).unwrap();
2702        assert_eq!(
2703            serialized["input"],
2704            json!([
2705                {
2706                    "type": "reasoning",
2707                    "id": "rs_123",
2708                    "summary": [
2709                        {
2710                            "type": "summary_text",
2711                            "text": "Later metadata has the complete summary."
2712                        }
2713                    ],
2714                    "encrypted_content": "ENC_NEW",
2715                    "status": "completed"
2716                },
2717                {
2718                    "type": "message",
2719                    "role": "assistant",
2720                    "content": [
2721                        { "type": "output_text", "text": "First.", "annotations": [] }
2722                    ],
2723                    "phase": "final_answer"
2724                },
2725                {
2726                    "type": "message",
2727                    "role": "assistant",
2728                    "content": [
2729                        { "type": "output_text", "text": "Second.", "annotations": [] }
2730                    ],
2731                    "phase": "final_answer"
2732                }
2733            ])
2734        );
2735    }
2736
2737    #[test]
2738    fn into_open_ai_response_replays_reasoning_details_but_not_thinking_text() {
2739        let request = LanguageModelRequest {
2740            thread_id: None,
2741            prompt_id: None,
2742            intent: None,
2743            messages: vec![LanguageModelRequestMessage {
2744                role: Role::Assistant,
2745                content: vec![
2746                    MessageContent::Thinking {
2747                        text: "This is a reasoning summary, not assistant output.".into(),
2748                        signature: None,
2749                    },
2750                    MessageContent::Text("This is visible assistant output.".into()),
2751                ],
2752                cache: false,
2753                reasoning_details: Some(Arc::new(json!({
2754                    "reasoning_items": [
2755                        {
2756                            "id": "rs_123",
2757                            "summary": [
2758                                {
2759                                    "type": "summary_text",
2760                                    "text": "This is the reasoning summary to preserve."
2761                                }
2762                            ],
2763                            "encrypted_content": "ENC",
2764                            "status": "completed"
2765                        }
2766                    ]
2767                }))),
2768            }],
2769            tools: Vec::new(),
2770            tool_choice: None,
2771            stop: Vec::new(),
2772            temperature: None,
2773            thinking_allowed: false,
2774            thinking_effort: None,
2775            speed: None,
2776            compact_at_tokens: None,
2777        };
2778
2779        let response = into_open_ai_response(
2780            request,
2781            "custom-model",
2782            false,
2783            false,
2784            None,
2785            None,
2786            false,
2787            &OPEN_AI_PROVIDER_ID,
2788        )
2789        .unwrap();
2790        let serialized = serde_json::to_value(&response).unwrap();
2791
2792        assert_eq!(
2793            serialized["input"],
2794            json!([
2795                {
2796                    "type": "reasoning",
2797                    "id": "rs_123",
2798                    "summary": [
2799                        {
2800                            "type": "summary_text",
2801                            "text": "This is the reasoning summary to preserve."
2802                        }
2803                    ],
2804                    "encrypted_content": "ENC",
2805                    "status": "completed"
2806                },
2807                {
2808                    "type": "message",
2809                    "role": "assistant",
2810                    "content": [
2811                        {
2812                            "type": "output_text",
2813                            "text": "This is visible assistant output.",
2814                            "annotations": []
2815                        }
2816                    ]
2817                }
2818            ])
2819        );
2820        assert_eq!(
2821            serialized["include"],
2822            json!(["reasoning.encrypted_content"])
2823        );
2824    }
2825
2826    #[test]
2827    fn responses_stream_maps_tool_calls() {
2828        let events = vec![
2829            ResponsesStreamEvent::OutputItemAdded {
2830                output_index: 0,
2831                sequence_number: None,
2832                item: response_item_function_call("item_fn", Some("{\"city\":\"Bos")),
2833            },
2834            ResponsesStreamEvent::FunctionCallArgumentsDelta {
2835                item_id: "item_fn".into(),
2836                output_index: 0,
2837                delta: "ton\"}".into(),
2838                sequence_number: None,
2839            },
2840            ResponsesStreamEvent::FunctionCallArgumentsDone {
2841                item_id: "item_fn".into(),
2842                output_index: 0,
2843                arguments: "{\"city\":\"Boston\"}".into(),
2844                sequence_number: None,
2845            },
2846            ResponsesStreamEvent::Completed {
2847                response: ResponseSummary::default(),
2848            },
2849        ];
2850
2851        let mapped = map_response_events(events);
2852        assert_eq!(mapped.len(), 3);
2853        assert!(matches!(
2854            mapped[0],
2855            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
2856                is_input_complete: false,
2857                ..
2858            })
2859        ));
2860        assert!(matches!(
2861            mapped[1],
2862            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
2863                ref id,
2864                ref name,
2865                ref raw_input,
2866                is_input_complete: true,
2867                ..
2868            }) if id.to_string() == "call_123"
2869                && name.as_ref() == "get_weather"
2870                && raw_input == "{\"city\":\"Boston\"}"
2871        ));
2872        assert!(matches!(
2873            mapped[2],
2874            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
2875        ));
2876    }
2877
2878    #[test]
2879    fn responses_stream_uses_max_tokens_stop_reason() {
2880        let events = vec![ResponsesStreamEvent::Incomplete {
2881            response: ResponseSummary {
2882                incomplete_details: Some(ResponseIncompleteDetails {
2883                    reason: Some("max_tokens".into()),
2884                }),
2885                usage: Some(ResponseUsage {
2886                    input_tokens: Some(10),
2887                    output_tokens: Some(20),
2888                    total_tokens: Some(30),
2889                    ..Default::default()
2890                }),
2891                ..Default::default()
2892            },
2893        }];
2894
2895        let mapped = map_response_events(events);
2896        assert!(matches!(
2897            mapped[0],
2898            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
2899                input_tokens: 10,
2900                output_tokens: 20,
2901                ..
2902            })
2903        ));
2904        assert!(matches!(
2905            mapped[1],
2906            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
2907        ));
2908    }
2909
2910    #[test]
2911    fn responses_stream_failed_uses_response_error_message() {
2912        let mut mapper = OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID);
2913        let mapped = mapper.map_event(ResponsesStreamEvent::Failed {
2914            response: ResponseSummary {
2915                status: Some("failed".into()),
2916                error: Some(ResponseError {
2917                    code: Some("server_error".into()),
2918                    message: "The model failed to generate a response.".into(),
2919                    param: None,
2920                }),
2921                ..Default::default()
2922            },
2923        });
2924
2925        assert_eq!(mapped.len(), 1);
2926        let error = mapped.into_iter().next().unwrap().unwrap_err();
2927        assert_eq!(
2928            error.to_string(),
2929            "server_error: The model failed to generate a response."
2930        );
2931    }
2932
2933    #[test]
2934    fn responses_stream_deserializes_documented_error_event() {
2935        let event = serde_json::from_value::<ResponsesStreamEvent>(json!({
2936            "type": "error",
2937            "code": "ERR_SOMETHING",
2938            "message": "Something went wrong",
2939            "param": null,
2940            "sequence_number": 1
2941        }))
2942        .expect("documented error event");
2943
2944        let mut mapper = OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID);
2945        let mapped = mapper.map_event(event);
2946
2947        assert_eq!(mapped.len(), 1);
2948        let error = mapped.into_iter().next().unwrap().unwrap_err();
2949        assert_eq!(error.to_string(), "ERR_SOMETHING: Something went wrong");
2950    }
2951
2952    #[test]
2953    fn responses_stream_deserializes_nested_error_event() {
2954        // In practice the Responses API often nests error fields under an
2955        // `error` object even though the public spec documents them at the top
2956        // level. Make sure we don't lose the message and code in that case.
2957        let event = serde_json::from_value::<ResponsesStreamEvent>(json!({
2958            "type": "error",
2959            "error": {
2960                "type": "invalid_request_error",
2961                "code": "invalid_prompt",
2962                "message": "Your prompt was flagged.",
2963                "param": "input"
2964            },
2965            "sequence_number": 2
2966        }))
2967        .expect("nested error event");
2968
2969        let mut mapper = OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID);
2970        let mapped = mapper.map_event(event);
2971
2972        assert_eq!(mapped.len(), 1);
2973        let error = mapped.into_iter().next().unwrap().unwrap_err();
2974        assert_eq!(
2975            error.to_string(),
2976            "invalid_prompt: Your prompt was flagged."
2977        );
2978    }
2979
2980    #[test]
2981    fn responses_stream_maps_context_length_exceeded_to_prompt_too_large() {
2982        let event = serde_json::from_value::<ResponsesStreamEvent>(json!({
2983            "type": "error",
2984            "error": {
2985                "type": "invalid_request_error",
2986                "code": "context_length_exceeded",
2987                "message": "Your input exceeds the context window of this model. Please adjust your input and try again.",
2988                "param": "input"
2989            },
2990            "sequence_number": 2
2991        }))
2992        .expect("nested error event");
2993
2994        let mut mapper = OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID);
2995        let mapped = mapper.map_event(event);
2996
2997        assert_eq!(mapped.len(), 1);
2998        let error = mapped.into_iter().next().unwrap().unwrap_err();
2999        assert!(matches!(
3000            error,
3001            LanguageModelCompletionError::PromptTooLarge { tokens: None }
3002        ));
3003    }
3004
3005    #[test]
3006    fn responses_stream_maps_failed_context_length_exceeded_to_prompt_too_large() {
3007        let mut mapper = OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID);
3008        let mapped = mapper.map_event(ResponsesStreamEvent::Failed {
3009            response: ResponseSummary {
3010                status: Some("failed".into()),
3011                error: Some(ResponseError {
3012                    code: Some("context_length_exceeded".into()),
3013                    message: "Your input exceeds the context window of this model.".into(),
3014                    param: Some("input".into()),
3015                }),
3016                ..Default::default()
3017            },
3018        });
3019
3020        assert_eq!(mapped.len(), 1);
3021        let error = mapped.into_iter().next().unwrap().unwrap_err();
3022        assert!(matches!(
3023            error,
3024            LanguageModelCompletionError::PromptTooLarge { tokens: None }
3025        ));
3026    }
3027
3028    #[test]
3029    fn responses_stream_deserializes_response_error_event() {
3030        let event = serde_json::from_value::<ResponsesStreamEvent>(json!({
3031            "type": "response.error",
3032            "error": {
3033                "code": "invalid_request_error",
3034                "message": "Invalid request."
3035            }
3036        }))
3037        .expect("response error event");
3038
3039        let mut mapper = OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID);
3040        let mapped = mapper.map_event(event);
3041
3042        assert_eq!(mapped.len(), 1);
3043        let error = mapped.into_iter().next().unwrap().unwrap_err();
3044        assert_eq!(error.to_string(), "invalid_request_error: Invalid request.");
3045    }
3046
3047    #[test]
3048    fn responses_stream_maps_refusal_events_to_refusal_stop() {
3049        let delta = serde_json::from_value::<ResponsesStreamEvent>(json!({
3050            "type": "response.refusal.delta",
3051            "item_id": "msg_123",
3052            "output_index": 0,
3053            "content_index": 0,
3054            "delta": "I can't help",
3055            "sequence_number": 1
3056        }))
3057        .expect("documented refusal delta event");
3058        let done = serde_json::from_value::<ResponsesStreamEvent>(json!({
3059            "type": "response.refusal.done",
3060            "item_id": "msg_123",
3061            "output_index": 0,
3062            "content_index": 0,
3063            "refusal": "I can't help with that.",
3064            "sequence_number": 2
3065        }))
3066        .expect("documented refusal done event");
3067
3068        let mapped = map_response_events(vec![
3069            delta,
3070            done,
3071            ResponsesStreamEvent::Completed {
3072                response: ResponseSummary::default(),
3073            },
3074        ]);
3075
3076        assert_eq!(mapped.len(), 1);
3077        assert!(matches!(
3078            mapped[0],
3079            LanguageModelCompletionEvent::Stop(StopReason::Refusal)
3080        ));
3081    }
3082
3083    #[test]
3084    fn responses_stream_maps_refusal_output_to_refusal_stop() {
3085        let mapped = map_response_events(vec![ResponsesStreamEvent::Completed {
3086            response: ResponseSummary {
3087                output: vec![ResponseOutputItem::Message(ResponseOutputMessage {
3088                    id: Some("msg_123".into()),
3089                    role: Some("assistant".into()),
3090                    status: Some("completed".into()),
3091                    content: vec![json!({
3092                        "type": "refusal",
3093                        "refusal": "I can't help with that."
3094                    })],
3095                    phase: None,
3096                })],
3097                ..Default::default()
3098            },
3099        }]);
3100
3101        assert_eq!(mapped.len(), 1);
3102        assert!(matches!(
3103            mapped[0],
3104            LanguageModelCompletionEvent::Stop(StopReason::Refusal)
3105        ));
3106    }
3107
3108    #[test]
3109    fn responses_stream_handles_multiple_tool_calls() {
3110        let events = vec![
3111            ResponsesStreamEvent::OutputItemAdded {
3112                output_index: 0,
3113                sequence_number: None,
3114                item: response_item_function_call("item_fn1", Some("{\"city\":\"NYC\"}")),
3115            },
3116            ResponsesStreamEvent::FunctionCallArgumentsDone {
3117                item_id: "item_fn1".into(),
3118                output_index: 0,
3119                arguments: "{\"city\":\"NYC\"}".into(),
3120                sequence_number: None,
3121            },
3122            ResponsesStreamEvent::OutputItemAdded {
3123                output_index: 1,
3124                sequence_number: None,
3125                item: response_item_function_call("item_fn2", Some("{\"city\":\"LA\"}")),
3126            },
3127            ResponsesStreamEvent::FunctionCallArgumentsDone {
3128                item_id: "item_fn2".into(),
3129                output_index: 1,
3130                arguments: "{\"city\":\"LA\"}".into(),
3131                sequence_number: None,
3132            },
3133            ResponsesStreamEvent::Completed {
3134                response: ResponseSummary::default(),
3135            },
3136        ];
3137
3138        let mapped = map_response_events(events);
3139        assert_eq!(mapped.len(), 3);
3140        assert!(matches!(
3141            mapped[0],
3142            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
3143            if raw_input == "{\"city\":\"NYC\"}"
3144        ));
3145        assert!(matches!(
3146            mapped[1],
3147            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
3148            if raw_input == "{\"city\":\"LA\"}"
3149        ));
3150        assert!(matches!(
3151            mapped[2],
3152            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
3153        ));
3154    }
3155
3156    #[test]
3157    fn responses_stream_handles_mixed_text_and_tool_calls() {
3158        let events = vec![
3159            ResponsesStreamEvent::OutputItemAdded {
3160                output_index: 0,
3161                sequence_number: None,
3162                item: response_item_message("msg_123"),
3163            },
3164            ResponsesStreamEvent::OutputTextDelta {
3165                item_id: "msg_123".into(),
3166                output_index: 0,
3167                content_index: Some(0),
3168                delta: "Let me check that".into(),
3169            },
3170            ResponsesStreamEvent::OutputItemAdded {
3171                output_index: 1,
3172                sequence_number: None,
3173                item: response_item_function_call("item_fn", Some("{\"query\":\"test\"}")),
3174            },
3175            ResponsesStreamEvent::FunctionCallArgumentsDone {
3176                item_id: "item_fn".into(),
3177                output_index: 1,
3178                arguments: "{\"query\":\"test\"}".into(),
3179                sequence_number: None,
3180            },
3181            ResponsesStreamEvent::Completed {
3182                response: ResponseSummary::default(),
3183            },
3184        ];
3185
3186        let mapped = map_response_events(events);
3187        assert!(matches!(
3188            mapped[0],
3189            LanguageModelCompletionEvent::StartMessage { .. }
3190        ));
3191        assert!(
3192            matches!(mapped[1], LanguageModelCompletionEvent::Text(ref text) if text == "Let me check that")
3193        );
3194        assert!(
3195            matches!(mapped[2], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"query\":\"test\"}")
3196        );
3197        assert!(matches!(
3198            mapped[3],
3199            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
3200        ));
3201    }
3202
3203    #[test]
3204    fn responses_stream_handles_json_parse_error() {
3205        let events = vec![
3206            ResponsesStreamEvent::OutputItemAdded {
3207                output_index: 0,
3208                sequence_number: None,
3209                item: response_item_function_call("item_fn", Some("{invalid json")),
3210            },
3211            ResponsesStreamEvent::FunctionCallArgumentsDone {
3212                item_id: "item_fn".into(),
3213                output_index: 0,
3214                arguments: "{invalid json".into(),
3215                sequence_number: None,
3216            },
3217            ResponsesStreamEvent::Completed {
3218                response: ResponseSummary::default(),
3219            },
3220        ];
3221
3222        let mapped = map_response_events(events);
3223        assert!(matches!(
3224            mapped[0],
3225            LanguageModelCompletionEvent::ToolUseJsonParseError { ref raw_input, .. }
3226            if raw_input.as_ref() == "{invalid json"
3227        ));
3228    }
3229
3230    #[test]
3231    fn responses_stream_handles_incomplete_function_call() {
3232        let events = vec![
3233            ResponsesStreamEvent::OutputItemAdded {
3234                output_index: 0,
3235                sequence_number: None,
3236                item: response_item_function_call("item_fn", Some("{\"city\":")),
3237            },
3238            ResponsesStreamEvent::FunctionCallArgumentsDelta {
3239                item_id: "item_fn".into(),
3240                output_index: 0,
3241                delta: "\"Boston\"".into(),
3242                sequence_number: None,
3243            },
3244            ResponsesStreamEvent::Incomplete {
3245                response: ResponseSummary {
3246                    incomplete_details: Some(ResponseIncompleteDetails {
3247                        reason: Some("max_tokens".into()),
3248                    }),
3249                    output: vec![response_item_function_call(
3250                        "item_fn",
3251                        Some("{\"city\":\"Boston\"}"),
3252                    )],
3253                    ..Default::default()
3254                },
3255            },
3256        ];
3257
3258        let mapped = map_response_events(events);
3259        assert_eq!(mapped.len(), 3);
3260        assert!(matches!(
3261            mapped[0],
3262            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
3263                is_input_complete: false,
3264                ..
3265            })
3266        ));
3267        assert!(
3268            matches!(mapped[1], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, is_input_complete: true, .. }) if raw_input == "{\"city\":\"Boston\"}")
3269        );
3270        assert!(matches!(
3271            mapped[2],
3272            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
3273        ));
3274    }
3275
3276    #[test]
3277    fn responses_stream_incomplete_does_not_duplicate_tool_calls() {
3278        let events = vec![
3279            ResponsesStreamEvent::OutputItemAdded {
3280                output_index: 0,
3281                sequence_number: None,
3282                item: response_item_function_call("item_fn", Some("{\"city\":\"Boston\"}")),
3283            },
3284            ResponsesStreamEvent::FunctionCallArgumentsDone {
3285                item_id: "item_fn".into(),
3286                output_index: 0,
3287                arguments: "{\"city\":\"Boston\"}".into(),
3288                sequence_number: None,
3289            },
3290            ResponsesStreamEvent::Incomplete {
3291                response: ResponseSummary {
3292                    incomplete_details: Some(ResponseIncompleteDetails {
3293                        reason: Some("max_tokens".into()),
3294                    }),
3295                    output: vec![response_item_function_call(
3296                        "item_fn",
3297                        Some("{\"city\":\"Boston\"}"),
3298                    )],
3299                    ..Default::default()
3300                },
3301            },
3302        ];
3303
3304        let mapped = map_response_events(events);
3305        assert_eq!(mapped.len(), 2);
3306        assert!(
3307            matches!(mapped[0], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"city\":\"Boston\"}")
3308        );
3309        assert!(matches!(
3310            mapped[1],
3311            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
3312        ));
3313    }
3314
3315    #[test]
3316    fn responses_stream_handles_empty_tool_arguments() {
3317        let events = vec![
3318            ResponsesStreamEvent::OutputItemAdded {
3319                output_index: 0,
3320                sequence_number: None,
3321                item: response_item_function_call("item_fn", Some("")),
3322            },
3323            ResponsesStreamEvent::FunctionCallArgumentsDone {
3324                item_id: "item_fn".into(),
3325                output_index: 0,
3326                arguments: "".into(),
3327                sequence_number: None,
3328            },
3329            ResponsesStreamEvent::Completed {
3330                response: ResponseSummary::default(),
3331            },
3332        ];
3333
3334        let mapped = map_response_events(events);
3335        assert_eq!(mapped.len(), 2);
3336        assert!(matches!(
3337            &mapped[0],
3338            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
3339                id, name, raw_input, input, ..
3340            }) if id.to_string() == "call_123"
3341                && name.as_ref() == "get_weather"
3342                && raw_input == ""
3343                && matches!(input, LanguageModelToolUseInput::Json(value) if value.as_object().is_some_and(|object| object.is_empty()))
3344        ));
3345        assert!(matches!(
3346            mapped[1],
3347            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
3348        ));
3349    }
3350
3351    #[test]
3352    fn responses_stream_emits_partial_tool_use_events() {
3353        let events = vec![
3354            ResponsesStreamEvent::OutputItemAdded {
3355                output_index: 0,
3356                sequence_number: None,
3357                item: ResponseOutputItem::FunctionCall(
3358                    crate::responses::ResponseFunctionToolCall {
3359                        id: Some("item_fn".to_string()),
3360                        status: Some("in_progress".to_string()),
3361                        name: Some("get_weather".to_string()),
3362                        call_id: Some("call_abc".to_string()),
3363                        arguments: String::new(),
3364                    },
3365                ),
3366            },
3367            ResponsesStreamEvent::FunctionCallArgumentsDelta {
3368                item_id: "item_fn".into(),
3369                output_index: 0,
3370                delta: "{\"city\":\"Bos".into(),
3371                sequence_number: None,
3372            },
3373            ResponsesStreamEvent::FunctionCallArgumentsDelta {
3374                item_id: "item_fn".into(),
3375                output_index: 0,
3376                delta: "ton\"}".into(),
3377                sequence_number: None,
3378            },
3379            ResponsesStreamEvent::FunctionCallArgumentsDone {
3380                item_id: "item_fn".into(),
3381                output_index: 0,
3382                arguments: "{\"city\":\"Boston\"}".into(),
3383                sequence_number: None,
3384            },
3385            ResponsesStreamEvent::Completed {
3386                response: ResponseSummary::default(),
3387            },
3388        ];
3389
3390        let mapped = map_response_events(events);
3391        assert!(mapped.len() >= 3);
3392
3393        let complete_tool_use = mapped.iter().find(|e| {
3394            matches!(
3395                e,
3396                LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
3397                    is_input_complete: true,
3398                    ..
3399                })
3400            )
3401        });
3402        assert!(
3403            complete_tool_use.is_some(),
3404            "should have a complete tool use event"
3405        );
3406
3407        let tool_uses: Vec<_> = mapped
3408            .iter()
3409            .filter(|e| matches!(e, LanguageModelCompletionEvent::ToolUse(_)))
3410            .collect();
3411        assert!(
3412            tool_uses.len() >= 2,
3413            "should have at least one partial and one complete event"
3414        );
3415        assert!(matches!(
3416            tool_uses.last().unwrap(),
3417            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
3418                is_input_complete: true,
3419                ..
3420            })
3421        ));
3422    }
3423
3424    #[test]
3425    fn responses_stream_maps_reasoning_summary_deltas() {
3426        let events = vec![
3427            ResponsesStreamEvent::OutputItemAdded {
3428                output_index: 0,
3429                sequence_number: None,
3430                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3431                    "rs_123",
3432                    vec![],
3433                    None,
3434                    None,
3435                )),
3436            },
3437            ResponsesStreamEvent::ReasoningSummaryPartAdded {
3438                item_id: "rs_123".into(),
3439                output_index: 0,
3440                summary_index: 0,
3441            },
3442            ResponsesStreamEvent::ReasoningSummaryTextDelta {
3443                item_id: "rs_123".into(),
3444                output_index: 0,
3445                delta: "Thinking about".into(),
3446            },
3447            ResponsesStreamEvent::ReasoningSummaryTextDelta {
3448                item_id: "rs_123".into(),
3449                output_index: 0,
3450                delta: " the answer".into(),
3451            },
3452            ResponsesStreamEvent::ReasoningSummaryTextDone {
3453                item_id: "rs_123".into(),
3454                output_index: 0,
3455                text: "Thinking about the answer".into(),
3456            },
3457            ResponsesStreamEvent::ReasoningSummaryPartDone {
3458                item_id: "rs_123".into(),
3459                output_index: 0,
3460                summary_index: 0,
3461            },
3462            ResponsesStreamEvent::ReasoningSummaryPartAdded {
3463                item_id: "rs_123".into(),
3464                output_index: 0,
3465                summary_index: 1,
3466            },
3467            ResponsesStreamEvent::ReasoningSummaryTextDelta {
3468                item_id: "rs_123".into(),
3469                output_index: 0,
3470                delta: "Second part".into(),
3471            },
3472            ResponsesStreamEvent::ReasoningSummaryTextDone {
3473                item_id: "rs_123".into(),
3474                output_index: 0,
3475                text: "Second part".into(),
3476            },
3477            ResponsesStreamEvent::ReasoningSummaryPartDone {
3478                item_id: "rs_123".into(),
3479                output_index: 0,
3480                summary_index: 1,
3481            },
3482            ResponsesStreamEvent::OutputItemDone {
3483                output_index: 0,
3484                sequence_number: None,
3485                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3486                    "rs_123",
3487                    vec![
3488                        ReasoningSummaryPart::SummaryText {
3489                            text: "Thinking about the answer".into(),
3490                        },
3491                        ReasoningSummaryPart::SummaryText {
3492                            text: "Second part".into(),
3493                        },
3494                    ],
3495                    None,
3496                    None,
3497                )),
3498            },
3499            ResponsesStreamEvent::OutputItemAdded {
3500                output_index: 1,
3501                sequence_number: None,
3502                item: response_item_message("msg_456"),
3503            },
3504            ResponsesStreamEvent::OutputTextDelta {
3505                item_id: "msg_456".into(),
3506                output_index: 1,
3507                content_index: Some(0),
3508                delta: "The answer is 42".into(),
3509            },
3510            ResponsesStreamEvent::Completed {
3511                response: ResponseSummary::default(),
3512            },
3513        ];
3514
3515        let mapped = map_response_events(events);
3516
3517        let thinking_events: Vec<_> = mapped
3518            .iter()
3519            .filter(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. }))
3520            .collect();
3521        assert_eq!(
3522            thinking_events.len(),
3523            4,
3524            "expected 4 thinking events, got {:?}",
3525            thinking_events
3526        );
3527        assert!(
3528            matches!(&thinking_events[0], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Thinking about")
3529        );
3530        assert!(
3531            matches!(&thinking_events[1], LanguageModelCompletionEvent::Thinking { text, .. } if text == " the answer")
3532        );
3533        assert!(
3534            matches!(&thinking_events[2], LanguageModelCompletionEvent::Thinking { text, .. } if text == "\n\n"),
3535            "expected separator between summary parts"
3536        );
3537        assert!(
3538            matches!(&thinking_events[3], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Second part")
3539        );
3540
3541        assert!(mapped.iter().any(
3542            |e| matches!(e, LanguageModelCompletionEvent::Text(t) if t == "The answer is 42")
3543        ));
3544    }
3545
3546    #[test]
3547    fn responses_stream_maps_reasoning_from_done_only() {
3548        let events = vec![
3549            ResponsesStreamEvent::OutputItemAdded {
3550                output_index: 0,
3551                sequence_number: None,
3552                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3553                    "rs_789",
3554                    vec![],
3555                    None,
3556                    None,
3557                )),
3558            },
3559            ResponsesStreamEvent::OutputItemDone {
3560                output_index: 0,
3561                sequence_number: None,
3562                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3563                    "rs_789",
3564                    vec![ReasoningSummaryPart::SummaryText {
3565                        text: "Summary without deltas".into(),
3566                    }],
3567                    None,
3568                    None,
3569                )),
3570            },
3571            ResponsesStreamEvent::Completed {
3572                response: ResponseSummary::default(),
3573            },
3574        ];
3575
3576        let mapped = map_response_events(events);
3577        assert!(
3578            !mapped
3579                .iter()
3580                .any(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. })),
3581            "OutputItemDone reasoning should not produce Thinking events"
3582        );
3583    }
3584
3585    #[test]
3586    fn responses_stream_preserves_encrypted_reasoning_details() {
3587        let mut reasoning_item = response_reasoning_item(
3588            "rs_123",
3589            vec![ReasoningSummaryPart::SummaryText {
3590                text: "Checked what information is needed.".into(),
3591            }],
3592            Some("ENC"),
3593            Some("completed".into()),
3594        );
3595        reasoning_item.content = vec![json!({
3596            "type": "reasoning_text",
3597            "text": "Internal reasoning text."
3598        })];
3599
3600        let events = vec![
3601            ResponsesStreamEvent::OutputItemDone {
3602                output_index: 0,
3603                sequence_number: None,
3604                item: ResponseOutputItem::Reasoning(reasoning_item),
3605            },
3606            ResponsesStreamEvent::Completed {
3607                response: ResponseSummary::default(),
3608            },
3609        ];
3610
3611        let mapped = map_response_events(events);
3612        let details = mapped
3613            .iter()
3614            .find_map(|event| match event {
3615                LanguageModelCompletionEvent::ReasoningDetails(details) => Some(details),
3616                _ => None,
3617            })
3618            .expect("reasoning details");
3619
3620        assert_eq!(
3621            details,
3622            &json!({
3623                "reasoning_items": [
3624                    {
3625                        "id": "rs_123",
3626                        "summary": [
3627                            {
3628                                "type": "summary_text",
3629                                "text": "Checked what information is needed."
3630                            }
3631                        ],
3632                        "content": [
3633                            {
3634                                "type": "reasoning_text",
3635                                "text": "Internal reasoning text."
3636                            }
3637                        ],
3638                        "encrypted_content": "ENC",
3639                        "status": "completed",
3640                    }
3641                ]
3642            })
3643        );
3644    }
3645
3646    #[test]
3647    fn responses_stream_replaces_reasoning_details_with_same_id() {
3648        let events = vec![
3649            ResponsesStreamEvent::OutputItemDone {
3650                output_index: 0,
3651                sequence_number: None,
3652                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3653                    "rs_123",
3654                    Vec::new(),
3655                    Some("ENC_OLD"),
3656                    Some("in_progress".into()),
3657                )),
3658            },
3659            ResponsesStreamEvent::OutputItemDone {
3660                output_index: 0,
3661                sequence_number: None,
3662                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3663                    "rs_123",
3664                    vec![ReasoningSummaryPart::SummaryText {
3665                        text: "Finished reasoning.".into(),
3666                    }],
3667                    Some("ENC_NEW"),
3668                    Some("completed".into()),
3669                )),
3670            },
3671            ResponsesStreamEvent::Completed {
3672                response: ResponseSummary::default(),
3673            },
3674        ];
3675
3676        let mapped = map_response_events(events);
3677        let details = mapped
3678            .iter()
3679            .filter_map(|event| match event {
3680                LanguageModelCompletionEvent::ReasoningDetails(details) => Some(details),
3681                _ => None,
3682            })
3683            .next_back()
3684            .expect("reasoning details");
3685
3686        assert_eq!(
3687            details,
3688            &json!({
3689                "reasoning_items": [
3690                    {
3691                        "id": "rs_123",
3692                        "summary": [
3693                            {
3694                                "type": "summary_text",
3695                                "text": "Finished reasoning."
3696                            }
3697                        ],
3698                        "encrypted_content": "ENC_NEW",
3699                        "status": "completed"
3700                    }
3701                ]
3702            })
3703        );
3704    }
3705
3706    #[test]
3707    fn responses_stream_reemits_reasoning_details_after_phase_less_message_start() {
3708        let events = vec![
3709            ResponsesStreamEvent::OutputItemDone {
3710                output_index: 0,
3711                sequence_number: None,
3712                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3713                    "rs_123",
3714                    Vec::new(),
3715                    Some("ENC"),
3716                    Some("completed".into()),
3717                )),
3718            },
3719            ResponsesStreamEvent::OutputItemAdded {
3720                output_index: 1,
3721                sequence_number: None,
3722                item: ResponseOutputItem::Message(ResponseOutputMessage {
3723                    id: Some("msg_123".into()),
3724                    role: Some("assistant".into()),
3725                    status: Some("in_progress".into()),
3726                    content: vec![],
3727                    phase: None,
3728                }),
3729            },
3730            ResponsesStreamEvent::OutputTextDelta {
3731                item_id: "msg_123".into(),
3732                output_index: 1,
3733                content_index: Some(0),
3734                delta: "Hello".into(),
3735            },
3736            ResponsesStreamEvent::Completed {
3737                response: ResponseSummary::default(),
3738            },
3739        ];
3740
3741        let mapped = map_response_events(events);
3742        let start_message_index = mapped
3743            .iter()
3744            .position(|event| matches!(event, LanguageModelCompletionEvent::StartMessage { .. }))
3745            .expect("start message");
3746        let details = mapped
3747            .iter()
3748            .skip(start_message_index + 1)
3749            .find_map(|event| match event {
3750                LanguageModelCompletionEvent::ReasoningDetails(details) => Some(details),
3751                _ => None,
3752            })
3753            .expect("reasoning details after start message");
3754
3755        assert_eq!(
3756            details,
3757            &json!({
3758                "reasoning_items": [
3759                    {
3760                        "id": "rs_123",
3761                        "summary": [],
3762                        "encrypted_content": "ENC",
3763                        "status": "completed"
3764                    }
3765                ]
3766            })
3767        );
3768    }
3769
3770    #[test]
3771    fn responses_stream_preserves_assistant_phase_with_reasoning_details() {
3772        let events = vec![
3773            ResponsesStreamEvent::OutputItemAdded {
3774                output_index: 0,
3775                sequence_number: None,
3776                item: ResponseOutputItem::Message(ResponseOutputMessage {
3777                    id: Some("msg_123".into()),
3778                    role: Some("assistant".into()),
3779                    status: Some("in_progress".into()),
3780                    content: vec![],
3781                    phase: Some("commentary".into()),
3782                }),
3783            },
3784            ResponsesStreamEvent::OutputTextDelta {
3785                item_id: "msg_123".into(),
3786                output_index: 0,
3787                content_index: Some(0),
3788                delta: "I will inspect the workspace.".into(),
3789            },
3790            ResponsesStreamEvent::OutputItemDone {
3791                output_index: 1,
3792                sequence_number: None,
3793                item: ResponseOutputItem::Reasoning(response_reasoning_item(
3794                    "rs_123",
3795                    Vec::new(),
3796                    Some("ENC"),
3797                    Some("completed".into()),
3798                )),
3799            },
3800            ResponsesStreamEvent::Completed {
3801                response: ResponseSummary::default(),
3802            },
3803        ];
3804
3805        let mapped = map_response_events(events);
3806        let details = mapped
3807            .iter()
3808            .filter_map(|event| match event {
3809                LanguageModelCompletionEvent::ReasoningDetails(details) => Some(details),
3810                _ => None,
3811            })
3812            .next_back()
3813            .expect("reasoning details");
3814
3815        assert_eq!(
3816            details,
3817            &json!({
3818                "phase": "commentary",
3819                "reasoning_items": [
3820                    {
3821                        "id": "rs_123",
3822                        "summary": [],
3823                        "encrypted_content": "ENC",
3824                        "status": "completed"
3825                    }
3826                ]
3827            })
3828        );
3829    }
3830
3831    #[test]
3832    fn into_open_ai_interleaved_reasoning() {
3833        let tool_use_id = LanguageModelToolUseId::from("call-1");
3834        let tool_input = json!({"query": "foo"});
3835        let tool_arguments = serde_json::to_string(&tool_input).unwrap();
3836        let tool_use = LanguageModelToolUse {
3837            id: tool_use_id.clone(),
3838            name: Arc::from("search"),
3839            raw_input: tool_arguments.clone(),
3840            input: LanguageModelToolUseInput::Json(tool_input),
3841            is_input_complete: true,
3842            thought_signature: None,
3843        };
3844        let tool_result = LanguageModelToolResult {
3845            tool_use_id: tool_use_id,
3846            tool_name: Arc::from("search"),
3847            is_error: false,
3848            content: vec![LanguageModelToolResultContent::Text(Arc::from("result"))],
3849            output: None,
3850        };
3851        let request = LanguageModelRequest {
3852            thread_id: None,
3853            prompt_id: None,
3854            intent: None,
3855            messages: vec![
3856                LanguageModelRequestMessage {
3857                    role: Role::User,
3858                    content: vec![MessageContent::Text("search for something".into())],
3859                    cache: false,
3860                    reasoning_details: None,
3861                },
3862                LanguageModelRequestMessage {
3863                    role: Role::Assistant,
3864                    content: vec![
3865                        MessageContent::Thinking {
3866                            text: "I should search".into(),
3867                            signature: None,
3868                        },
3869                        MessageContent::Text("Searching now.".into()),
3870                        MessageContent::ToolUse(tool_use),
3871                    ],
3872                    cache: false,
3873                    reasoning_details: None,
3874                },
3875                LanguageModelRequestMessage {
3876                    role: Role::Assistant,
3877                    content: vec![MessageContent::ToolResult(tool_result)],
3878                    cache: false,
3879                    reasoning_details: None,
3880                },
3881            ],
3882            tools: vec![],
3883            tool_choice: None,
3884            stop: vec![],
3885            temperature: None,
3886            thinking_allowed: true,
3887            thinking_effort: None,
3888            speed: None,
3889            compact_at_tokens: None,
3890        };
3891
3892        let result = into_open_ai(
3893            request.clone(),
3894            "model",
3895            false,
3896            false,
3897            None,
3898            ChatCompletionMaxTokensParameter::MaxCompletionTokens,
3899            None,
3900            true,
3901        )
3902        .unwrap();
3903        assert_eq!(
3904            serde_json::to_value(&result).unwrap()["messages"],
3905            json!([
3906                {"role": "user", "content": "search for something"},
3907                {
3908                    "role": "assistant",
3909                    "content": "Searching now.",
3910                    "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}],
3911                    "reasoning_content": "I should search"
3912                },
3913                {"role": "tool", "content": "result", "tool_call_id": "call-1"}
3914            ])
3915        );
3916
3917        let result = into_open_ai(
3918            request,
3919            "model",
3920            false,
3921            false,
3922            None,
3923            ChatCompletionMaxTokensParameter::MaxCompletionTokens,
3924            None,
3925            false,
3926        )
3927        .unwrap();
3928        assert_eq!(
3929            serde_json::to_value(&result).unwrap()["messages"],
3930            json!([
3931                {"role": "user", "content": "search for something"},
3932                {
3933                    "role": "assistant",
3934                    "content": [
3935                        {"type": "text", "text": "I should search"},
3936                        {"type": "text", "text": "Searching now."}
3937                    ],
3938                    "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}]
3939                },
3940                {"role": "tool", "content": "result", "tool_call_id": "call-1"}
3941            ])
3942        );
3943    }
3944
3945    #[test]
3946    fn stream_maps_reasoning() {
3947        let events = map_completion_events(vec![ResponseStreamEvent {
3948            choices: vec![ChoiceDelta {
3949                index: 0,
3950                delta: Some(ResponseMessageDelta {
3951                    role: None,
3952                    content: None,
3953                    reasoning: Some("thinking".into()),
3954                    tool_calls: None,
3955                    reasoning_content: None,
3956                }),
3957                finish_reason: None,
3958            }],
3959            usage: None,
3960        }]);
3961
3962        assert_eq!(
3963            events,
3964            vec![LanguageModelCompletionEvent::Thinking {
3965                text: "thinking".into(),
3966                signature: None,
3967            }]
3968        );
3969    }
3970
3971    #[test]
3972    fn stream_maps_preserves_tool_id_and_name_across_empty_deltas() {
3973        // DashScope sends id="" and name="" in subsequent tool_calls delta
3974        // chunks after the first chunk. OpenAiEventMapper must not overwrite
3975        // the accumulated id and name with these empty strings.
3976
3977        let events = vec![
3978            // First chunk: id and name are present
3979            ResponseStreamEvent {
3980                choices: vec![ChoiceDelta {
3981                    index: 0,
3982                    delta: Some(ResponseMessageDelta {
3983                        role: None,
3984                        content: None,
3985                        reasoning: None,
3986                        tool_calls: Some(vec![ToolCallChunk {
3987                            index: 0,
3988                            id: Some("call_dashscope_test".into()),
3989                            function: Some(FunctionChunk {
3990                                name: Some("list_directory".into()),
3991                                arguments: Some("".into()),
3992                            }),
3993                        }]),
3994                        reasoning_content: None,
3995                    }),
3996                    finish_reason: None,
3997                }],
3998                usage: None,
3999            },
4000            // Subsequent chunks: DashScope sends id="" and name=""
4001            ResponseStreamEvent {
4002                choices: vec![ChoiceDelta {
4003                    index: 0,
4004                    delta: Some(ResponseMessageDelta {
4005                        role: None,
4006                        content: None,
4007                        reasoning: None,
4008                        tool_calls: Some(vec![ToolCallChunk {
4009                            index: 0,
4010                            id: Some("".into()),
4011                            function: Some(FunctionChunk {
4012                                name: Some("".into()),
4013                                arguments: Some("{\"path\": \"".into()),
4014                            }),
4015                        }]),
4016                        reasoning_content: None,
4017                    }),
4018                    finish_reason: None,
4019                }],
4020                usage: None,
4021            },
4022            ResponseStreamEvent {
4023                choices: vec![ChoiceDelta {
4024                    index: 0,
4025                    delta: Some(ResponseMessageDelta {
4026                        role: None,
4027                        content: None,
4028                        reasoning: None,
4029                        tool_calls: Some(vec![ToolCallChunk {
4030                            index: 0,
4031                            id: Some("".into()),
4032                            function: Some(FunctionChunk {
4033                                name: Some("".into()),
4034                                arguments: Some("blog-scraper\"}".into()),
4035                            }),
4036                        }]),
4037                        reasoning_content: None,
4038                    }),
4039                    finish_reason: None,
4040                }],
4041                usage: None,
4042            },
4043            // Final chunk: finish_reason = "tool_calls"
4044            ResponseStreamEvent {
4045                choices: vec![ChoiceDelta {
4046                    index: 0,
4047                    delta: None,
4048                    finish_reason: Some("tool_calls".into()),
4049                }],
4050                usage: None,
4051            },
4052        ];
4053
4054        let mapped = map_completion_events(events);
4055
4056        // Events emitted:
4057        //   1. Partial ToolUse from chunk 1 (fix_json("") → "{}", parseable)
4058        //   2. Partial ToolUse from chunk 3 (arguments fully assembled)
4059        //   3. Complete ToolUse from finish_reason="tool_calls" drain
4060        //   4. Stop(ToolUse)
4061        assert_eq!(mapped.len(), 4);
4062
4063        // Verify the complete ToolUse event (from finish_reason drain)
4064        // has the correct id, name, and accumulated arguments.
4065        let complete_tool_use = mapped.iter().find_map(|event| {
4066            if let LanguageModelCompletionEvent::ToolUse(tool_use) = event {
4067                if tool_use.is_input_complete {
4068                    return Some(tool_use);
4069                }
4070            }
4071            None
4072        });
4073        assert!(
4074            complete_tool_use.is_some(),
4075            "expected a completed ToolUse event"
4076        );
4077        let tool_use = complete_tool_use.unwrap();
4078        assert_eq!(
4079            tool_use.id.to_string(),
4080            "call_dashscope_test",
4081            "id must survive empty-string overwrites"
4082        );
4083        assert_eq!(
4084            tool_use.name.as_ref(),
4085            "list_directory",
4086            "name must survive empty-string overwrites"
4087        );
4088        assert_eq!(
4089            tool_use.raw_input, "{\"path\": \"blog-scraper\"}",
4090            "arguments should accumulate across chunks"
4091        );
4092
4093        // Verify the Stop event
4094        assert!(mapped.iter().any(|event| {
4095            matches!(
4096                event,
4097                LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
4098            )
4099        }));
4100    }
4101
4102    #[test]
4103    fn into_open_ai_response_prepends_provider_input_unchanged() {
4104        let provider_input = json!([
4105            {
4106                "type": "message",
4107                "role": "user",
4108                "content": "Retained user context.",
4109                "provider_extension": {"preserve": true}
4110            },
4111            {
4112                "type": "compaction",
4113                "id": "cmp_manual",
4114                "encrypted_content": "opaque-state"
4115            }
4116        ]);
4117        let request = LanguageModelRequest {
4118            messages: vec![
4119                LanguageModelRequestMessage {
4120                    role: Role::Assistant,
4121                    content: vec![MessageContent::Compaction(CompactedContext::ProviderState(
4122                        provider_compaction_state_from_items(
4123                            OPEN_AI_PROVIDER_ID,
4124                            provider_input.as_array().unwrap().clone(),
4125                        )
4126                        .unwrap(),
4127                    ))],
4128                    cache: false,
4129                    reasoning_details: None,
4130                },
4131                LanguageModelRequestMessage {
4132                    role: Role::User,
4133                    content: vec![MessageContent::Text("Continue.".into())],
4134                    cache: false,
4135                    reasoning_details: None,
4136                },
4137            ],
4138            ..Default::default()
4139        };
4140
4141        let response = into_open_ai_response(
4142            request,
4143            "gpt-5.4",
4144            true,
4145            true,
4146            None,
4147            None,
4148            false,
4149            &OPEN_AI_PROVIDER_ID,
4150        )
4151        .unwrap();
4152
4153        assert_eq!(
4154            serde_json::to_value(&response).unwrap()["input"],
4155            json!([
4156                {
4157                    "type": "message",
4158                    "role": "user",
4159                    "content": "Retained user context.",
4160                    "provider_extension": {"preserve": true}
4161                },
4162                {
4163                    "type": "compaction",
4164                    "id": "cmp_manual",
4165                    "encrypted_content": "opaque-state"
4166                },
4167                {
4168                    "type": "message",
4169                    "role": "user",
4170                    "content": [{"type": "input_text", "text": "Continue."}]
4171                }
4172            ])
4173        );
4174    }
4175
4176    #[test]
4177    fn open_ai_response_converts_to_compact_request_with_supported_controls() {
4178        let request = LanguageModelRequest {
4179            thread_id: Some("thread-123".to_string()),
4180            messages: vec![LanguageModelRequestMessage {
4181                role: Role::User,
4182                content: vec![MessageContent::Text("Retain this context.".into())],
4183                cache: false,
4184                reasoning_details: None,
4185            }],
4186            speed: Some(Speed::Fast),
4187            compact_at_tokens: Some(100_000),
4188            ..Default::default()
4189        };
4190
4191        let mut response_request = into_open_ai_response(
4192            request,
4193            "gpt-5.4",
4194            true,
4195            true,
4196            None,
4197            None,
4198            false,
4199            &OPEN_AI_PROVIDER_ID,
4200        )
4201        .unwrap();
4202        response_request.instructions = Some("Preserve implementation details.".to_string());
4203        let compact_request = response_request.into_compact_request();
4204
4205        assert_eq!(
4206            serde_json::to_value(compact_request).unwrap(),
4207            json!({
4208                "model": "gpt-5.4",
4209                "instructions": "Preserve implementation details.",
4210                "input": [{
4211                    "type": "message",
4212                    "role": "user",
4213                    "content": [{
4214                        "type": "input_text",
4215                        "text": "Retain this context."
4216                    }]
4217                }],
4218                "prompt_cache_key": "thread-123",
4219                "service_tier": "priority"
4220            })
4221        );
4222    }
4223
4224    #[test]
4225    fn into_open_ai_response_maps_compact_at_tokens_to_context_management() {
4226        let request = LanguageModelRequest {
4227            messages: vec![LanguageModelRequestMessage {
4228                role: Role::User,
4229                content: vec![MessageContent::Text("Hello".into())],
4230                cache: false,
4231                reasoning_details: None,
4232            }],
4233            compact_at_tokens: Some(100_000),
4234            ..Default::default()
4235        };
4236
4237        let response = into_open_ai_response(
4238            request,
4239            "gpt-5.1",
4240            true,
4241            true,
4242            None,
4243            None,
4244            false,
4245            &OPEN_AI_PROVIDER_ID,
4246        )
4247        .unwrap();
4248
4249        assert_eq!(
4250            serde_json::to_value(&response).unwrap()["context_management"],
4251            json!([{ "type": "compaction", "compact_threshold": 100_000 }])
4252        );
4253    }
4254
4255    #[test]
4256    fn into_open_ai_response_omits_context_management_without_compact_at_tokens() {
4257        let request = LanguageModelRequest {
4258            messages: vec![LanguageModelRequestMessage {
4259                role: Role::User,
4260                content: vec![MessageContent::Text("Hello".into())],
4261                cache: false,
4262                reasoning_details: None,
4263            }],
4264            ..Default::default()
4265        };
4266
4267        let response = into_open_ai_response(
4268            request,
4269            "gpt-5.1",
4270            true,
4271            true,
4272            None,
4273            None,
4274            false,
4275            &OPEN_AI_PROVIDER_ID,
4276        )
4277        .unwrap();
4278
4279        assert!(
4280            serde_json::to_value(&response)
4281                .unwrap()
4282                .get("context_management")
4283                .is_none()
4284        );
4285    }
4286
4287    #[test]
4288    fn into_open_ai_response_replays_provider_compaction_block() {
4289        let state = provider_compaction_state_from_items(
4290            OPEN_AI_PROVIDER_ID,
4291            vec![json!({
4292                "type": "compaction",
4293                "id": "cmp_1",
4294                "encrypted_content": "encrypted-blob"
4295            })],
4296        )
4297        .unwrap();
4298        let request = LanguageModelRequest {
4299            messages: vec![LanguageModelRequestMessage {
4300                role: Role::Assistant,
4301                content: vec![
4302                    MessageContent::Compaction(CompactedContext::ProviderState(state)),
4303                    MessageContent::Text("Done.".into()),
4304                ],
4305                cache: false,
4306                reasoning_details: None,
4307            }],
4308            ..Default::default()
4309        };
4310
4311        let response = into_open_ai_response(
4312            request,
4313            "gpt-5.1",
4314            true,
4315            true,
4316            None,
4317            None,
4318            false,
4319            &OPEN_AI_PROVIDER_ID,
4320        )
4321        .unwrap();
4322
4323        assert_eq!(
4324            serde_json::to_value(&response).unwrap()["input"],
4325            json!([
4326                {
4327                    "type": "compaction",
4328                    "id": "cmp_1",
4329                    "encrypted_content": "encrypted-blob"
4330                },
4331                {
4332                    "type": "message",
4333                    "role": "assistant",
4334                    "content": [
4335                        { "type": "output_text", "text": "Done.", "annotations": [] }
4336                    ]
4337                }
4338            ])
4339        );
4340    }
4341
4342    #[test]
4343    fn into_open_ai_response_hoists_system_messages_into_instructions() {
4344        let request = LanguageModelRequest {
4345            messages: vec![
4346                LanguageModelRequestMessage {
4347                    role: Role::System,
4348                    content: vec![MessageContent::Text("You are a coding assistant.".into())],
4349                    cache: false,
4350                    reasoning_details: None,
4351                },
4352                LanguageModelRequestMessage {
4353                    role: Role::User,
4354                    content: vec![MessageContent::Text("Hello.".into())],
4355                    cache: false,
4356                    reasoning_details: None,
4357                },
4358                LanguageModelRequestMessage {
4359                    role: Role::System,
4360                    content: vec![MessageContent::Text("Prefer terse answers.".into())],
4361                    cache: false,
4362                    reasoning_details: None,
4363                },
4364            ],
4365            ..Default::default()
4366        };
4367
4368        let response = into_open_ai_response(
4369            request,
4370            "gpt-5.1",
4371            true,
4372            true,
4373            None,
4374            None,
4375            false,
4376            &OPEN_AI_PROVIDER_ID,
4377        )
4378        .unwrap();
4379
4380        let serialized = serde_json::to_value(&response).unwrap();
4381        assert_eq!(
4382            serialized["instructions"],
4383            json!("You are a coding assistant.\n\nPrefer terse answers.")
4384        );
4385        assert_eq!(
4386            serialized["input"],
4387            json!([
4388                {
4389                    "type": "message",
4390                    "role": "user",
4391                    "content": [
4392                        { "type": "input_text", "text": "Hello." }
4393                    ]
4394                }
4395            ])
4396        );
4397    }
4398
4399    /// Replaying provider compaction state discards all input items
4400    /// accumulated before it, but the system prompt must not be lost with
4401    /// them: it lives in the per-request `instructions` field, outside the
4402    /// compacted window, so the model keeps running on the current prompt
4403    /// rather than whatever was frozen into the window at compaction time.
4404    #[test]
4405    fn into_open_ai_response_preserves_system_prompt_across_compaction_replay() {
4406        let state = provider_compaction_state_from_items(
4407            OPEN_AI_PROVIDER_ID,
4408            vec![json!({
4409                "type": "compaction",
4410                "id": "cmp_1",
4411                "encrypted_content": "encrypted-blob"
4412            })],
4413        )
4414        .unwrap();
4415        let request = LanguageModelRequest {
4416            messages: vec![
4417                LanguageModelRequestMessage {
4418                    role: Role::System,
4419                    content: vec![MessageContent::Text("Current system prompt.".into())],
4420                    cache: false,
4421                    reasoning_details: None,
4422                },
4423                LanguageModelRequestMessage {
4424                    role: Role::User,
4425                    content: vec![MessageContent::Text("Old context.".into())],
4426                    cache: false,
4427                    reasoning_details: None,
4428                },
4429                LanguageModelRequestMessage {
4430                    role: Role::Assistant,
4431                    content: vec![MessageContent::Compaction(CompactedContext::ProviderState(
4432                        state,
4433                    ))],
4434                    cache: false,
4435                    reasoning_details: None,
4436                },
4437                LanguageModelRequestMessage {
4438                    role: Role::User,
4439                    content: vec![MessageContent::Text("Continue.".into())],
4440                    cache: false,
4441                    reasoning_details: None,
4442                },
4443            ],
4444            ..Default::default()
4445        };
4446
4447        let response = into_open_ai_response(
4448            request,
4449            "gpt-5.1",
4450            true,
4451            true,
4452            None,
4453            None,
4454            false,
4455            &OPEN_AI_PROVIDER_ID,
4456        )
4457        .unwrap();
4458
4459        let serialized = serde_json::to_value(&response).unwrap();
4460        assert_eq!(serialized["instructions"], json!("Current system prompt."));
4461        assert_eq!(
4462            serialized["input"],
4463            json!([
4464                {
4465                    "type": "compaction",
4466                    "id": "cmp_1",
4467                    "encrypted_content": "encrypted-blob"
4468                },
4469                {
4470                    "type": "message",
4471                    "role": "user",
4472                    "content": [
4473                        { "type": "input_text", "text": "Continue." }
4474                    ]
4475                }
4476            ])
4477        );
4478    }
4479
4480    #[test]
4481    fn into_open_ai_response_rejects_malformed_provider_compaction_state() {
4482        let request = LanguageModelRequest {
4483            messages: vec![LanguageModelRequestMessage {
4484                role: Role::Assistant,
4485                content: vec![MessageContent::Compaction(CompactedContext::ProviderState(
4486                    language_model_core::ProviderCompactionState::new(
4487                        language_model_core::OPEN_AI_PROVIDER_ID,
4488                        crate::responses::COMPACTION_STATE_FORMAT,
4489                        "not valid JSON",
4490                    ),
4491                ))],
4492                cache: false,
4493                reasoning_details: None,
4494            }],
4495            ..Default::default()
4496        };
4497
4498        let error = into_open_ai_response(
4499            request,
4500            "gpt-5.1",
4501            true,
4502            true,
4503            None,
4504            None,
4505            false,
4506            &OPEN_AI_PROVIDER_ID,
4507        )
4508        .unwrap_err();
4509
4510        assert!(
4511            error
4512                .to_string()
4513                .contains("malformed OpenAI compaction state payload")
4514        );
4515    }
4516
4517    #[test]
4518    fn into_open_ai_response_ignores_compaction_state_owned_by_another_backend() {
4519        // Several backends share this request conversion (OpenAI itself,
4520        // OpenAI-compatible endpoints, Codex, Mantle), but an encrypted
4521        // compaction item is only decryptable by the backend that produced
4522        // it. Replaying OpenAI-owned state through a different backend would
4523        // discard the entire transcript in exchange for an opaque blob that
4524        // backend cannot read, so the state must be ignored and the full
4525        // transcript replayed instead.
4526        let state = provider_compaction_state_from_items(
4527            OPEN_AI_PROVIDER_ID,
4528            vec![json!({
4529                "type": "compaction",
4530                "id": "cmp_1",
4531                "encrypted_content": "encrypted-blob"
4532            })],
4533        )
4534        .unwrap();
4535        let request = LanguageModelRequest {
4536            messages: vec![
4537                LanguageModelRequestMessage {
4538                    role: Role::User,
4539                    content: vec![MessageContent::Text("Set up the project.".into())],
4540                    cache: false,
4541                    reasoning_details: None,
4542                },
4543                LanguageModelRequestMessage {
4544                    role: Role::Assistant,
4545                    content: vec![
4546                        MessageContent::Compaction(CompactedContext::ProviderState(state)),
4547                        MessageContent::Text("Done.".into()),
4548                    ],
4549                    cache: false,
4550                    reasoning_details: None,
4551                },
4552                LanguageModelRequestMessage {
4553                    role: Role::User,
4554                    content: vec![MessageContent::Text("Continue.".into())],
4555                    cache: false,
4556                    reasoning_details: None,
4557                },
4558            ],
4559            ..Default::default()
4560        };
4561
4562        let response = into_open_ai_response(
4563            request,
4564            "compatible-model",
4565            true,
4566            true,
4567            None,
4568            None,
4569            false,
4570            &LanguageModelProviderId::new("my-compatible-endpoint"),
4571        )
4572        .unwrap();
4573
4574        assert_eq!(
4575            serde_json::to_value(&response).unwrap()["input"],
4576            json!([
4577                {
4578                    "type": "message",
4579                    "role": "user",
4580                    "content": [{ "type": "input_text", "text": "Set up the project." }]
4581                },
4582                {
4583                    "type": "message",
4584                    "role": "assistant",
4585                    "content": [
4586                        { "type": "output_text", "text": "Done.", "annotations": [] }
4587                    ]
4588                },
4589                {
4590                    "type": "message",
4591                    "role": "user",
4592                    "content": [{ "type": "input_text", "text": "Continue." }]
4593                }
4594            ])
4595        );
4596    }
4597
4598    #[test]
4599    fn responses_stream_stamps_compaction_state_with_owning_backend() {
4600        let owner = LanguageModelProviderId::new("my-compatible-endpoint");
4601        let mut mapper = OpenAiResponseEventMapper::new(owner.clone());
4602        let item: ResponseOutputItem = serde_json::from_value(json!({
4603            "type": "compaction",
4604            "id": "cmp_1",
4605            "encrypted_content": "encrypted-blob"
4606        }))
4607        .unwrap();
4608
4609        let mut events = mapper.map_event(ResponsesStreamEvent::OutputItemDone {
4610            output_index: 0,
4611            sequence_number: None,
4612            item,
4613        });
4614
4615        let Some(Ok(LanguageModelCompletionEvent::Compaction(CompactionUpdate::Finished(
4616            CompactedContext::ProviderState(state),
4617        )))) = events.pop()
4618        else {
4619            panic!("expected finished provider compaction state");
4620        };
4621        assert_eq!(state.provider_id(), &owner);
4622        assert!(
4623            crate::responses::provider_compaction_items(&state, &owner)
4624                .unwrap()
4625                .is_some()
4626        );
4627        // OpenAI proper must not attempt to replay a window produced by a
4628        // different backend's infrastructure.
4629        assert_eq!(
4630            crate::responses::provider_compaction_items(&state, &OPEN_AI_PROVIDER_ID).unwrap(),
4631            None
4632        );
4633    }
4634
4635    #[test]
4636    fn responses_stream_rejects_completion_with_unfinished_compaction() {
4637        let mut mapper = OpenAiResponseEventMapper::new(OPEN_AI_PROVIDER_ID);
4638        let item: ResponseOutputItem = serde_json::from_value(json!({
4639            "type": "compaction",
4640            "id": "cmp_1",
4641            "encrypted_content": "encrypted-blob"
4642        }))
4643        .unwrap();
4644
4645        let started = mapper.map_event(ResponsesStreamEvent::OutputItemAdded {
4646            output_index: 0,
4647            sequence_number: None,
4648            item,
4649        });
4650        assert!(matches!(
4651            started.as_slice(),
4652            [Ok(LanguageModelCompletionEvent::Compaction(
4653                CompactionUpdate::Started
4654            ))]
4655        ));
4656
4657        let mut completed = mapper.map_event(ResponsesStreamEvent::Completed {
4658            response: ResponseSummary::default(),
4659        });
4660        let error = completed.pop().unwrap().unwrap_err();
4661
4662        assert!(error.to_string().contains("unfinished compaction"));
4663    }
4664
4665    #[test]
4666    fn responses_stream_maps_compaction_output_item() {
4667        let item: ResponseOutputItem = serde_json::from_value(json!({
4668            "type": "compaction",
4669            "id": "cmp_1",
4670            "encrypted_content": "encrypted-blob"
4671        }))
4672        .unwrap();
4673        let events = vec![
4674            ResponsesStreamEvent::OutputItemAdded {
4675                output_index: 0,
4676                sequence_number: None,
4677                item: item.clone(),
4678            },
4679            ResponsesStreamEvent::OutputItemDone {
4680                output_index: 0,
4681                sequence_number: None,
4682                item,
4683            },
4684        ];
4685
4686        let mapped = map_response_events(events);
4687
4688        assert_eq!(
4689            mapped,
4690            vec![
4691                LanguageModelCompletionEvent::Compaction(CompactionUpdate::Started),
4692                LanguageModelCompletionEvent::Compaction(CompactionUpdate::Finished(
4693                    CompactedContext::ProviderState(
4694                        provider_compaction_state_from_items(
4695                            OPEN_AI_PROVIDER_ID,
4696                            vec![json!({
4697                                "type": "compaction",
4698                                "id": "cmp_1",
4699                                "encrypted_content": "encrypted-blob"
4700                            })],
4701                        )
4702                        .unwrap()
4703                    )
4704                )),
4705            ]
4706        );
4707    }
4708}
4709
Served at tenant.openagents/omega Member data and write actions are omitted.