completion.rs

   1use anyhow::{Result, anyhow};
   2use collections::HashMap;
   3use futures::{Stream, StreamExt};
   4use language_model_core::{
   5    LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
   6    LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
   7    LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
   8    Role, StopReason, TokenUsage,
   9    util::{fix_streamed_json, parse_tool_arguments},
  10};
  11use std::pin::Pin;
  12use std::sync::Arc;
  13
  14use crate::responses::{
  15    Request as ResponseRequest, ResponseFunctionCallItem, ResponseFunctionCallOutputContent,
  16    ResponseFunctionCallOutputItem, ResponseInputContent, ResponseInputItem, ResponseMessageItem,
  17    ResponseOutputItem, ResponseSummary as ResponsesSummary, ResponseUsage as ResponsesUsage,
  18    StreamEvent as ResponsesStreamEvent,
  19};
  20use crate::{
  21    FunctionContent, FunctionDefinition, ImageUrl, MessagePart, ReasoningEffort,
  22    ResponseStreamEvent, ToolCall, ToolCallContent,
  23};
  24
  25pub fn into_open_ai(
  26    request: LanguageModelRequest,
  27    model_id: &str,
  28    supports_parallel_tool_calls: bool,
  29    supports_prompt_cache_key: bool,
  30    max_output_tokens: Option<u64>,
  31    reasoning_effort: Option<ReasoningEffort>,
  32    interleaved_reasoning: bool,
  33) -> crate::Request {
  34    let stream = !model_id.starts_with("o1-");
  35
  36    let mut messages = Vec::new();
  37    let mut current_reasoning: Option<String> = None;
  38    for message in request.messages {
  39        for content in message.content {
  40            match content {
  41                MessageContent::Thinking { text, .. } if interleaved_reasoning => {
  42                    current_reasoning.get_or_insert_default().push_str(&text);
  43                }
  44                MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
  45                    let should_add = if message.role == Role::User {
  46                        // Including whitespace-only user messages can cause error with OpenAI compatible APIs
  47                        // See https://github.com/zed-industries/zed/issues/40097
  48                        !text.trim().is_empty()
  49                    } else {
  50                        !text.is_empty()
  51                    };
  52                    if should_add {
  53                        add_message_content_part(
  54                            MessagePart::Text { text },
  55                            message.role,
  56                            &mut messages,
  57                        );
  58                        if let Some(reasoning) = current_reasoning.take() {
  59                            if let Some(crate::RequestMessage::Assistant {
  60                                reasoning_content,
  61                                ..
  62                            }) = messages.last_mut()
  63                            {
  64                                *reasoning_content = Some(reasoning);
  65                            }
  66                        }
  67                    }
  68                }
  69                MessageContent::RedactedThinking(_) => {}
  70                MessageContent::Image(image) => {
  71                    add_message_content_part(
  72                        MessagePart::Image {
  73                            image_url: ImageUrl {
  74                                url: image.to_base64_url(),
  75                                detail: None,
  76                            },
  77                        },
  78                        message.role,
  79                        &mut messages,
  80                    );
  81                }
  82                MessageContent::ToolUse(tool_use) => {
  83                    let tool_call = ToolCall {
  84                        id: tool_use.id.to_string(),
  85                        content: ToolCallContent::Function {
  86                            function: FunctionContent {
  87                                name: tool_use.name.to_string(),
  88                                arguments: serde_json::to_string(&tool_use.input)
  89                                    .unwrap_or_default(),
  90                            },
  91                        },
  92                    };
  93
  94                    if let Some(crate::RequestMessage::Assistant { tool_calls, .. }) =
  95                        messages.last_mut()
  96                    {
  97                        tool_calls.push(tool_call);
  98                    } else {
  99                        messages.push(crate::RequestMessage::Assistant {
 100                            content: None,
 101                            tool_calls: vec![tool_call],
 102                            reasoning_content: current_reasoning.take(),
 103                        });
 104                    }
 105                }
 106                MessageContent::ToolResult(tool_result) => {
 107                    let content = match &tool_result.content {
 108                        LanguageModelToolResultContent::Text(text) => {
 109                            vec![MessagePart::Text {
 110                                text: text.to_string(),
 111                            }]
 112                        }
 113                        LanguageModelToolResultContent::Image(image) => {
 114                            vec![MessagePart::Image {
 115                                image_url: ImageUrl {
 116                                    url: image.to_base64_url(),
 117                                    detail: None,
 118                                },
 119                            }]
 120                        }
 121                    };
 122
 123                    messages.push(crate::RequestMessage::Tool {
 124                        content: content.into(),
 125                        tool_call_id: tool_result.tool_use_id.to_string(),
 126                    });
 127                }
 128            }
 129        }
 130    }
 131
 132    crate::Request {
 133        model: model_id.into(),
 134        messages,
 135        stream,
 136        stream_options: if stream {
 137            Some(crate::StreamOptions::default())
 138        } else {
 139            None
 140        },
 141        stop: request.stop,
 142        temperature: request.temperature.or(Some(1.0)),
 143        max_completion_tokens: max_output_tokens,
 144        parallel_tool_calls: if supports_parallel_tool_calls && !request.tools.is_empty() {
 145            Some(supports_parallel_tool_calls)
 146        } else {
 147            None
 148        },
 149        prompt_cache_key: if supports_prompt_cache_key {
 150            request.thread_id
 151        } else {
 152            None
 153        },
 154        tools: request
 155            .tools
 156            .into_iter()
 157            .map(|tool| crate::ToolDefinition::Function {
 158                function: FunctionDefinition {
 159                    name: tool.name,
 160                    description: Some(tool.description),
 161                    parameters: Some(tool.input_schema),
 162                },
 163            })
 164            .collect(),
 165        tool_choice: request.tool_choice.map(|choice| match choice {
 166            LanguageModelToolChoice::Auto => crate::ToolChoice::Auto,
 167            LanguageModelToolChoice::Any => crate::ToolChoice::Required,
 168            LanguageModelToolChoice::None => crate::ToolChoice::None,
 169        }),
 170        reasoning_effort,
 171    }
 172}
 173
 174pub fn into_open_ai_response(
 175    request: LanguageModelRequest,
 176    model_id: &str,
 177    supports_parallel_tool_calls: bool,
 178    supports_prompt_cache_key: bool,
 179    max_output_tokens: Option<u64>,
 180    reasoning_effort: Option<ReasoningEffort>,
 181) -> ResponseRequest {
 182    let stream = !model_id.starts_with("o1-");
 183
 184    let LanguageModelRequest {
 185        thread_id,
 186        prompt_id: _,
 187        intent: _,
 188        messages,
 189        tools,
 190        tool_choice,
 191        stop: _,
 192        temperature,
 193        thinking_allowed: _,
 194        thinking_effort: _,
 195        speed: _,
 196    } = request;
 197
 198    let mut input_items = Vec::new();
 199    for (index, message) in messages.into_iter().enumerate() {
 200        append_message_to_response_items(message, index, &mut input_items);
 201    }
 202
 203    let tools: Vec<_> = tools
 204        .into_iter()
 205        .map(|tool| crate::responses::ToolDefinition::Function {
 206            name: tool.name,
 207            description: Some(tool.description),
 208            parameters: Some(tool.input_schema),
 209            strict: None,
 210        })
 211        .collect();
 212
 213    ResponseRequest {
 214        model: model_id.into(),
 215        input: input_items,
 216        stream,
 217        temperature,
 218        top_p: None,
 219        max_output_tokens,
 220        parallel_tool_calls: if tools.is_empty() {
 221            None
 222        } else {
 223            Some(supports_parallel_tool_calls)
 224        },
 225        tool_choice: tool_choice.map(|choice| match choice {
 226            LanguageModelToolChoice::Auto => crate::ToolChoice::Auto,
 227            LanguageModelToolChoice::Any => crate::ToolChoice::Required,
 228            LanguageModelToolChoice::None => crate::ToolChoice::None,
 229        }),
 230        tools,
 231        prompt_cache_key: if supports_prompt_cache_key {
 232            thread_id
 233        } else {
 234            None
 235        },
 236        reasoning: reasoning_effort.map(|effort| crate::responses::ReasoningConfig {
 237            effort,
 238            summary: Some(crate::responses::ReasoningSummaryMode::Auto),
 239        }),
 240    }
 241}
 242
 243fn append_message_to_response_items(
 244    message: LanguageModelRequestMessage,
 245    index: usize,
 246    input_items: &mut Vec<ResponseInputItem>,
 247) {
 248    let mut content_parts: Vec<ResponseInputContent> = Vec::new();
 249
 250    for content in message.content {
 251        match content {
 252            MessageContent::Text(text) => {
 253                push_response_text_part(&message.role, text, &mut content_parts);
 254            }
 255            MessageContent::Thinking { text, .. } => {
 256                push_response_text_part(&message.role, text, &mut content_parts);
 257            }
 258            MessageContent::RedactedThinking(_) => {}
 259            MessageContent::Image(image) => {
 260                push_response_image_part(&message.role, image, &mut content_parts);
 261            }
 262            MessageContent::ToolUse(tool_use) => {
 263                flush_response_parts(&message.role, index, &mut content_parts, input_items);
 264                let call_id = tool_use.id.to_string();
 265                input_items.push(ResponseInputItem::FunctionCall(ResponseFunctionCallItem {
 266                    call_id,
 267                    name: tool_use.name.to_string(),
 268                    arguments: tool_use.raw_input,
 269                }));
 270            }
 271            MessageContent::ToolResult(tool_result) => {
 272                flush_response_parts(&message.role, index, &mut content_parts, input_items);
 273                input_items.push(ResponseInputItem::FunctionCallOutput(
 274                    ResponseFunctionCallOutputItem {
 275                        call_id: tool_result.tool_use_id.to_string(),
 276                        output: match tool_result.content {
 277                            LanguageModelToolResultContent::Text(text) => {
 278                                ResponseFunctionCallOutputContent::Text(text.to_string())
 279                            }
 280                            LanguageModelToolResultContent::Image(image) => {
 281                                ResponseFunctionCallOutputContent::List(vec![
 282                                    ResponseInputContent::Image {
 283                                        image_url: image.to_base64_url(),
 284                                    },
 285                                ])
 286                            }
 287                        },
 288                    },
 289                ));
 290            }
 291        }
 292    }
 293
 294    flush_response_parts(&message.role, index, &mut content_parts, input_items);
 295}
 296
 297fn push_response_text_part(
 298    role: &Role,
 299    text: impl Into<String>,
 300    parts: &mut Vec<ResponseInputContent>,
 301) {
 302    let text = text.into();
 303    if text.trim().is_empty() {
 304        return;
 305    }
 306
 307    match role {
 308        Role::Assistant => parts.push(ResponseInputContent::OutputText {
 309            text,
 310            annotations: Vec::new(),
 311        }),
 312        _ => parts.push(ResponseInputContent::Text { text }),
 313    }
 314}
 315
 316fn push_response_image_part(
 317    role: &Role,
 318    image: LanguageModelImage,
 319    parts: &mut Vec<ResponseInputContent>,
 320) {
 321    match role {
 322        Role::Assistant => parts.push(ResponseInputContent::OutputText {
 323            text: "[image omitted]".to_string(),
 324            annotations: Vec::new(),
 325        }),
 326        _ => parts.push(ResponseInputContent::Image {
 327            image_url: image.to_base64_url(),
 328        }),
 329    }
 330}
 331
 332fn flush_response_parts(
 333    role: &Role,
 334    _index: usize,
 335    parts: &mut Vec<ResponseInputContent>,
 336    input_items: &mut Vec<ResponseInputItem>,
 337) {
 338    if parts.is_empty() {
 339        return;
 340    }
 341
 342    let item = ResponseInputItem::Message(ResponseMessageItem {
 343        role: match role {
 344            Role::User => crate::Role::User,
 345            Role::Assistant => crate::Role::Assistant,
 346            Role::System => crate::Role::System,
 347        },
 348        content: parts.clone(),
 349    });
 350
 351    input_items.push(item);
 352    parts.clear();
 353}
 354
 355fn add_message_content_part(
 356    new_part: MessagePart,
 357    role: Role,
 358    messages: &mut Vec<crate::RequestMessage>,
 359) {
 360    match (role, messages.last_mut()) {
 361        (Role::User, Some(crate::RequestMessage::User { content }))
 362        | (
 363            Role::Assistant,
 364            Some(crate::RequestMessage::Assistant {
 365                content: Some(content),
 366                ..
 367            }),
 368        )
 369        | (Role::System, Some(crate::RequestMessage::System { content, .. })) => {
 370            content.push_part(new_part);
 371        }
 372        _ => {
 373            messages.push(match role {
 374                Role::User => crate::RequestMessage::User {
 375                    content: crate::MessageContent::from(vec![new_part]),
 376                },
 377                Role::Assistant => crate::RequestMessage::Assistant {
 378                    content: Some(crate::MessageContent::from(vec![new_part])),
 379                    tool_calls: Vec::new(),
 380                    reasoning_content: None,
 381                },
 382                Role::System => crate::RequestMessage::System {
 383                    content: crate::MessageContent::from(vec![new_part]),
 384                },
 385            });
 386        }
 387    }
 388}
 389
 390pub struct OpenAiEventMapper {
 391    tool_calls_by_index: HashMap<usize, RawToolCall>,
 392}
 393
 394impl OpenAiEventMapper {
 395    pub fn new() -> Self {
 396        Self {
 397            tool_calls_by_index: HashMap::default(),
 398        }
 399    }
 400
 401    pub fn map_stream(
 402        mut self,
 403        events: Pin<Box<dyn Send + Stream<Item = Result<ResponseStreamEvent>>>>,
 404    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
 405    {
 406        events.flat_map(move |event| {
 407            futures::stream::iter(match event {
 408                Ok(event) => self.map_event(event),
 409                Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
 410            })
 411        })
 412    }
 413
 414    pub fn map_event(
 415        &mut self,
 416        event: ResponseStreamEvent,
 417    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 418        let mut events = Vec::new();
 419        if let Some(usage) = event.usage {
 420            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
 421                input_tokens: usage.prompt_tokens,
 422                output_tokens: usage.completion_tokens,
 423                cache_creation_input_tokens: 0,
 424                cache_read_input_tokens: 0,
 425            })));
 426        }
 427
 428        let Some(choice) = event.choices.first() else {
 429            return events;
 430        };
 431
 432        if let Some(delta) = choice.delta.as_ref() {
 433            if let Some(reasoning_content) = delta.reasoning_content.clone() {
 434                if !reasoning_content.is_empty() {
 435                    events.push(Ok(LanguageModelCompletionEvent::Thinking {
 436                        text: reasoning_content,
 437                        signature: None,
 438                    }));
 439                }
 440            }
 441            if let Some(content) = delta.content.clone() {
 442                if !content.is_empty() {
 443                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
 444                }
 445            }
 446
 447            if let Some(tool_calls) = delta.tool_calls.as_ref() {
 448                for tool_call in tool_calls {
 449                    let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
 450
 451                    if let Some(tool_id) = tool_call.id.clone() {
 452                        entry.id = tool_id;
 453                    }
 454
 455                    if let Some(function) = tool_call.function.as_ref() {
 456                        if let Some(name) = function.name.clone() {
 457                            entry.name = name;
 458                        }
 459
 460                        if let Some(arguments) = function.arguments.clone() {
 461                            entry.arguments.push_str(&arguments);
 462                        }
 463                    }
 464
 465                    if !entry.id.is_empty() && !entry.name.is_empty() {
 466                        if let Ok(input) = serde_json::from_str::<serde_json::Value>(
 467                            &fix_streamed_json(&entry.arguments),
 468                        ) {
 469                            events.push(Ok(LanguageModelCompletionEvent::ToolUse(
 470                                LanguageModelToolUse {
 471                                    id: entry.id.clone().into(),
 472                                    name: entry.name.as_str().into(),
 473                                    is_input_complete: false,
 474                                    input,
 475                                    raw_input: entry.arguments.clone(),
 476                                    thought_signature: None,
 477                                },
 478                            )));
 479                        }
 480                    }
 481                }
 482            }
 483        }
 484
 485        match choice.finish_reason.as_deref() {
 486            Some("stop") => {
 487                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
 488            }
 489            Some("tool_calls") => {
 490                events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
 491                    match parse_tool_arguments(&tool_call.arguments) {
 492                        Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
 493                            LanguageModelToolUse {
 494                                id: tool_call.id.clone().into(),
 495                                name: tool_call.name.as_str().into(),
 496                                is_input_complete: true,
 497                                input,
 498                                raw_input: tool_call.arguments.clone(),
 499                                thought_signature: None,
 500                            },
 501                        )),
 502                        Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
 503                            id: tool_call.id.into(),
 504                            tool_name: tool_call.name.into(),
 505                            raw_input: tool_call.arguments.clone().into(),
 506                            json_parse_error: error.to_string(),
 507                        }),
 508                    }
 509                }));
 510
 511                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
 512            }
 513            Some(stop_reason) => {
 514                log::error!("Unexpected OpenAI stop_reason: {stop_reason:?}",);
 515                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
 516            }
 517            None => {}
 518        }
 519
 520        events
 521    }
 522}
 523
 524#[derive(Default)]
 525struct RawToolCall {
 526    id: String,
 527    name: String,
 528    arguments: String,
 529}
 530
 531pub struct OpenAiResponseEventMapper {
 532    function_calls_by_item: HashMap<String, PendingResponseFunctionCall>,
 533    pending_stop_reason: Option<StopReason>,
 534}
 535
 536#[derive(Default)]
 537struct PendingResponseFunctionCall {
 538    call_id: String,
 539    name: Arc<str>,
 540    arguments: String,
 541}
 542
 543impl OpenAiResponseEventMapper {
 544    pub fn new() -> Self {
 545        Self {
 546            function_calls_by_item: HashMap::default(),
 547            pending_stop_reason: None,
 548        }
 549    }
 550
 551    pub fn map_stream(
 552        mut self,
 553        events: Pin<Box<dyn Send + Stream<Item = Result<ResponsesStreamEvent>>>>,
 554    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
 555    {
 556        events.flat_map(move |event| {
 557            futures::stream::iter(match event {
 558                Ok(event) => self.map_event(event),
 559                Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
 560            })
 561        })
 562    }
 563
 564    pub fn map_event(
 565        &mut self,
 566        event: ResponsesStreamEvent,
 567    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 568        match event {
 569            ResponsesStreamEvent::OutputItemAdded { item, .. } => {
 570                let mut events = Vec::new();
 571
 572                match &item {
 573                    ResponseOutputItem::Message(message) => {
 574                        if let Some(id) = &message.id {
 575                            events.push(Ok(LanguageModelCompletionEvent::StartMessage {
 576                                message_id: id.clone(),
 577                            }));
 578                        }
 579                    }
 580                    ResponseOutputItem::FunctionCall(function_call) => {
 581                        if let Some(item_id) = function_call.id.clone() {
 582                            let call_id = function_call
 583                                .call_id
 584                                .clone()
 585                                .or_else(|| function_call.id.clone())
 586                                .unwrap_or_else(|| item_id.clone());
 587                            let entry = PendingResponseFunctionCall {
 588                                call_id,
 589                                name: Arc::<str>::from(
 590                                    function_call.name.clone().unwrap_or_default(),
 591                                ),
 592                                arguments: function_call.arguments.clone(),
 593                            };
 594                            self.function_calls_by_item.insert(item_id, entry);
 595                        }
 596                    }
 597                    ResponseOutputItem::Reasoning(_) | ResponseOutputItem::Unknown => {}
 598                }
 599                events
 600            }
 601            ResponsesStreamEvent::ReasoningSummaryTextDelta { delta, .. } => {
 602                if delta.is_empty() {
 603                    Vec::new()
 604                } else {
 605                    vec![Ok(LanguageModelCompletionEvent::Thinking {
 606                        text: delta,
 607                        signature: None,
 608                    })]
 609                }
 610            }
 611            ResponsesStreamEvent::OutputTextDelta { delta, .. } => {
 612                if delta.is_empty() {
 613                    Vec::new()
 614                } else {
 615                    vec![Ok(LanguageModelCompletionEvent::Text(delta))]
 616                }
 617            }
 618            ResponsesStreamEvent::FunctionCallArgumentsDelta { item_id, delta, .. } => {
 619                if let Some(entry) = self.function_calls_by_item.get_mut(&item_id) {
 620                    entry.arguments.push_str(&delta);
 621                    if let Ok(input) = serde_json::from_str::<serde_json::Value>(
 622                        &fix_streamed_json(&entry.arguments),
 623                    ) {
 624                        return vec![Ok(LanguageModelCompletionEvent::ToolUse(
 625                            LanguageModelToolUse {
 626                                id: LanguageModelToolUseId::from(entry.call_id.clone()),
 627                                name: entry.name.clone(),
 628                                is_input_complete: false,
 629                                input,
 630                                raw_input: entry.arguments.clone(),
 631                                thought_signature: None,
 632                            },
 633                        ))];
 634                    }
 635                }
 636                Vec::new()
 637            }
 638            ResponsesStreamEvent::FunctionCallArgumentsDone {
 639                item_id, arguments, ..
 640            } => {
 641                if let Some(mut entry) = self.function_calls_by_item.remove(&item_id) {
 642                    if !arguments.is_empty() {
 643                        entry.arguments = arguments;
 644                    }
 645                    let raw_input = entry.arguments.clone();
 646                    self.pending_stop_reason = Some(StopReason::ToolUse);
 647                    match parse_tool_arguments(&entry.arguments) {
 648                        Ok(input) => {
 649                            vec![Ok(LanguageModelCompletionEvent::ToolUse(
 650                                LanguageModelToolUse {
 651                                    id: LanguageModelToolUseId::from(entry.call_id.clone()),
 652                                    name: entry.name.clone(),
 653                                    is_input_complete: true,
 654                                    input,
 655                                    raw_input,
 656                                    thought_signature: None,
 657                                },
 658                            ))]
 659                        }
 660                        Err(error) => {
 661                            vec![Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
 662                                id: LanguageModelToolUseId::from(entry.call_id.clone()),
 663                                tool_name: entry.name.clone(),
 664                                raw_input: Arc::<str>::from(raw_input),
 665                                json_parse_error: error.to_string(),
 666                            })]
 667                        }
 668                    }
 669                } else {
 670                    Vec::new()
 671                }
 672            }
 673            ResponsesStreamEvent::Completed { response } => {
 674                self.handle_completion(response, StopReason::EndTurn)
 675            }
 676            ResponsesStreamEvent::Incomplete { response } => {
 677                let reason = response
 678                    .status_details
 679                    .as_ref()
 680                    .and_then(|details| details.reason.as_deref());
 681                let stop_reason = match reason {
 682                    Some("max_output_tokens") => StopReason::MaxTokens,
 683                    Some("content_filter") => {
 684                        self.pending_stop_reason = Some(StopReason::Refusal);
 685                        StopReason::Refusal
 686                    }
 687                    _ => self
 688                        .pending_stop_reason
 689                        .take()
 690                        .unwrap_or(StopReason::EndTurn),
 691                };
 692
 693                let mut events = Vec::new();
 694                if self.pending_stop_reason.is_none() {
 695                    events.extend(self.emit_tool_calls_from_output(&response.output));
 696                }
 697                if let Some(usage) = response.usage.as_ref() {
 698                    events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
 699                        token_usage_from_response_usage(usage),
 700                    )));
 701                }
 702                events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
 703                events
 704            }
 705            ResponsesStreamEvent::Failed { response } => {
 706                let message = response
 707                    .status_details
 708                    .and_then(|details| details.error)
 709                    .map(|error| error.to_string())
 710                    .unwrap_or_else(|| "response failed".to_string());
 711                vec![Err(LanguageModelCompletionError::Other(anyhow!(message)))]
 712            }
 713            ResponsesStreamEvent::Error { error }
 714            | ResponsesStreamEvent::GenericError { error } => {
 715                vec![Err(LanguageModelCompletionError::Other(anyhow!(
 716                    error.message
 717                )))]
 718            }
 719            ResponsesStreamEvent::ReasoningSummaryPartAdded { summary_index, .. } => {
 720                if summary_index > 0 {
 721                    vec![Ok(LanguageModelCompletionEvent::Thinking {
 722                        text: "\n\n".to_string(),
 723                        signature: None,
 724                    })]
 725                } else {
 726                    Vec::new()
 727                }
 728            }
 729            ResponsesStreamEvent::OutputTextDone { .. }
 730            | ResponsesStreamEvent::OutputItemDone { .. }
 731            | ResponsesStreamEvent::ContentPartAdded { .. }
 732            | ResponsesStreamEvent::ContentPartDone { .. }
 733            | ResponsesStreamEvent::ReasoningSummaryTextDone { .. }
 734            | ResponsesStreamEvent::ReasoningSummaryPartDone { .. }
 735            | ResponsesStreamEvent::Created { .. }
 736            | ResponsesStreamEvent::InProgress { .. }
 737            | ResponsesStreamEvent::Unknown => Vec::new(),
 738        }
 739    }
 740
 741    fn handle_completion(
 742        &mut self,
 743        response: ResponsesSummary,
 744        default_reason: StopReason,
 745    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 746        let mut events = Vec::new();
 747
 748        if self.pending_stop_reason.is_none() {
 749            events.extend(self.emit_tool_calls_from_output(&response.output));
 750        }
 751
 752        if let Some(usage) = response.usage.as_ref() {
 753            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
 754                token_usage_from_response_usage(usage),
 755            )));
 756        }
 757
 758        let stop_reason = self.pending_stop_reason.take().unwrap_or(default_reason);
 759        events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
 760        events
 761    }
 762
 763    fn emit_tool_calls_from_output(
 764        &mut self,
 765        output: &[ResponseOutputItem],
 766    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 767        let mut events = Vec::new();
 768        for item in output {
 769            if let ResponseOutputItem::FunctionCall(function_call) = item {
 770                let Some(call_id) = function_call
 771                    .call_id
 772                    .clone()
 773                    .or_else(|| function_call.id.clone())
 774                else {
 775                    log::error!(
 776                        "Function call item missing both call_id and id: {:?}",
 777                        function_call
 778                    );
 779                    continue;
 780                };
 781                let name: Arc<str> = Arc::from(function_call.name.clone().unwrap_or_default());
 782                let arguments = &function_call.arguments;
 783                self.pending_stop_reason = Some(StopReason::ToolUse);
 784                match parse_tool_arguments(arguments) {
 785                    Ok(input) => {
 786                        events.push(Ok(LanguageModelCompletionEvent::ToolUse(
 787                            LanguageModelToolUse {
 788                                id: LanguageModelToolUseId::from(call_id.clone()),
 789                                name: name.clone(),
 790                                is_input_complete: true,
 791                                input,
 792                                raw_input: arguments.clone(),
 793                                thought_signature: None,
 794                            },
 795                        )));
 796                    }
 797                    Err(error) => {
 798                        events.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
 799                            id: LanguageModelToolUseId::from(call_id.clone()),
 800                            tool_name: name.clone(),
 801                            raw_input: Arc::<str>::from(arguments.clone()),
 802                            json_parse_error: error.to_string(),
 803                        }));
 804                    }
 805                }
 806            }
 807        }
 808        events
 809    }
 810}
 811
 812fn token_usage_from_response_usage(usage: &ResponsesUsage) -> TokenUsage {
 813    TokenUsage {
 814        input_tokens: usage.input_tokens.unwrap_or_default(),
 815        output_tokens: usage.output_tokens.unwrap_or_default(),
 816        cache_creation_input_tokens: 0,
 817        cache_read_input_tokens: 0,
 818    }
 819}
 820
 821#[cfg(test)]
 822mod tests {
 823    use crate::responses::{
 824        ReasoningSummaryPart, ResponseFunctionToolCall, ResponseOutputItem, ResponseOutputMessage,
 825        ResponseReasoningItem, ResponseStatusDetails, ResponseSummary, ResponseUsage,
 826        StreamEvent as ResponsesStreamEvent,
 827    };
 828    use futures::{StreamExt, executor::block_on};
 829    use language_model_core::{
 830        LanguageModelImage, LanguageModelRequestMessage, LanguageModelRequestTool,
 831        LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
 832        LanguageModelToolUseId, SharedString,
 833    };
 834    use pretty_assertions::assert_eq;
 835    use serde_json::json;
 836
 837    use super::*;
 838
 839    fn map_response_events(events: Vec<ResponsesStreamEvent>) -> Vec<LanguageModelCompletionEvent> {
 840        block_on(async {
 841            OpenAiResponseEventMapper::new()
 842                .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
 843                .collect::<Vec<_>>()
 844                .await
 845                .into_iter()
 846                .map(Result::unwrap)
 847                .collect()
 848        })
 849    }
 850
 851    fn response_item_message(id: &str) -> ResponseOutputItem {
 852        ResponseOutputItem::Message(ResponseOutputMessage {
 853            id: Some(id.to_string()),
 854            role: Some("assistant".to_string()),
 855            status: Some("in_progress".to_string()),
 856            content: vec![],
 857        })
 858    }
 859
 860    fn response_item_function_call(id: &str, args: Option<&str>) -> ResponseOutputItem {
 861        ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
 862            id: Some(id.to_string()),
 863            status: Some("in_progress".to_string()),
 864            name: Some("get_weather".to_string()),
 865            call_id: Some("call_123".to_string()),
 866            arguments: args.map(|s| s.to_string()).unwrap_or_default(),
 867        })
 868    }
 869
 870    #[test]
 871    fn responses_stream_maps_text_and_usage() {
 872        let events = vec![
 873            ResponsesStreamEvent::OutputItemAdded {
 874                output_index: 0,
 875                sequence_number: None,
 876                item: response_item_message("msg_123"),
 877            },
 878            ResponsesStreamEvent::OutputTextDelta {
 879                item_id: "msg_123".into(),
 880                output_index: 0,
 881                content_index: Some(0),
 882                delta: "Hello".into(),
 883            },
 884            ResponsesStreamEvent::Completed {
 885                response: ResponseSummary {
 886                    usage: Some(ResponseUsage {
 887                        input_tokens: Some(5),
 888                        output_tokens: Some(3),
 889                        total_tokens: Some(8),
 890                    }),
 891                    ..Default::default()
 892                },
 893            },
 894        ];
 895
 896        let mapped = map_response_events(events);
 897        assert!(matches!(
 898            mapped[0],
 899            LanguageModelCompletionEvent::StartMessage { ref message_id } if message_id == "msg_123"
 900        ));
 901        assert!(matches!(
 902            mapped[1],
 903            LanguageModelCompletionEvent::Text(ref text) if text == "Hello"
 904        ));
 905        assert!(matches!(
 906            mapped[2],
 907            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
 908                input_tokens: 5,
 909                output_tokens: 3,
 910                ..
 911            })
 912        ));
 913        assert!(matches!(
 914            mapped[3],
 915            LanguageModelCompletionEvent::Stop(StopReason::EndTurn)
 916        ));
 917    }
 918
 919    #[test]
 920    fn into_open_ai_response_builds_complete_payload() {
 921        let tool_call_id = LanguageModelToolUseId::from("call-42");
 922        let tool_input = json!({ "city": "Boston" });
 923        let tool_arguments = serde_json::to_string(&tool_input).unwrap();
 924        let tool_use = LanguageModelToolUse {
 925            id: tool_call_id.clone(),
 926            name: Arc::from("get_weather"),
 927            raw_input: tool_arguments.clone(),
 928            input: tool_input,
 929            is_input_complete: true,
 930            thought_signature: None,
 931        };
 932        let tool_result = LanguageModelToolResult {
 933            tool_use_id: tool_call_id,
 934            tool_name: Arc::from("get_weather"),
 935            is_error: false,
 936            content: LanguageModelToolResultContent::Text(Arc::from("Sunny")),
 937            output: Some(json!({ "forecast": "Sunny" })),
 938        };
 939        let user_image = LanguageModelImage {
 940            source: SharedString::from("aGVsbG8="),
 941            size: None,
 942        };
 943        let expected_image_url = user_image.to_base64_url();
 944
 945        let request = LanguageModelRequest {
 946            thread_id: Some("thread-123".into()),
 947            prompt_id: None,
 948            intent: None,
 949            messages: vec![
 950                LanguageModelRequestMessage {
 951                    role: Role::System,
 952                    content: vec![MessageContent::Text("System context".into())],
 953                    cache: false,
 954                    reasoning_details: None,
 955                },
 956                LanguageModelRequestMessage {
 957                    role: Role::User,
 958                    content: vec![
 959                        MessageContent::Text("Please check the weather.".into()),
 960                        MessageContent::Image(user_image),
 961                    ],
 962                    cache: false,
 963                    reasoning_details: None,
 964                },
 965                LanguageModelRequestMessage {
 966                    role: Role::Assistant,
 967                    content: vec![
 968                        MessageContent::Text("Looking that up.".into()),
 969                        MessageContent::ToolUse(tool_use),
 970                    ],
 971                    cache: false,
 972                    reasoning_details: None,
 973                },
 974                LanguageModelRequestMessage {
 975                    role: Role::Assistant,
 976                    content: vec![MessageContent::ToolResult(tool_result)],
 977                    cache: false,
 978                    reasoning_details: None,
 979                },
 980            ],
 981            tools: vec![LanguageModelRequestTool {
 982                name: "get_weather".into(),
 983                description: "Fetches the weather".into(),
 984                input_schema: json!({ "type": "object" }),
 985                use_input_streaming: false,
 986            }],
 987            tool_choice: Some(LanguageModelToolChoice::Any),
 988            stop: vec!["<STOP>".into()],
 989            temperature: None,
 990            thinking_allowed: false,
 991            thinking_effort: None,
 992            speed: None,
 993        };
 994
 995        let response = into_open_ai_response(
 996            request,
 997            "custom-model",
 998            true,
 999            true,
1000            Some(2048),
1001            Some(ReasoningEffort::Low),
1002        );
1003
1004        let serialized = serde_json::to_value(&response).unwrap();
1005        let expected = json!({
1006            "model": "custom-model",
1007            "input": [
1008                {
1009                    "type": "message",
1010                    "role": "system",
1011                    "content": [
1012                        { "type": "input_text", "text": "System context" }
1013                    ]
1014                },
1015                {
1016                    "type": "message",
1017                    "role": "user",
1018                    "content": [
1019                        { "type": "input_text", "text": "Please check the weather." },
1020                        { "type": "input_image", "image_url": expected_image_url }
1021                    ]
1022                },
1023                {
1024                    "type": "message",
1025                    "role": "assistant",
1026                    "content": [
1027                        { "type": "output_text", "text": "Looking that up.", "annotations": [] }
1028                    ]
1029                },
1030                {
1031                    "type": "function_call",
1032                    "call_id": "call-42",
1033                    "name": "get_weather",
1034                    "arguments": tool_arguments
1035                },
1036                {
1037                    "type": "function_call_output",
1038                    "call_id": "call-42",
1039                    "output": "Sunny"
1040                }
1041            ],
1042            "stream": true,
1043            "max_output_tokens": 2048,
1044            "parallel_tool_calls": true,
1045            "tool_choice": "required",
1046            "tools": [
1047                {
1048                    "type": "function",
1049                    "name": "get_weather",
1050                    "description": "Fetches the weather",
1051                    "parameters": { "type": "object" }
1052                }
1053            ],
1054            "prompt_cache_key": "thread-123",
1055            "reasoning": { "effort": "low", "summary": "auto" }
1056        });
1057
1058        assert_eq!(serialized, expected);
1059    }
1060
1061    #[test]
1062    fn responses_stream_maps_tool_calls() {
1063        let events = vec![
1064            ResponsesStreamEvent::OutputItemAdded {
1065                output_index: 0,
1066                sequence_number: None,
1067                item: response_item_function_call("item_fn", Some("{\"city\":\"Bos")),
1068            },
1069            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1070                item_id: "item_fn".into(),
1071                output_index: 0,
1072                delta: "ton\"}".into(),
1073                sequence_number: None,
1074            },
1075            ResponsesStreamEvent::FunctionCallArgumentsDone {
1076                item_id: "item_fn".into(),
1077                output_index: 0,
1078                arguments: "{\"city\":\"Boston\"}".into(),
1079                sequence_number: None,
1080            },
1081            ResponsesStreamEvent::Completed {
1082                response: ResponseSummary::default(),
1083            },
1084        ];
1085
1086        let mapped = map_response_events(events);
1087        assert_eq!(mapped.len(), 3);
1088        assert!(matches!(
1089            mapped[0],
1090            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1091                is_input_complete: false,
1092                ..
1093            })
1094        ));
1095        assert!(matches!(
1096            mapped[1],
1097            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1098                ref id,
1099                ref name,
1100                ref raw_input,
1101                is_input_complete: true,
1102                ..
1103            }) if id.to_string() == "call_123"
1104                && name.as_ref() == "get_weather"
1105                && raw_input == "{\"city\":\"Boston\"}"
1106        ));
1107        assert!(matches!(
1108            mapped[2],
1109            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1110        ));
1111    }
1112
1113    #[test]
1114    fn responses_stream_uses_max_tokens_stop_reason() {
1115        let events = vec![ResponsesStreamEvent::Incomplete {
1116            response: ResponseSummary {
1117                status_details: Some(ResponseStatusDetails {
1118                    reason: Some("max_output_tokens".into()),
1119                    r#type: Some("incomplete".into()),
1120                    error: None,
1121                }),
1122                usage: Some(ResponseUsage {
1123                    input_tokens: Some(10),
1124                    output_tokens: Some(20),
1125                    total_tokens: Some(30),
1126                }),
1127                ..Default::default()
1128            },
1129        }];
1130
1131        let mapped = map_response_events(events);
1132        assert!(matches!(
1133            mapped[0],
1134            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1135                input_tokens: 10,
1136                output_tokens: 20,
1137                ..
1138            })
1139        ));
1140        assert!(matches!(
1141            mapped[1],
1142            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1143        ));
1144    }
1145
1146    #[test]
1147    fn responses_stream_handles_multiple_tool_calls() {
1148        let events = vec![
1149            ResponsesStreamEvent::OutputItemAdded {
1150                output_index: 0,
1151                sequence_number: None,
1152                item: response_item_function_call("item_fn1", Some("{\"city\":\"NYC\"}")),
1153            },
1154            ResponsesStreamEvent::FunctionCallArgumentsDone {
1155                item_id: "item_fn1".into(),
1156                output_index: 0,
1157                arguments: "{\"city\":\"NYC\"}".into(),
1158                sequence_number: None,
1159            },
1160            ResponsesStreamEvent::OutputItemAdded {
1161                output_index: 1,
1162                sequence_number: None,
1163                item: response_item_function_call("item_fn2", Some("{\"city\":\"LA\"}")),
1164            },
1165            ResponsesStreamEvent::FunctionCallArgumentsDone {
1166                item_id: "item_fn2".into(),
1167                output_index: 1,
1168                arguments: "{\"city\":\"LA\"}".into(),
1169                sequence_number: None,
1170            },
1171            ResponsesStreamEvent::Completed {
1172                response: ResponseSummary::default(),
1173            },
1174        ];
1175
1176        let mapped = map_response_events(events);
1177        assert_eq!(mapped.len(), 3);
1178        assert!(matches!(
1179            mapped[0],
1180            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1181            if raw_input == "{\"city\":\"NYC\"}"
1182        ));
1183        assert!(matches!(
1184            mapped[1],
1185            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1186            if raw_input == "{\"city\":\"LA\"}"
1187        ));
1188        assert!(matches!(
1189            mapped[2],
1190            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1191        ));
1192    }
1193
1194    #[test]
1195    fn responses_stream_handles_mixed_text_and_tool_calls() {
1196        let events = vec![
1197            ResponsesStreamEvent::OutputItemAdded {
1198                output_index: 0,
1199                sequence_number: None,
1200                item: response_item_message("msg_123"),
1201            },
1202            ResponsesStreamEvent::OutputTextDelta {
1203                item_id: "msg_123".into(),
1204                output_index: 0,
1205                content_index: Some(0),
1206                delta: "Let me check that".into(),
1207            },
1208            ResponsesStreamEvent::OutputItemAdded {
1209                output_index: 1,
1210                sequence_number: None,
1211                item: response_item_function_call("item_fn", Some("{\"query\":\"test\"}")),
1212            },
1213            ResponsesStreamEvent::FunctionCallArgumentsDone {
1214                item_id: "item_fn".into(),
1215                output_index: 1,
1216                arguments: "{\"query\":\"test\"}".into(),
1217                sequence_number: None,
1218            },
1219            ResponsesStreamEvent::Completed {
1220                response: ResponseSummary::default(),
1221            },
1222        ];
1223
1224        let mapped = map_response_events(events);
1225        assert!(matches!(
1226            mapped[0],
1227            LanguageModelCompletionEvent::StartMessage { .. }
1228        ));
1229        assert!(
1230            matches!(mapped[1], LanguageModelCompletionEvent::Text(ref text) if text == "Let me check that")
1231        );
1232        assert!(
1233            matches!(mapped[2], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"query\":\"test\"}")
1234        );
1235        assert!(matches!(
1236            mapped[3],
1237            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1238        ));
1239    }
1240
1241    #[test]
1242    fn responses_stream_handles_json_parse_error() {
1243        let events = vec![
1244            ResponsesStreamEvent::OutputItemAdded {
1245                output_index: 0,
1246                sequence_number: None,
1247                item: response_item_function_call("item_fn", Some("{invalid json")),
1248            },
1249            ResponsesStreamEvent::FunctionCallArgumentsDone {
1250                item_id: "item_fn".into(),
1251                output_index: 0,
1252                arguments: "{invalid json".into(),
1253                sequence_number: None,
1254            },
1255            ResponsesStreamEvent::Completed {
1256                response: ResponseSummary::default(),
1257            },
1258        ];
1259
1260        let mapped = map_response_events(events);
1261        assert!(matches!(
1262            mapped[0],
1263            LanguageModelCompletionEvent::ToolUseJsonParseError { ref raw_input, .. }
1264            if raw_input.as_ref() == "{invalid json"
1265        ));
1266    }
1267
1268    #[test]
1269    fn responses_stream_handles_incomplete_function_call() {
1270        let events = vec![
1271            ResponsesStreamEvent::OutputItemAdded {
1272                output_index: 0,
1273                sequence_number: None,
1274                item: response_item_function_call("item_fn", Some("{\"city\":")),
1275            },
1276            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1277                item_id: "item_fn".into(),
1278                output_index: 0,
1279                delta: "\"Boston\"".into(),
1280                sequence_number: None,
1281            },
1282            ResponsesStreamEvent::Incomplete {
1283                response: ResponseSummary {
1284                    status_details: Some(ResponseStatusDetails {
1285                        reason: Some("max_output_tokens".into()),
1286                        r#type: Some("incomplete".into()),
1287                        error: None,
1288                    }),
1289                    output: vec![response_item_function_call(
1290                        "item_fn",
1291                        Some("{\"city\":\"Boston\"}"),
1292                    )],
1293                    ..Default::default()
1294                },
1295            },
1296        ];
1297
1298        let mapped = map_response_events(events);
1299        assert_eq!(mapped.len(), 3);
1300        assert!(matches!(
1301            mapped[0],
1302            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1303                is_input_complete: false,
1304                ..
1305            })
1306        ));
1307        assert!(
1308            matches!(mapped[1], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, is_input_complete: true, .. }) if raw_input == "{\"city\":\"Boston\"}")
1309        );
1310        assert!(matches!(
1311            mapped[2],
1312            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1313        ));
1314    }
1315
1316    #[test]
1317    fn responses_stream_incomplete_does_not_duplicate_tool_calls() {
1318        let events = vec![
1319            ResponsesStreamEvent::OutputItemAdded {
1320                output_index: 0,
1321                sequence_number: None,
1322                item: response_item_function_call("item_fn", Some("{\"city\":\"Boston\"}")),
1323            },
1324            ResponsesStreamEvent::FunctionCallArgumentsDone {
1325                item_id: "item_fn".into(),
1326                output_index: 0,
1327                arguments: "{\"city\":\"Boston\"}".into(),
1328                sequence_number: None,
1329            },
1330            ResponsesStreamEvent::Incomplete {
1331                response: ResponseSummary {
1332                    status_details: Some(ResponseStatusDetails {
1333                        reason: Some("max_output_tokens".into()),
1334                        r#type: Some("incomplete".into()),
1335                        error: None,
1336                    }),
1337                    output: vec![response_item_function_call(
1338                        "item_fn",
1339                        Some("{\"city\":\"Boston\"}"),
1340                    )],
1341                    ..Default::default()
1342                },
1343            },
1344        ];
1345
1346        let mapped = map_response_events(events);
1347        assert_eq!(mapped.len(), 2);
1348        assert!(
1349            matches!(mapped[0], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"city\":\"Boston\"}")
1350        );
1351        assert!(matches!(
1352            mapped[1],
1353            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1354        ));
1355    }
1356
1357    #[test]
1358    fn responses_stream_handles_empty_tool_arguments() {
1359        let events = vec![
1360            ResponsesStreamEvent::OutputItemAdded {
1361                output_index: 0,
1362                sequence_number: None,
1363                item: response_item_function_call("item_fn", Some("")),
1364            },
1365            ResponsesStreamEvent::FunctionCallArgumentsDone {
1366                item_id: "item_fn".into(),
1367                output_index: 0,
1368                arguments: "".into(),
1369                sequence_number: None,
1370            },
1371            ResponsesStreamEvent::Completed {
1372                response: ResponseSummary::default(),
1373            },
1374        ];
1375
1376        let mapped = map_response_events(events);
1377        assert_eq!(mapped.len(), 2);
1378        assert!(matches!(
1379            &mapped[0],
1380            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1381                id, name, raw_input, input, ..
1382            }) if id.to_string() == "call_123"
1383                && name.as_ref() == "get_weather"
1384                && raw_input == ""
1385                && input.is_object()
1386                && input.as_object().unwrap().is_empty()
1387        ));
1388        assert!(matches!(
1389            mapped[1],
1390            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1391        ));
1392    }
1393
1394    #[test]
1395    fn responses_stream_emits_partial_tool_use_events() {
1396        let events = vec![
1397            ResponsesStreamEvent::OutputItemAdded {
1398                output_index: 0,
1399                sequence_number: None,
1400                item: ResponseOutputItem::FunctionCall(
1401                    crate::responses::ResponseFunctionToolCall {
1402                        id: Some("item_fn".to_string()),
1403                        status: Some("in_progress".to_string()),
1404                        name: Some("get_weather".to_string()),
1405                        call_id: Some("call_abc".to_string()),
1406                        arguments: String::new(),
1407                    },
1408                ),
1409            },
1410            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1411                item_id: "item_fn".into(),
1412                output_index: 0,
1413                delta: "{\"city\":\"Bos".into(),
1414                sequence_number: None,
1415            },
1416            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1417                item_id: "item_fn".into(),
1418                output_index: 0,
1419                delta: "ton\"}".into(),
1420                sequence_number: None,
1421            },
1422            ResponsesStreamEvent::FunctionCallArgumentsDone {
1423                item_id: "item_fn".into(),
1424                output_index: 0,
1425                arguments: "{\"city\":\"Boston\"}".into(),
1426                sequence_number: None,
1427            },
1428            ResponsesStreamEvent::Completed {
1429                response: ResponseSummary::default(),
1430            },
1431        ];
1432
1433        let mapped = map_response_events(events);
1434        assert!(mapped.len() >= 3);
1435
1436        let complete_tool_use = mapped.iter().find(|e| {
1437            matches!(
1438                e,
1439                LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1440                    is_input_complete: true,
1441                    ..
1442                })
1443            )
1444        });
1445        assert!(
1446            complete_tool_use.is_some(),
1447            "should have a complete tool use event"
1448        );
1449
1450        let tool_uses: Vec<_> = mapped
1451            .iter()
1452            .filter(|e| matches!(e, LanguageModelCompletionEvent::ToolUse(_)))
1453            .collect();
1454        assert!(
1455            tool_uses.len() >= 2,
1456            "should have at least one partial and one complete event"
1457        );
1458        assert!(matches!(
1459            tool_uses.last().unwrap(),
1460            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1461                is_input_complete: true,
1462                ..
1463            })
1464        ));
1465    }
1466
1467    #[test]
1468    fn responses_stream_maps_reasoning_summary_deltas() {
1469        let events = vec![
1470            ResponsesStreamEvent::OutputItemAdded {
1471                output_index: 0,
1472                sequence_number: None,
1473                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1474                    id: Some("rs_123".into()),
1475                    summary: vec![],
1476                }),
1477            },
1478            ResponsesStreamEvent::ReasoningSummaryPartAdded {
1479                item_id: "rs_123".into(),
1480                output_index: 0,
1481                summary_index: 0,
1482            },
1483            ResponsesStreamEvent::ReasoningSummaryTextDelta {
1484                item_id: "rs_123".into(),
1485                output_index: 0,
1486                delta: "Thinking about".into(),
1487            },
1488            ResponsesStreamEvent::ReasoningSummaryTextDelta {
1489                item_id: "rs_123".into(),
1490                output_index: 0,
1491                delta: " the answer".into(),
1492            },
1493            ResponsesStreamEvent::ReasoningSummaryTextDone {
1494                item_id: "rs_123".into(),
1495                output_index: 0,
1496                text: "Thinking about the answer".into(),
1497            },
1498            ResponsesStreamEvent::ReasoningSummaryPartDone {
1499                item_id: "rs_123".into(),
1500                output_index: 0,
1501                summary_index: 0,
1502            },
1503            ResponsesStreamEvent::ReasoningSummaryPartAdded {
1504                item_id: "rs_123".into(),
1505                output_index: 0,
1506                summary_index: 1,
1507            },
1508            ResponsesStreamEvent::ReasoningSummaryTextDelta {
1509                item_id: "rs_123".into(),
1510                output_index: 0,
1511                delta: "Second part".into(),
1512            },
1513            ResponsesStreamEvent::ReasoningSummaryTextDone {
1514                item_id: "rs_123".into(),
1515                output_index: 0,
1516                text: "Second part".into(),
1517            },
1518            ResponsesStreamEvent::ReasoningSummaryPartDone {
1519                item_id: "rs_123".into(),
1520                output_index: 0,
1521                summary_index: 1,
1522            },
1523            ResponsesStreamEvent::OutputItemDone {
1524                output_index: 0,
1525                sequence_number: None,
1526                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1527                    id: Some("rs_123".into()),
1528                    summary: vec![
1529                        ReasoningSummaryPart::SummaryText {
1530                            text: "Thinking about the answer".into(),
1531                        },
1532                        ReasoningSummaryPart::SummaryText {
1533                            text: "Second part".into(),
1534                        },
1535                    ],
1536                }),
1537            },
1538            ResponsesStreamEvent::OutputItemAdded {
1539                output_index: 1,
1540                sequence_number: None,
1541                item: response_item_message("msg_456"),
1542            },
1543            ResponsesStreamEvent::OutputTextDelta {
1544                item_id: "msg_456".into(),
1545                output_index: 1,
1546                content_index: Some(0),
1547                delta: "The answer is 42".into(),
1548            },
1549            ResponsesStreamEvent::Completed {
1550                response: ResponseSummary::default(),
1551            },
1552        ];
1553
1554        let mapped = map_response_events(events);
1555
1556        let thinking_events: Vec<_> = mapped
1557            .iter()
1558            .filter(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. }))
1559            .collect();
1560        assert_eq!(
1561            thinking_events.len(),
1562            4,
1563            "expected 4 thinking events, got {:?}",
1564            thinking_events
1565        );
1566        assert!(
1567            matches!(&thinking_events[0], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Thinking about")
1568        );
1569        assert!(
1570            matches!(&thinking_events[1], LanguageModelCompletionEvent::Thinking { text, .. } if text == " the answer")
1571        );
1572        assert!(
1573            matches!(&thinking_events[2], LanguageModelCompletionEvent::Thinking { text, .. } if text == "\n\n"),
1574            "expected separator between summary parts"
1575        );
1576        assert!(
1577            matches!(&thinking_events[3], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Second part")
1578        );
1579
1580        assert!(mapped.iter().any(
1581            |e| matches!(e, LanguageModelCompletionEvent::Text(t) if t == "The answer is 42")
1582        ));
1583    }
1584
1585    #[test]
1586    fn responses_stream_maps_reasoning_from_done_only() {
1587        let events = vec![
1588            ResponsesStreamEvent::OutputItemAdded {
1589                output_index: 0,
1590                sequence_number: None,
1591                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1592                    id: Some("rs_789".into()),
1593                    summary: vec![],
1594                }),
1595            },
1596            ResponsesStreamEvent::OutputItemDone {
1597                output_index: 0,
1598                sequence_number: None,
1599                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1600                    id: Some("rs_789".into()),
1601                    summary: vec![ReasoningSummaryPart::SummaryText {
1602                        text: "Summary without deltas".into(),
1603                    }],
1604                }),
1605            },
1606            ResponsesStreamEvent::Completed {
1607                response: ResponseSummary::default(),
1608            },
1609        ];
1610
1611        let mapped = map_response_events(events);
1612        assert!(
1613            !mapped
1614                .iter()
1615                .any(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. })),
1616            "OutputItemDone reasoning should not produce Thinking events"
1617        );
1618    }
1619
1620    #[test]
1621    fn into_open_ai_interleaved_reasoning() {
1622        let tool_use_id = LanguageModelToolUseId::from("call-1");
1623        let tool_input = json!({"query": "foo"});
1624        let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1625        let tool_use = LanguageModelToolUse {
1626            id: tool_use_id.clone(),
1627            name: Arc::from("search"),
1628            raw_input: tool_arguments.clone(),
1629            input: tool_input,
1630            is_input_complete: true,
1631            thought_signature: None,
1632        };
1633        let tool_result = LanguageModelToolResult {
1634            tool_use_id: tool_use_id,
1635            tool_name: Arc::from("search"),
1636            is_error: false,
1637            content: LanguageModelToolResultContent::Text(Arc::from("result")),
1638            output: None,
1639        };
1640        let request = LanguageModelRequest {
1641            thread_id: None,
1642            prompt_id: None,
1643            intent: None,
1644            messages: vec![
1645                LanguageModelRequestMessage {
1646                    role: Role::User,
1647                    content: vec![MessageContent::Text("search for something".into())],
1648                    cache: false,
1649                    reasoning_details: None,
1650                },
1651                LanguageModelRequestMessage {
1652                    role: Role::Assistant,
1653                    content: vec![
1654                        MessageContent::Thinking {
1655                            text: "I should search".into(),
1656                            signature: None,
1657                        },
1658                        MessageContent::Text("Searching now.".into()),
1659                        MessageContent::ToolUse(tool_use),
1660                    ],
1661                    cache: false,
1662                    reasoning_details: None,
1663                },
1664                LanguageModelRequestMessage {
1665                    role: Role::Assistant,
1666                    content: vec![MessageContent::ToolResult(tool_result)],
1667                    cache: false,
1668                    reasoning_details: None,
1669                },
1670            ],
1671            tools: vec![],
1672            tool_choice: None,
1673            stop: vec![],
1674            temperature: None,
1675            thinking_allowed: true,
1676            thinking_effort: None,
1677            speed: None,
1678        };
1679
1680        let result = into_open_ai(request.clone(), "model", false, false, None, None, true);
1681        assert_eq!(
1682            serde_json::to_value(&result).unwrap()["messages"],
1683            json!([
1684                {"role": "user", "content": "search for something"},
1685                {
1686                    "role": "assistant",
1687                    "content": "Searching now.",
1688                    "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}],
1689                    "reasoning_content": "I should search"
1690                },
1691                {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1692            ])
1693        );
1694
1695        let result = into_open_ai(request, "model", false, false, None, None, false);
1696        assert_eq!(
1697            serde_json::to_value(&result).unwrap()["messages"],
1698            json!([
1699                {"role": "user", "content": "search for something"},
1700                {
1701                    "role": "assistant",
1702                    "content": [
1703                        {"type": "text", "text": "I should search"},
1704                        {"type": "text", "text": "Searching now."}
1705                    ],
1706                    "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}]
1707                },
1708                {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1709            ])
1710        );
1711    }
1712}