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