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, Model, 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
 821pub fn collect_tiktoken_messages(
 822    request: LanguageModelRequest,
 823) -> Vec<tiktoken_rs::ChatCompletionRequestMessage> {
 824    request
 825        .messages
 826        .into_iter()
 827        .map(|message| tiktoken_rs::ChatCompletionRequestMessage {
 828            role: match message.role {
 829                Role::User => "user".into(),
 830                Role::Assistant => "assistant".into(),
 831                Role::System => "system".into(),
 832            },
 833            content: Some(message.string_contents()),
 834            name: None,
 835            function_call: None,
 836        })
 837        .collect::<Vec<_>>()
 838}
 839
 840/// Count tokens for an OpenAI model. This is synchronous; callers should spawn
 841/// it on a background thread if needed.
 842pub fn count_open_ai_tokens(request: LanguageModelRequest, model: Model) -> Result<u64> {
 843    let messages = collect_tiktoken_messages(request);
 844    match model {
 845        Model::Custom { max_tokens, .. } => {
 846            let model = if max_tokens >= 100_000 {
 847                // If the max tokens is 100k or more, it likely uses the o200k_base tokenizer
 848                "gpt-4o"
 849            } else {
 850                // Otherwise fallback to gpt-4, since only cl100k_base and o200k_base are
 851                // supported with this tiktoken method
 852                "gpt-4"
 853            };
 854            tiktoken_rs::num_tokens_from_messages(model, &messages)
 855        }
 856        // Currently supported by tiktoken_rs
 857        // Sometimes tiktoken-rs is behind on model support. If that is the case, make a new branch
 858        // arm with an override. We enumerate all supported models here so that we can check if new
 859        // models are supported yet or not.
 860        Model::ThreePointFiveTurbo
 861        | Model::Four
 862        | Model::FourTurbo
 863        | Model::FourOmniMini
 864        | Model::FourPointOneNano
 865        | Model::O1
 866        | Model::O3
 867        | Model::O3Mini
 868        | Model::Five
 869        | Model::FiveCodex
 870        | Model::FiveMini
 871        | Model::FiveNano => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
 872        // GPT-5.1, 5.2, 5.2-codex, 5.3-codex, 5.4, and 5.4-pro don't have dedicated tiktoken support; use gpt-5 tokenizer
 873        Model::FivePointOne
 874        | Model::FivePointTwo
 875        | Model::FivePointTwoCodex
 876        | Model::FivePointThreeCodex
 877        | Model::FivePointFour
 878        | Model::FivePointFourPro => tiktoken_rs::num_tokens_from_messages("gpt-5", &messages),
 879    }
 880    .map(|tokens| tokens as u64)
 881}
 882
 883#[cfg(test)]
 884mod tests {
 885    use crate::responses::{
 886        ReasoningSummaryPart, ResponseFunctionToolCall, ResponseOutputItem, ResponseOutputMessage,
 887        ResponseReasoningItem, ResponseStatusDetails, ResponseSummary, ResponseUsage,
 888        StreamEvent as ResponsesStreamEvent,
 889    };
 890    use futures::{StreamExt, executor::block_on};
 891    use language_model_core::{
 892        LanguageModelImage, LanguageModelRequestMessage, LanguageModelRequestTool,
 893        LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
 894        LanguageModelToolUseId, SharedString,
 895    };
 896    use pretty_assertions::assert_eq;
 897    use serde_json::json;
 898
 899    use super::*;
 900
 901    fn map_response_events(events: Vec<ResponsesStreamEvent>) -> Vec<LanguageModelCompletionEvent> {
 902        block_on(async {
 903            OpenAiResponseEventMapper::new()
 904                .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
 905                .collect::<Vec<_>>()
 906                .await
 907                .into_iter()
 908                .map(Result::unwrap)
 909                .collect()
 910        })
 911    }
 912
 913    fn response_item_message(id: &str) -> ResponseOutputItem {
 914        ResponseOutputItem::Message(ResponseOutputMessage {
 915            id: Some(id.to_string()),
 916            role: Some("assistant".to_string()),
 917            status: Some("in_progress".to_string()),
 918            content: vec![],
 919        })
 920    }
 921
 922    fn response_item_function_call(id: &str, args: Option<&str>) -> ResponseOutputItem {
 923        ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
 924            id: Some(id.to_string()),
 925            status: Some("in_progress".to_string()),
 926            name: Some("get_weather".to_string()),
 927            call_id: Some("call_123".to_string()),
 928            arguments: args.map(|s| s.to_string()).unwrap_or_default(),
 929        })
 930    }
 931
 932    #[test]
 933    fn tiktoken_rs_support() {
 934        let request = LanguageModelRequest {
 935            thread_id: None,
 936            prompt_id: None,
 937            intent: None,
 938            messages: vec![LanguageModelRequestMessage {
 939                role: Role::User,
 940                content: vec![MessageContent::Text("message".into())],
 941                cache: false,
 942                reasoning_details: None,
 943            }],
 944            tools: vec![],
 945            tool_choice: None,
 946            stop: vec![],
 947            temperature: None,
 948            thinking_allowed: true,
 949            thinking_effort: None,
 950            speed: None,
 951        };
 952
 953        // Validate that all models are supported by tiktoken-rs
 954        for model in <Model as strum::IntoEnumIterator>::iter() {
 955            let count = count_open_ai_tokens(request.clone(), model).unwrap();
 956            assert!(count > 0);
 957        }
 958    }
 959
 960    #[test]
 961    fn responses_stream_maps_text_and_usage() {
 962        let events = vec![
 963            ResponsesStreamEvent::OutputItemAdded {
 964                output_index: 0,
 965                sequence_number: None,
 966                item: response_item_message("msg_123"),
 967            },
 968            ResponsesStreamEvent::OutputTextDelta {
 969                item_id: "msg_123".into(),
 970                output_index: 0,
 971                content_index: Some(0),
 972                delta: "Hello".into(),
 973            },
 974            ResponsesStreamEvent::Completed {
 975                response: ResponseSummary {
 976                    usage: Some(ResponseUsage {
 977                        input_tokens: Some(5),
 978                        output_tokens: Some(3),
 979                        total_tokens: Some(8),
 980                    }),
 981                    ..Default::default()
 982                },
 983            },
 984        ];
 985
 986        let mapped = map_response_events(events);
 987        assert!(matches!(
 988            mapped[0],
 989            LanguageModelCompletionEvent::StartMessage { ref message_id } if message_id == "msg_123"
 990        ));
 991        assert!(matches!(
 992            mapped[1],
 993            LanguageModelCompletionEvent::Text(ref text) if text == "Hello"
 994        ));
 995        assert!(matches!(
 996            mapped[2],
 997            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
 998                input_tokens: 5,
 999                output_tokens: 3,
1000                ..
1001            })
1002        ));
1003        assert!(matches!(
1004            mapped[3],
1005            LanguageModelCompletionEvent::Stop(StopReason::EndTurn)
1006        ));
1007    }
1008
1009    #[test]
1010    fn into_open_ai_response_builds_complete_payload() {
1011        let tool_call_id = LanguageModelToolUseId::from("call-42");
1012        let tool_input = json!({ "city": "Boston" });
1013        let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1014        let tool_use = LanguageModelToolUse {
1015            id: tool_call_id.clone(),
1016            name: Arc::from("get_weather"),
1017            raw_input: tool_arguments.clone(),
1018            input: tool_input,
1019            is_input_complete: true,
1020            thought_signature: None,
1021        };
1022        let tool_result = LanguageModelToolResult {
1023            tool_use_id: tool_call_id,
1024            tool_name: Arc::from("get_weather"),
1025            is_error: false,
1026            content: LanguageModelToolResultContent::Text(Arc::from("Sunny")),
1027            output: Some(json!({ "forecast": "Sunny" })),
1028        };
1029        let user_image = LanguageModelImage {
1030            source: SharedString::from("aGVsbG8="),
1031            size: None,
1032        };
1033        let expected_image_url = user_image.to_base64_url();
1034
1035        let request = LanguageModelRequest {
1036            thread_id: Some("thread-123".into()),
1037            prompt_id: None,
1038            intent: None,
1039            messages: vec![
1040                LanguageModelRequestMessage {
1041                    role: Role::System,
1042                    content: vec![MessageContent::Text("System context".into())],
1043                    cache: false,
1044                    reasoning_details: None,
1045                },
1046                LanguageModelRequestMessage {
1047                    role: Role::User,
1048                    content: vec![
1049                        MessageContent::Text("Please check the weather.".into()),
1050                        MessageContent::Image(user_image),
1051                    ],
1052                    cache: false,
1053                    reasoning_details: None,
1054                },
1055                LanguageModelRequestMessage {
1056                    role: Role::Assistant,
1057                    content: vec![
1058                        MessageContent::Text("Looking that up.".into()),
1059                        MessageContent::ToolUse(tool_use),
1060                    ],
1061                    cache: false,
1062                    reasoning_details: None,
1063                },
1064                LanguageModelRequestMessage {
1065                    role: Role::Assistant,
1066                    content: vec![MessageContent::ToolResult(tool_result)],
1067                    cache: false,
1068                    reasoning_details: None,
1069                },
1070            ],
1071            tools: vec![LanguageModelRequestTool {
1072                name: "get_weather".into(),
1073                description: "Fetches the weather".into(),
1074                input_schema: json!({ "type": "object" }),
1075                use_input_streaming: false,
1076            }],
1077            tool_choice: Some(LanguageModelToolChoice::Any),
1078            stop: vec!["<STOP>".into()],
1079            temperature: None,
1080            thinking_allowed: false,
1081            thinking_effort: None,
1082            speed: None,
1083        };
1084
1085        let response = into_open_ai_response(
1086            request,
1087            "custom-model",
1088            true,
1089            true,
1090            Some(2048),
1091            Some(ReasoningEffort::Low),
1092        );
1093
1094        let serialized = serde_json::to_value(&response).unwrap();
1095        let expected = json!({
1096            "model": "custom-model",
1097            "input": [
1098                {
1099                    "type": "message",
1100                    "role": "system",
1101                    "content": [
1102                        { "type": "input_text", "text": "System context" }
1103                    ]
1104                },
1105                {
1106                    "type": "message",
1107                    "role": "user",
1108                    "content": [
1109                        { "type": "input_text", "text": "Please check the weather." },
1110                        { "type": "input_image", "image_url": expected_image_url }
1111                    ]
1112                },
1113                {
1114                    "type": "message",
1115                    "role": "assistant",
1116                    "content": [
1117                        { "type": "output_text", "text": "Looking that up.", "annotations": [] }
1118                    ]
1119                },
1120                {
1121                    "type": "function_call",
1122                    "call_id": "call-42",
1123                    "name": "get_weather",
1124                    "arguments": tool_arguments
1125                },
1126                {
1127                    "type": "function_call_output",
1128                    "call_id": "call-42",
1129                    "output": "Sunny"
1130                }
1131            ],
1132            "stream": true,
1133            "max_output_tokens": 2048,
1134            "parallel_tool_calls": true,
1135            "tool_choice": "required",
1136            "tools": [
1137                {
1138                    "type": "function",
1139                    "name": "get_weather",
1140                    "description": "Fetches the weather",
1141                    "parameters": { "type": "object" }
1142                }
1143            ],
1144            "prompt_cache_key": "thread-123",
1145            "reasoning": { "effort": "low", "summary": "auto" }
1146        });
1147
1148        assert_eq!(serialized, expected);
1149    }
1150
1151    #[test]
1152    fn responses_stream_maps_tool_calls() {
1153        let events = vec![
1154            ResponsesStreamEvent::OutputItemAdded {
1155                output_index: 0,
1156                sequence_number: None,
1157                item: response_item_function_call("item_fn", Some("{\"city\":\"Bos")),
1158            },
1159            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1160                item_id: "item_fn".into(),
1161                output_index: 0,
1162                delta: "ton\"}".into(),
1163                sequence_number: None,
1164            },
1165            ResponsesStreamEvent::FunctionCallArgumentsDone {
1166                item_id: "item_fn".into(),
1167                output_index: 0,
1168                arguments: "{\"city\":\"Boston\"}".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 {
1181                is_input_complete: false,
1182                ..
1183            })
1184        ));
1185        assert!(matches!(
1186            mapped[1],
1187            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1188                ref id,
1189                ref name,
1190                ref raw_input,
1191                is_input_complete: true,
1192                ..
1193            }) if id.to_string() == "call_123"
1194                && name.as_ref() == "get_weather"
1195                && raw_input == "{\"city\":\"Boston\"}"
1196        ));
1197        assert!(matches!(
1198            mapped[2],
1199            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1200        ));
1201    }
1202
1203    #[test]
1204    fn responses_stream_uses_max_tokens_stop_reason() {
1205        let events = vec![ResponsesStreamEvent::Incomplete {
1206            response: ResponseSummary {
1207                status_details: Some(ResponseStatusDetails {
1208                    reason: Some("max_output_tokens".into()),
1209                    r#type: Some("incomplete".into()),
1210                    error: None,
1211                }),
1212                usage: Some(ResponseUsage {
1213                    input_tokens: Some(10),
1214                    output_tokens: Some(20),
1215                    total_tokens: Some(30),
1216                }),
1217                ..Default::default()
1218            },
1219        }];
1220
1221        let mapped = map_response_events(events);
1222        assert!(matches!(
1223            mapped[0],
1224            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1225                input_tokens: 10,
1226                output_tokens: 20,
1227                ..
1228            })
1229        ));
1230        assert!(matches!(
1231            mapped[1],
1232            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1233        ));
1234    }
1235
1236    #[test]
1237    fn responses_stream_handles_multiple_tool_calls() {
1238        let events = vec![
1239            ResponsesStreamEvent::OutputItemAdded {
1240                output_index: 0,
1241                sequence_number: None,
1242                item: response_item_function_call("item_fn1", Some("{\"city\":\"NYC\"}")),
1243            },
1244            ResponsesStreamEvent::FunctionCallArgumentsDone {
1245                item_id: "item_fn1".into(),
1246                output_index: 0,
1247                arguments: "{\"city\":\"NYC\"}".into(),
1248                sequence_number: None,
1249            },
1250            ResponsesStreamEvent::OutputItemAdded {
1251                output_index: 1,
1252                sequence_number: None,
1253                item: response_item_function_call("item_fn2", Some("{\"city\":\"LA\"}")),
1254            },
1255            ResponsesStreamEvent::FunctionCallArgumentsDone {
1256                item_id: "item_fn2".into(),
1257                output_index: 1,
1258                arguments: "{\"city\":\"LA\"}".into(),
1259                sequence_number: None,
1260            },
1261            ResponsesStreamEvent::Completed {
1262                response: ResponseSummary::default(),
1263            },
1264        ];
1265
1266        let mapped = map_response_events(events);
1267        assert_eq!(mapped.len(), 3);
1268        assert!(matches!(
1269            mapped[0],
1270            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1271            if raw_input == "{\"city\":\"NYC\"}"
1272        ));
1273        assert!(matches!(
1274            mapped[1],
1275            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1276            if raw_input == "{\"city\":\"LA\"}"
1277        ));
1278        assert!(matches!(
1279            mapped[2],
1280            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1281        ));
1282    }
1283
1284    #[test]
1285    fn responses_stream_handles_mixed_text_and_tool_calls() {
1286        let events = vec![
1287            ResponsesStreamEvent::OutputItemAdded {
1288                output_index: 0,
1289                sequence_number: None,
1290                item: response_item_message("msg_123"),
1291            },
1292            ResponsesStreamEvent::OutputTextDelta {
1293                item_id: "msg_123".into(),
1294                output_index: 0,
1295                content_index: Some(0),
1296                delta: "Let me check that".into(),
1297            },
1298            ResponsesStreamEvent::OutputItemAdded {
1299                output_index: 1,
1300                sequence_number: None,
1301                item: response_item_function_call("item_fn", Some("{\"query\":\"test\"}")),
1302            },
1303            ResponsesStreamEvent::FunctionCallArgumentsDone {
1304                item_id: "item_fn".into(),
1305                output_index: 1,
1306                arguments: "{\"query\":\"test\"}".into(),
1307                sequence_number: None,
1308            },
1309            ResponsesStreamEvent::Completed {
1310                response: ResponseSummary::default(),
1311            },
1312        ];
1313
1314        let mapped = map_response_events(events);
1315        assert!(matches!(
1316            mapped[0],
1317            LanguageModelCompletionEvent::StartMessage { .. }
1318        ));
1319        assert!(
1320            matches!(mapped[1], LanguageModelCompletionEvent::Text(ref text) if text == "Let me check that")
1321        );
1322        assert!(
1323            matches!(mapped[2], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"query\":\"test\"}")
1324        );
1325        assert!(matches!(
1326            mapped[3],
1327            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1328        ));
1329    }
1330
1331    #[test]
1332    fn responses_stream_handles_json_parse_error() {
1333        let events = vec![
1334            ResponsesStreamEvent::OutputItemAdded {
1335                output_index: 0,
1336                sequence_number: None,
1337                item: response_item_function_call("item_fn", Some("{invalid json")),
1338            },
1339            ResponsesStreamEvent::FunctionCallArgumentsDone {
1340                item_id: "item_fn".into(),
1341                output_index: 0,
1342                arguments: "{invalid json".into(),
1343                sequence_number: None,
1344            },
1345            ResponsesStreamEvent::Completed {
1346                response: ResponseSummary::default(),
1347            },
1348        ];
1349
1350        let mapped = map_response_events(events);
1351        assert!(matches!(
1352            mapped[0],
1353            LanguageModelCompletionEvent::ToolUseJsonParseError { ref raw_input, .. }
1354            if raw_input.as_ref() == "{invalid json"
1355        ));
1356    }
1357
1358    #[test]
1359    fn responses_stream_handles_incomplete_function_call() {
1360        let events = vec![
1361            ResponsesStreamEvent::OutputItemAdded {
1362                output_index: 0,
1363                sequence_number: None,
1364                item: response_item_function_call("item_fn", Some("{\"city\":")),
1365            },
1366            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1367                item_id: "item_fn".into(),
1368                output_index: 0,
1369                delta: "\"Boston\"".into(),
1370                sequence_number: None,
1371            },
1372            ResponsesStreamEvent::Incomplete {
1373                response: ResponseSummary {
1374                    status_details: Some(ResponseStatusDetails {
1375                        reason: Some("max_output_tokens".into()),
1376                        r#type: Some("incomplete".into()),
1377                        error: None,
1378                    }),
1379                    output: vec![response_item_function_call(
1380                        "item_fn",
1381                        Some("{\"city\":\"Boston\"}"),
1382                    )],
1383                    ..Default::default()
1384                },
1385            },
1386        ];
1387
1388        let mapped = map_response_events(events);
1389        assert_eq!(mapped.len(), 3);
1390        assert!(matches!(
1391            mapped[0],
1392            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1393                is_input_complete: false,
1394                ..
1395            })
1396        ));
1397        assert!(
1398            matches!(mapped[1], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, is_input_complete: true, .. }) if raw_input == "{\"city\":\"Boston\"}")
1399        );
1400        assert!(matches!(
1401            mapped[2],
1402            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1403        ));
1404    }
1405
1406    #[test]
1407    fn responses_stream_incomplete_does_not_duplicate_tool_calls() {
1408        let events = vec![
1409            ResponsesStreamEvent::OutputItemAdded {
1410                output_index: 0,
1411                sequence_number: None,
1412                item: response_item_function_call("item_fn", Some("{\"city\":\"Boston\"}")),
1413            },
1414            ResponsesStreamEvent::FunctionCallArgumentsDone {
1415                item_id: "item_fn".into(),
1416                output_index: 0,
1417                arguments: "{\"city\":\"Boston\"}".into(),
1418                sequence_number: None,
1419            },
1420            ResponsesStreamEvent::Incomplete {
1421                response: ResponseSummary {
1422                    status_details: Some(ResponseStatusDetails {
1423                        reason: Some("max_output_tokens".into()),
1424                        r#type: Some("incomplete".into()),
1425                        error: None,
1426                    }),
1427                    output: vec![response_item_function_call(
1428                        "item_fn",
1429                        Some("{\"city\":\"Boston\"}"),
1430                    )],
1431                    ..Default::default()
1432                },
1433            },
1434        ];
1435
1436        let mapped = map_response_events(events);
1437        assert_eq!(mapped.len(), 2);
1438        assert!(
1439            matches!(mapped[0], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"city\":\"Boston\"}")
1440        );
1441        assert!(matches!(
1442            mapped[1],
1443            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1444        ));
1445    }
1446
1447    #[test]
1448    fn responses_stream_handles_empty_tool_arguments() {
1449        let events = vec![
1450            ResponsesStreamEvent::OutputItemAdded {
1451                output_index: 0,
1452                sequence_number: None,
1453                item: response_item_function_call("item_fn", Some("")),
1454            },
1455            ResponsesStreamEvent::FunctionCallArgumentsDone {
1456                item_id: "item_fn".into(),
1457                output_index: 0,
1458                arguments: "".into(),
1459                sequence_number: None,
1460            },
1461            ResponsesStreamEvent::Completed {
1462                response: ResponseSummary::default(),
1463            },
1464        ];
1465
1466        let mapped = map_response_events(events);
1467        assert_eq!(mapped.len(), 2);
1468        assert!(matches!(
1469            &mapped[0],
1470            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1471                id, name, raw_input, input, ..
1472            }) if id.to_string() == "call_123"
1473                && name.as_ref() == "get_weather"
1474                && raw_input == ""
1475                && input.is_object()
1476                && input.as_object().unwrap().is_empty()
1477        ));
1478        assert!(matches!(
1479            mapped[1],
1480            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1481        ));
1482    }
1483
1484    #[test]
1485    fn responses_stream_emits_partial_tool_use_events() {
1486        let events = vec![
1487            ResponsesStreamEvent::OutputItemAdded {
1488                output_index: 0,
1489                sequence_number: None,
1490                item: ResponseOutputItem::FunctionCall(
1491                    crate::responses::ResponseFunctionToolCall {
1492                        id: Some("item_fn".to_string()),
1493                        status: Some("in_progress".to_string()),
1494                        name: Some("get_weather".to_string()),
1495                        call_id: Some("call_abc".to_string()),
1496                        arguments: String::new(),
1497                    },
1498                ),
1499            },
1500            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1501                item_id: "item_fn".into(),
1502                output_index: 0,
1503                delta: "{\"city\":\"Bos".into(),
1504                sequence_number: None,
1505            },
1506            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1507                item_id: "item_fn".into(),
1508                output_index: 0,
1509                delta: "ton\"}".into(),
1510                sequence_number: None,
1511            },
1512            ResponsesStreamEvent::FunctionCallArgumentsDone {
1513                item_id: "item_fn".into(),
1514                output_index: 0,
1515                arguments: "{\"city\":\"Boston\"}".into(),
1516                sequence_number: None,
1517            },
1518            ResponsesStreamEvent::Completed {
1519                response: ResponseSummary::default(),
1520            },
1521        ];
1522
1523        let mapped = map_response_events(events);
1524        assert!(mapped.len() >= 3);
1525
1526        let complete_tool_use = mapped.iter().find(|e| {
1527            matches!(
1528                e,
1529                LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1530                    is_input_complete: true,
1531                    ..
1532                })
1533            )
1534        });
1535        assert!(
1536            complete_tool_use.is_some(),
1537            "should have a complete tool use event"
1538        );
1539
1540        let tool_uses: Vec<_> = mapped
1541            .iter()
1542            .filter(|e| matches!(e, LanguageModelCompletionEvent::ToolUse(_)))
1543            .collect();
1544        assert!(
1545            tool_uses.len() >= 2,
1546            "should have at least one partial and one complete event"
1547        );
1548        assert!(matches!(
1549            tool_uses.last().unwrap(),
1550            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1551                is_input_complete: true,
1552                ..
1553            })
1554        ));
1555    }
1556
1557    #[test]
1558    fn responses_stream_maps_reasoning_summary_deltas() {
1559        let events = vec![
1560            ResponsesStreamEvent::OutputItemAdded {
1561                output_index: 0,
1562                sequence_number: None,
1563                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1564                    id: Some("rs_123".into()),
1565                    summary: vec![],
1566                }),
1567            },
1568            ResponsesStreamEvent::ReasoningSummaryPartAdded {
1569                item_id: "rs_123".into(),
1570                output_index: 0,
1571                summary_index: 0,
1572            },
1573            ResponsesStreamEvent::ReasoningSummaryTextDelta {
1574                item_id: "rs_123".into(),
1575                output_index: 0,
1576                delta: "Thinking about".into(),
1577            },
1578            ResponsesStreamEvent::ReasoningSummaryTextDelta {
1579                item_id: "rs_123".into(),
1580                output_index: 0,
1581                delta: " the answer".into(),
1582            },
1583            ResponsesStreamEvent::ReasoningSummaryTextDone {
1584                item_id: "rs_123".into(),
1585                output_index: 0,
1586                text: "Thinking about the answer".into(),
1587            },
1588            ResponsesStreamEvent::ReasoningSummaryPartDone {
1589                item_id: "rs_123".into(),
1590                output_index: 0,
1591                summary_index: 0,
1592            },
1593            ResponsesStreamEvent::ReasoningSummaryPartAdded {
1594                item_id: "rs_123".into(),
1595                output_index: 0,
1596                summary_index: 1,
1597            },
1598            ResponsesStreamEvent::ReasoningSummaryTextDelta {
1599                item_id: "rs_123".into(),
1600                output_index: 0,
1601                delta: "Second part".into(),
1602            },
1603            ResponsesStreamEvent::ReasoningSummaryTextDone {
1604                item_id: "rs_123".into(),
1605                output_index: 0,
1606                text: "Second part".into(),
1607            },
1608            ResponsesStreamEvent::ReasoningSummaryPartDone {
1609                item_id: "rs_123".into(),
1610                output_index: 0,
1611                summary_index: 1,
1612            },
1613            ResponsesStreamEvent::OutputItemDone {
1614                output_index: 0,
1615                sequence_number: None,
1616                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1617                    id: Some("rs_123".into()),
1618                    summary: vec![
1619                        ReasoningSummaryPart::SummaryText {
1620                            text: "Thinking about the answer".into(),
1621                        },
1622                        ReasoningSummaryPart::SummaryText {
1623                            text: "Second part".into(),
1624                        },
1625                    ],
1626                }),
1627            },
1628            ResponsesStreamEvent::OutputItemAdded {
1629                output_index: 1,
1630                sequence_number: None,
1631                item: response_item_message("msg_456"),
1632            },
1633            ResponsesStreamEvent::OutputTextDelta {
1634                item_id: "msg_456".into(),
1635                output_index: 1,
1636                content_index: Some(0),
1637                delta: "The answer is 42".into(),
1638            },
1639            ResponsesStreamEvent::Completed {
1640                response: ResponseSummary::default(),
1641            },
1642        ];
1643
1644        let mapped = map_response_events(events);
1645
1646        let thinking_events: Vec<_> = mapped
1647            .iter()
1648            .filter(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. }))
1649            .collect();
1650        assert_eq!(
1651            thinking_events.len(),
1652            4,
1653            "expected 4 thinking events, got {:?}",
1654            thinking_events
1655        );
1656        assert!(
1657            matches!(&thinking_events[0], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Thinking about")
1658        );
1659        assert!(
1660            matches!(&thinking_events[1], LanguageModelCompletionEvent::Thinking { text, .. } if text == " the answer")
1661        );
1662        assert!(
1663            matches!(&thinking_events[2], LanguageModelCompletionEvent::Thinking { text, .. } if text == "\n\n"),
1664            "expected separator between summary parts"
1665        );
1666        assert!(
1667            matches!(&thinking_events[3], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Second part")
1668        );
1669
1670        assert!(mapped.iter().any(
1671            |e| matches!(e, LanguageModelCompletionEvent::Text(t) if t == "The answer is 42")
1672        ));
1673    }
1674
1675    #[test]
1676    fn responses_stream_maps_reasoning_from_done_only() {
1677        let events = vec![
1678            ResponsesStreamEvent::OutputItemAdded {
1679                output_index: 0,
1680                sequence_number: None,
1681                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1682                    id: Some("rs_789".into()),
1683                    summary: vec![],
1684                }),
1685            },
1686            ResponsesStreamEvent::OutputItemDone {
1687                output_index: 0,
1688                sequence_number: None,
1689                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1690                    id: Some("rs_789".into()),
1691                    summary: vec![ReasoningSummaryPart::SummaryText {
1692                        text: "Summary without deltas".into(),
1693                    }],
1694                }),
1695            },
1696            ResponsesStreamEvent::Completed {
1697                response: ResponseSummary::default(),
1698            },
1699        ];
1700
1701        let mapped = map_response_events(events);
1702        assert!(
1703            !mapped
1704                .iter()
1705                .any(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. })),
1706            "OutputItemDone reasoning should not produce Thinking events"
1707        );
1708    }
1709
1710    #[test]
1711    fn into_open_ai_interleaved_reasoning() {
1712        let tool_use_id = LanguageModelToolUseId::from("call-1");
1713        let tool_input = json!({"query": "foo"});
1714        let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1715        let tool_use = LanguageModelToolUse {
1716            id: tool_use_id.clone(),
1717            name: Arc::from("search"),
1718            raw_input: tool_arguments.clone(),
1719            input: tool_input,
1720            is_input_complete: true,
1721            thought_signature: None,
1722        };
1723        let tool_result = LanguageModelToolResult {
1724            tool_use_id: tool_use_id,
1725            tool_name: Arc::from("search"),
1726            is_error: false,
1727            content: LanguageModelToolResultContent::Text(Arc::from("result")),
1728            output: None,
1729        };
1730        let request = LanguageModelRequest {
1731            thread_id: None,
1732            prompt_id: None,
1733            intent: None,
1734            messages: vec![
1735                LanguageModelRequestMessage {
1736                    role: Role::User,
1737                    content: vec![MessageContent::Text("search for something".into())],
1738                    cache: false,
1739                    reasoning_details: None,
1740                },
1741                LanguageModelRequestMessage {
1742                    role: Role::Assistant,
1743                    content: vec![
1744                        MessageContent::Thinking {
1745                            text: "I should search".into(),
1746                            signature: None,
1747                        },
1748                        MessageContent::Text("Searching now.".into()),
1749                        MessageContent::ToolUse(tool_use),
1750                    ],
1751                    cache: false,
1752                    reasoning_details: None,
1753                },
1754                LanguageModelRequestMessage {
1755                    role: Role::Assistant,
1756                    content: vec![MessageContent::ToolResult(tool_result)],
1757                    cache: false,
1758                    reasoning_details: None,
1759                },
1760            ],
1761            tools: vec![],
1762            tool_choice: None,
1763            stop: vec![],
1764            temperature: None,
1765            thinking_allowed: true,
1766            thinking_effort: None,
1767            speed: None,
1768        };
1769
1770        let result = into_open_ai(request.clone(), "model", false, false, None, None, true);
1771        assert_eq!(
1772            serde_json::to_value(&result).unwrap()["messages"],
1773            json!([
1774                {"role": "user", "content": "search for something"},
1775                {
1776                    "role": "assistant",
1777                    "content": "Searching now.",
1778                    "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}],
1779                    "reasoning_content": "I should search"
1780                },
1781                {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1782            ])
1783        );
1784
1785        let result = into_open_ai(request, "model", false, false, None, None, false);
1786        assert_eq!(
1787            serde_json::to_value(&result).unwrap()["messages"],
1788            json!([
1789                {"role": "user", "content": "search for something"},
1790                {
1791                    "role": "assistant",
1792                    "content": [
1793                        {"type": "text", "text": "I should search"},
1794                        {"type": "text", "text": "Searching now."}
1795                    ],
1796                    "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}]
1797                },
1798                {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1799            ])
1800        );
1801    }
1802}