open_ai.rs

   1use anyhow::{Result, anyhow};
   2use collections::{BTreeMap, HashMap};
   3use credentials_provider::CredentialsProvider;
   4use futures::Stream;
   5use futures::{FutureExt, StreamExt, future::BoxFuture};
   6use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
   7use http_client::HttpClient;
   8use language_model::{
   9    ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
  10    LanguageModelCompletionEvent, LanguageModelId, LanguageModelImage, LanguageModelName,
  11    LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
  12    LanguageModelProviderState, LanguageModelRequest, LanguageModelRequestMessage,
  13    LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse,
  14    LanguageModelToolUseId, MessageContent, OPEN_AI_PROVIDER_ID, OPEN_AI_PROVIDER_NAME,
  15    RateLimiter, Role, StopReason, TokenUsage, env_var,
  16};
  17use menu;
  18use open_ai::responses::{
  19    ResponseFunctionCallItem, ResponseFunctionCallOutputContent, ResponseFunctionCallOutputItem,
  20    ResponseInputContent, ResponseInputItem, ResponseMessageItem,
  21};
  22use open_ai::{
  23    ImageUrl, Model, OPEN_AI_API_URL, ReasoningEffort, ResponseStreamEvent,
  24    responses::{
  25        Request as ResponseRequest, ResponseOutputItem, ResponseSummary as ResponsesSummary,
  26        ResponseUsage as ResponsesUsage, StreamEvent as ResponsesStreamEvent, stream_response,
  27    },
  28    stream_completion,
  29};
  30use settings::{OpenAiAvailableModel as AvailableModel, Settings, SettingsStore};
  31use std::pin::Pin;
  32use std::sync::{Arc, LazyLock};
  33use strum::IntoEnumIterator;
  34use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
  35use ui_input::InputField;
  36use util::ResultExt;
  37
  38use crate::provider::util::{fix_streamed_json, parse_tool_arguments};
  39
  40const PROVIDER_ID: LanguageModelProviderId = OPEN_AI_PROVIDER_ID;
  41const PROVIDER_NAME: LanguageModelProviderName = OPEN_AI_PROVIDER_NAME;
  42
  43const API_KEY_ENV_VAR_NAME: &str = "OPENAI_API_KEY";
  44static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
  45
  46#[derive(Default, Clone, Debug, PartialEq)]
  47pub struct OpenAiSettings {
  48    pub api_url: String,
  49    pub available_models: Vec<AvailableModel>,
  50}
  51
  52pub struct OpenAiLanguageModelProvider {
  53    http_client: Arc<dyn HttpClient>,
  54    state: Entity<State>,
  55}
  56
  57pub struct State {
  58    api_key_state: ApiKeyState,
  59    credentials_provider: Arc<dyn CredentialsProvider>,
  60}
  61
  62impl State {
  63    fn is_authenticated(&self) -> bool {
  64        self.api_key_state.has_key()
  65    }
  66
  67    fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
  68        let credentials_provider = self.credentials_provider.clone();
  69        let api_url = OpenAiLanguageModelProvider::api_url(cx);
  70        self.api_key_state.store(
  71            api_url,
  72            api_key,
  73            |this| &mut this.api_key_state,
  74            credentials_provider,
  75            cx,
  76        )
  77    }
  78
  79    fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
  80        let credentials_provider = self.credentials_provider.clone();
  81        let api_url = OpenAiLanguageModelProvider::api_url(cx);
  82        self.api_key_state.load_if_needed(
  83            api_url,
  84            |this| &mut this.api_key_state,
  85            credentials_provider,
  86            cx,
  87        )
  88    }
  89}
  90
  91impl OpenAiLanguageModelProvider {
  92    pub fn new(
  93        http_client: Arc<dyn HttpClient>,
  94        credentials_provider: Arc<dyn CredentialsProvider>,
  95        cx: &mut App,
  96    ) -> Self {
  97        let state = cx.new(|cx| {
  98            cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
  99                let credentials_provider = this.credentials_provider.clone();
 100                let api_url = Self::api_url(cx);
 101                this.api_key_state.handle_url_change(
 102                    api_url,
 103                    |this| &mut this.api_key_state,
 104                    credentials_provider,
 105                    cx,
 106                );
 107                cx.notify();
 108            })
 109            .detach();
 110            State {
 111                api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
 112                credentials_provider,
 113            }
 114        });
 115
 116        Self { http_client, state }
 117    }
 118
 119    fn create_language_model(&self, model: open_ai::Model) -> Arc<dyn LanguageModel> {
 120        Arc::new(OpenAiLanguageModel {
 121            id: LanguageModelId::from(model.id().to_string()),
 122            model,
 123            state: self.state.clone(),
 124            http_client: self.http_client.clone(),
 125            request_limiter: RateLimiter::new(4),
 126        })
 127    }
 128
 129    fn settings(cx: &App) -> &OpenAiSettings {
 130        &crate::AllLanguageModelSettings::get_global(cx).openai
 131    }
 132
 133    fn api_url(cx: &App) -> SharedString {
 134        let api_url = &Self::settings(cx).api_url;
 135        if api_url.is_empty() {
 136            open_ai::OPEN_AI_API_URL.into()
 137        } else {
 138            SharedString::new(api_url.as_str())
 139        }
 140    }
 141}
 142
 143impl LanguageModelProviderState for OpenAiLanguageModelProvider {
 144    type ObservableEntity = State;
 145
 146    fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
 147        Some(self.state.clone())
 148    }
 149}
 150
 151impl LanguageModelProvider for OpenAiLanguageModelProvider {
 152    fn id(&self) -> LanguageModelProviderId {
 153        PROVIDER_ID
 154    }
 155
 156    fn name(&self) -> LanguageModelProviderName {
 157        PROVIDER_NAME
 158    }
 159
 160    fn icon(&self) -> IconOrSvg {
 161        IconOrSvg::Icon(IconName::AiOpenAi)
 162    }
 163
 164    fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
 165        Some(self.create_language_model(open_ai::Model::default()))
 166    }
 167
 168    fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
 169        Some(self.create_language_model(open_ai::Model::default_fast()))
 170    }
 171
 172    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
 173        let mut models = BTreeMap::default();
 174
 175        // Add base models from open_ai::Model::iter()
 176        for model in open_ai::Model::iter() {
 177            if !matches!(model, open_ai::Model::Custom { .. }) {
 178                models.insert(model.id().to_string(), model);
 179            }
 180        }
 181
 182        // Override with available models from settings
 183        for model in &OpenAiLanguageModelProvider::settings(cx).available_models {
 184            models.insert(
 185                model.name.clone(),
 186                open_ai::Model::Custom {
 187                    name: model.name.clone(),
 188                    display_name: model.display_name.clone(),
 189                    max_tokens: model.max_tokens,
 190                    max_output_tokens: model.max_output_tokens,
 191                    max_completion_tokens: model.max_completion_tokens,
 192                    reasoning_effort: model.reasoning_effort.clone(),
 193                    supports_chat_completions: model.capabilities.chat_completions,
 194                },
 195            );
 196        }
 197
 198        models
 199            .into_values()
 200            .map(|model| self.create_language_model(model))
 201            .collect()
 202    }
 203
 204    fn is_authenticated(&self, cx: &App) -> bool {
 205        self.state.read(cx).is_authenticated()
 206    }
 207
 208    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
 209        self.state.update(cx, |state, cx| state.authenticate(cx))
 210    }
 211
 212    fn configuration_view(
 213        &self,
 214        _target_agent: language_model::ConfigurationViewTargetAgent,
 215        window: &mut Window,
 216        cx: &mut App,
 217    ) -> AnyView {
 218        cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
 219            .into()
 220    }
 221
 222    fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
 223        self.state
 224            .update(cx, |state, cx| state.set_api_key(None, cx))
 225    }
 226}
 227
 228pub struct OpenAiLanguageModel {
 229    id: LanguageModelId,
 230    model: open_ai::Model,
 231    state: Entity<State>,
 232    http_client: Arc<dyn HttpClient>,
 233    request_limiter: RateLimiter,
 234}
 235
 236impl OpenAiLanguageModel {
 237    fn stream_completion(
 238        &self,
 239        request: open_ai::Request,
 240        cx: &AsyncApp,
 241    ) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>>>
 242    {
 243        let http_client = self.http_client.clone();
 244
 245        let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
 246            let api_url = OpenAiLanguageModelProvider::api_url(cx);
 247            (state.api_key_state.key(&api_url), api_url)
 248        });
 249
 250        let future = self.request_limiter.stream(async move {
 251            let provider = PROVIDER_NAME;
 252            let Some(api_key) = api_key else {
 253                return Err(LanguageModelCompletionError::NoApiKey { provider });
 254            };
 255            let request = stream_completion(
 256                http_client.as_ref(),
 257                provider.0.as_str(),
 258                &api_url,
 259                &api_key,
 260                request,
 261            );
 262            let response = request.await?;
 263            Ok(response)
 264        });
 265
 266        async move { Ok(future.await?.boxed()) }.boxed()
 267    }
 268
 269    fn stream_response(
 270        &self,
 271        request: ResponseRequest,
 272        cx: &AsyncApp,
 273    ) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponsesStreamEvent>>>>
 274    {
 275        let http_client = self.http_client.clone();
 276
 277        let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
 278            let api_url = OpenAiLanguageModelProvider::api_url(cx);
 279            (state.api_key_state.key(&api_url), api_url)
 280        });
 281
 282        let provider = PROVIDER_NAME;
 283        let future = self.request_limiter.stream(async move {
 284            let Some(api_key) = api_key else {
 285                return Err(LanguageModelCompletionError::NoApiKey { provider });
 286            };
 287            let request = stream_response(
 288                http_client.as_ref(),
 289                provider.0.as_str(),
 290                &api_url,
 291                &api_key,
 292                request,
 293            );
 294            let response = request.await?;
 295            Ok(response)
 296        });
 297
 298        async move { Ok(future.await?.boxed()) }.boxed()
 299    }
 300}
 301
 302impl LanguageModel for OpenAiLanguageModel {
 303    fn id(&self) -> LanguageModelId {
 304        self.id.clone()
 305    }
 306
 307    fn name(&self) -> LanguageModelName {
 308        LanguageModelName::from(self.model.display_name().to_string())
 309    }
 310
 311    fn provider_id(&self) -> LanguageModelProviderId {
 312        PROVIDER_ID
 313    }
 314
 315    fn provider_name(&self) -> LanguageModelProviderName {
 316        PROVIDER_NAME
 317    }
 318
 319    fn supports_tools(&self) -> bool {
 320        true
 321    }
 322
 323    fn supports_images(&self) -> bool {
 324        use open_ai::Model;
 325        match &self.model {
 326            Model::FourOmniMini
 327            | Model::FourPointOneNano
 328            | Model::Five
 329            | Model::FiveCodex
 330            | Model::FiveMini
 331            | Model::FiveNano
 332            | Model::FivePointOne
 333            | Model::FivePointTwo
 334            | Model::FivePointTwoCodex
 335            | Model::FivePointThreeCodex
 336            | Model::FivePointFour
 337            | Model::FivePointFourPro
 338            | Model::O1
 339            | Model::O3 => true,
 340            Model::ThreePointFiveTurbo
 341            | Model::Four
 342            | Model::FourTurbo
 343            | Model::O3Mini
 344            | Model::Custom { .. } => false,
 345        }
 346    }
 347
 348    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
 349        match choice {
 350            LanguageModelToolChoice::Auto => true,
 351            LanguageModelToolChoice::Any => true,
 352            LanguageModelToolChoice::None => true,
 353        }
 354    }
 355
 356    fn supports_streaming_tools(&self) -> bool {
 357        true
 358    }
 359
 360    fn supports_thinking(&self) -> bool {
 361        self.model.reasoning_effort().is_some()
 362    }
 363
 364    fn supports_split_token_display(&self) -> bool {
 365        true
 366    }
 367
 368    fn telemetry_id(&self) -> String {
 369        format!("openai/{}", self.model.id())
 370    }
 371
 372    fn max_token_count(&self) -> u64 {
 373        self.model.max_token_count()
 374    }
 375
 376    fn max_output_tokens(&self) -> Option<u64> {
 377        self.model.max_output_tokens()
 378    }
 379
 380    fn count_tokens(
 381        &self,
 382        request: LanguageModelRequest,
 383        cx: &App,
 384    ) -> BoxFuture<'static, Result<u64>> {
 385        count_open_ai_tokens(request, self.model.clone(), cx)
 386    }
 387
 388    fn stream_completion(
 389        &self,
 390        request: LanguageModelRequest,
 391        cx: &AsyncApp,
 392    ) -> BoxFuture<
 393        'static,
 394        Result<
 395            futures::stream::BoxStream<
 396                'static,
 397                Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
 398            >,
 399            LanguageModelCompletionError,
 400        >,
 401    > {
 402        if self.model.supports_chat_completions() {
 403            let request = into_open_ai(
 404                request,
 405                self.model.id(),
 406                self.model.supports_parallel_tool_calls(),
 407                self.model.supports_prompt_cache_key(),
 408                self.max_output_tokens(),
 409                self.model.reasoning_effort(),
 410            );
 411            let completions = self.stream_completion(request, cx);
 412            async move {
 413                let mapper = OpenAiEventMapper::new();
 414                Ok(mapper.map_stream(completions.await?).boxed())
 415            }
 416            .boxed()
 417        } else {
 418            let request = into_open_ai_response(
 419                request,
 420                self.model.id(),
 421                self.model.supports_parallel_tool_calls(),
 422                self.model.supports_prompt_cache_key(),
 423                self.max_output_tokens(),
 424                self.model.reasoning_effort(),
 425            );
 426            let completions = self.stream_response(request, cx);
 427            async move {
 428                let mapper = OpenAiResponseEventMapper::new();
 429                Ok(mapper.map_stream(completions.await?).boxed())
 430            }
 431            .boxed()
 432        }
 433    }
 434}
 435
 436pub fn into_open_ai(
 437    request: LanguageModelRequest,
 438    model_id: &str,
 439    supports_parallel_tool_calls: bool,
 440    supports_prompt_cache_key: bool,
 441    max_output_tokens: Option<u64>,
 442    reasoning_effort: Option<ReasoningEffort>,
 443) -> open_ai::Request {
 444    let stream = !model_id.starts_with("o1-");
 445
 446    let mut messages = Vec::new();
 447    for message in request.messages {
 448        for content in message.content {
 449            match content {
 450                MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
 451                    let should_add = if message.role == Role::User {
 452                        // Including whitespace-only user messages can cause error with OpenAI compatible APIs
 453                        // See https://github.com/zed-industries/zed/issues/40097
 454                        !text.trim().is_empty()
 455                    } else {
 456                        !text.is_empty()
 457                    };
 458                    if should_add {
 459                        add_message_content_part(
 460                            open_ai::MessagePart::Text { text },
 461                            message.role,
 462                            &mut messages,
 463                        );
 464                    }
 465                }
 466                MessageContent::RedactedThinking(_) => {}
 467                MessageContent::Image(image) => {
 468                    add_message_content_part(
 469                        open_ai::MessagePart::Image {
 470                            image_url: ImageUrl {
 471                                url: image.to_base64_url(),
 472                                detail: None,
 473                            },
 474                        },
 475                        message.role,
 476                        &mut messages,
 477                    );
 478                }
 479                MessageContent::ToolUse(tool_use) => {
 480                    let tool_call = open_ai::ToolCall {
 481                        id: tool_use.id.to_string(),
 482                        content: open_ai::ToolCallContent::Function {
 483                            function: open_ai::FunctionContent {
 484                                name: tool_use.name.to_string(),
 485                                arguments: serde_json::to_string(&tool_use.input)
 486                                    .unwrap_or_default(),
 487                            },
 488                        },
 489                    };
 490
 491                    if let Some(open_ai::RequestMessage::Assistant { tool_calls, .. }) =
 492                        messages.last_mut()
 493                    {
 494                        tool_calls.push(tool_call);
 495                    } else {
 496                        messages.push(open_ai::RequestMessage::Assistant {
 497                            content: None,
 498                            tool_calls: vec![tool_call],
 499                        });
 500                    }
 501                }
 502                MessageContent::ToolResult(tool_result) => {
 503                    let content = match &tool_result.content {
 504                        LanguageModelToolResultContent::Text(text) => {
 505                            vec![open_ai::MessagePart::Text {
 506                                text: text.to_string(),
 507                            }]
 508                        }
 509                        LanguageModelToolResultContent::Image(image) => {
 510                            vec![open_ai::MessagePart::Image {
 511                                image_url: ImageUrl {
 512                                    url: image.to_base64_url(),
 513                                    detail: None,
 514                                },
 515                            }]
 516                        }
 517                    };
 518
 519                    messages.push(open_ai::RequestMessage::Tool {
 520                        content: content.into(),
 521                        tool_call_id: tool_result.tool_use_id.to_string(),
 522                    });
 523                }
 524            }
 525        }
 526    }
 527
 528    open_ai::Request {
 529        model: model_id.into(),
 530        messages,
 531        stream,
 532        stream_options: if stream {
 533            Some(open_ai::StreamOptions::default())
 534        } else {
 535            None
 536        },
 537        stop: request.stop,
 538        temperature: request.temperature.or(Some(1.0)),
 539        max_completion_tokens: max_output_tokens,
 540        parallel_tool_calls: if supports_parallel_tool_calls && !request.tools.is_empty() {
 541            Some(supports_parallel_tool_calls)
 542        } else {
 543            None
 544        },
 545        prompt_cache_key: if supports_prompt_cache_key {
 546            request.thread_id
 547        } else {
 548            None
 549        },
 550        tools: request
 551            .tools
 552            .into_iter()
 553            .map(|tool| open_ai::ToolDefinition::Function {
 554                function: open_ai::FunctionDefinition {
 555                    name: tool.name,
 556                    description: Some(tool.description),
 557                    parameters: Some(tool.input_schema),
 558                },
 559            })
 560            .collect(),
 561        tool_choice: request.tool_choice.map(|choice| match choice {
 562            LanguageModelToolChoice::Auto => open_ai::ToolChoice::Auto,
 563            LanguageModelToolChoice::Any => open_ai::ToolChoice::Required,
 564            LanguageModelToolChoice::None => open_ai::ToolChoice::None,
 565        }),
 566        reasoning_effort,
 567    }
 568}
 569
 570pub fn into_open_ai_response(
 571    request: LanguageModelRequest,
 572    model_id: &str,
 573    supports_parallel_tool_calls: bool,
 574    supports_prompt_cache_key: bool,
 575    max_output_tokens: Option<u64>,
 576    reasoning_effort: Option<ReasoningEffort>,
 577) -> ResponseRequest {
 578    let stream = !model_id.starts_with("o1-");
 579
 580    let LanguageModelRequest {
 581        thread_id,
 582        prompt_id: _,
 583        intent: _,
 584        messages,
 585        tools,
 586        tool_choice,
 587        stop: _,
 588        temperature,
 589        thinking_allowed: _,
 590        thinking_effort: _,
 591        speed: _,
 592    } = request;
 593
 594    let mut input_items = Vec::new();
 595    for (index, message) in messages.into_iter().enumerate() {
 596        append_message_to_response_items(message, index, &mut input_items);
 597    }
 598
 599    let tools: Vec<_> = tools
 600        .into_iter()
 601        .map(|tool| open_ai::responses::ToolDefinition::Function {
 602            name: tool.name,
 603            description: Some(tool.description),
 604            parameters: Some(tool.input_schema),
 605            strict: None,
 606        })
 607        .collect();
 608
 609    ResponseRequest {
 610        model: model_id.into(),
 611        input: input_items,
 612        stream,
 613        temperature,
 614        top_p: None,
 615        max_output_tokens,
 616        parallel_tool_calls: if tools.is_empty() {
 617            None
 618        } else {
 619            Some(supports_parallel_tool_calls)
 620        },
 621        tool_choice: tool_choice.map(|choice| match choice {
 622            LanguageModelToolChoice::Auto => open_ai::ToolChoice::Auto,
 623            LanguageModelToolChoice::Any => open_ai::ToolChoice::Required,
 624            LanguageModelToolChoice::None => open_ai::ToolChoice::None,
 625        }),
 626        tools,
 627        prompt_cache_key: if supports_prompt_cache_key {
 628            thread_id
 629        } else {
 630            None
 631        },
 632        reasoning: reasoning_effort.map(|effort| open_ai::responses::ReasoningConfig {
 633            effort,
 634            summary: Some(open_ai::responses::ReasoningSummaryMode::Auto),
 635        }),
 636    }
 637}
 638
 639fn append_message_to_response_items(
 640    message: LanguageModelRequestMessage,
 641    index: usize,
 642    input_items: &mut Vec<ResponseInputItem>,
 643) {
 644    let mut content_parts: Vec<ResponseInputContent> = Vec::new();
 645
 646    for content in message.content {
 647        match content {
 648            MessageContent::Text(text) => {
 649                push_response_text_part(&message.role, text, &mut content_parts);
 650            }
 651            MessageContent::Thinking { text, .. } => {
 652                push_response_text_part(&message.role, text, &mut content_parts);
 653            }
 654            MessageContent::RedactedThinking(_) => {}
 655            MessageContent::Image(image) => {
 656                push_response_image_part(&message.role, image, &mut content_parts);
 657            }
 658            MessageContent::ToolUse(tool_use) => {
 659                flush_response_parts(&message.role, index, &mut content_parts, input_items);
 660                let call_id = tool_use.id.to_string();
 661                input_items.push(ResponseInputItem::FunctionCall(ResponseFunctionCallItem {
 662                    call_id,
 663                    name: tool_use.name.to_string(),
 664                    arguments: tool_use.raw_input,
 665                }));
 666            }
 667            MessageContent::ToolResult(tool_result) => {
 668                flush_response_parts(&message.role, index, &mut content_parts, input_items);
 669                input_items.push(ResponseInputItem::FunctionCallOutput(
 670                    ResponseFunctionCallOutputItem {
 671                        call_id: tool_result.tool_use_id.to_string(),
 672                        output: match tool_result.content {
 673                            LanguageModelToolResultContent::Text(text) => {
 674                                ResponseFunctionCallOutputContent::Text(text.to_string())
 675                            }
 676                            LanguageModelToolResultContent::Image(image) => {
 677                                ResponseFunctionCallOutputContent::List(vec![
 678                                    ResponseInputContent::Image {
 679                                        image_url: image.to_base64_url(),
 680                                    },
 681                                ])
 682                            }
 683                        },
 684                    },
 685                ));
 686            }
 687        }
 688    }
 689
 690    flush_response_parts(&message.role, index, &mut content_parts, input_items);
 691}
 692
 693fn push_response_text_part(
 694    role: &Role,
 695    text: impl Into<String>,
 696    parts: &mut Vec<ResponseInputContent>,
 697) {
 698    let text = text.into();
 699    if text.trim().is_empty() {
 700        return;
 701    }
 702
 703    match role {
 704        Role::Assistant => parts.push(ResponseInputContent::OutputText {
 705            text,
 706            annotations: Vec::new(),
 707        }),
 708        _ => parts.push(ResponseInputContent::Text { text }),
 709    }
 710}
 711
 712fn push_response_image_part(
 713    role: &Role,
 714    image: LanguageModelImage,
 715    parts: &mut Vec<ResponseInputContent>,
 716) {
 717    match role {
 718        Role::Assistant => parts.push(ResponseInputContent::OutputText {
 719            text: "[image omitted]".to_string(),
 720            annotations: Vec::new(),
 721        }),
 722        _ => parts.push(ResponseInputContent::Image {
 723            image_url: image.to_base64_url(),
 724        }),
 725    }
 726}
 727
 728fn flush_response_parts(
 729    role: &Role,
 730    _index: usize,
 731    parts: &mut Vec<ResponseInputContent>,
 732    input_items: &mut Vec<ResponseInputItem>,
 733) {
 734    if parts.is_empty() {
 735        return;
 736    }
 737
 738    let item = ResponseInputItem::Message(ResponseMessageItem {
 739        role: match role {
 740            Role::User => open_ai::Role::User,
 741            Role::Assistant => open_ai::Role::Assistant,
 742            Role::System => open_ai::Role::System,
 743        },
 744        content: parts.clone(),
 745    });
 746
 747    input_items.push(item);
 748    parts.clear();
 749}
 750
 751fn add_message_content_part(
 752    new_part: open_ai::MessagePart,
 753    role: Role,
 754    messages: &mut Vec<open_ai::RequestMessage>,
 755) {
 756    match (role, messages.last_mut()) {
 757        (Role::User, Some(open_ai::RequestMessage::User { content }))
 758        | (
 759            Role::Assistant,
 760            Some(open_ai::RequestMessage::Assistant {
 761                content: Some(content),
 762                ..
 763            }),
 764        )
 765        | (Role::System, Some(open_ai::RequestMessage::System { content, .. })) => {
 766            content.push_part(new_part);
 767        }
 768        _ => {
 769            messages.push(match role {
 770                Role::User => open_ai::RequestMessage::User {
 771                    content: open_ai::MessageContent::from(vec![new_part]),
 772                },
 773                Role::Assistant => open_ai::RequestMessage::Assistant {
 774                    content: Some(open_ai::MessageContent::from(vec![new_part])),
 775                    tool_calls: Vec::new(),
 776                },
 777                Role::System => open_ai::RequestMessage::System {
 778                    content: open_ai::MessageContent::from(vec![new_part]),
 779                },
 780            });
 781        }
 782    }
 783}
 784
 785pub struct OpenAiEventMapper {
 786    tool_calls_by_index: HashMap<usize, RawToolCall>,
 787}
 788
 789impl OpenAiEventMapper {
 790    pub fn new() -> Self {
 791        Self {
 792            tool_calls_by_index: HashMap::default(),
 793        }
 794    }
 795
 796    pub fn map_stream(
 797        mut self,
 798        events: Pin<Box<dyn Send + Stream<Item = Result<ResponseStreamEvent>>>>,
 799    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
 800    {
 801        events.flat_map(move |event| {
 802            futures::stream::iter(match event {
 803                Ok(event) => self.map_event(event),
 804                Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
 805            })
 806        })
 807    }
 808
 809    pub fn map_event(
 810        &mut self,
 811        event: ResponseStreamEvent,
 812    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 813        let mut events = Vec::new();
 814        if let Some(usage) = event.usage {
 815            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
 816                input_tokens: usage.prompt_tokens,
 817                output_tokens: usage.completion_tokens,
 818                cache_creation_input_tokens: 0,
 819                cache_read_input_tokens: 0,
 820            })));
 821        }
 822
 823        let Some(choice) = event.choices.first() else {
 824            return events;
 825        };
 826
 827        if let Some(delta) = choice.delta.as_ref() {
 828            if let Some(reasoning_content) = delta.reasoning_content.clone() {
 829                if !reasoning_content.is_empty() {
 830                    events.push(Ok(LanguageModelCompletionEvent::Thinking {
 831                        text: reasoning_content,
 832                        signature: None,
 833                    }));
 834                }
 835            }
 836            if let Some(content) = delta.content.clone() {
 837                if !content.is_empty() {
 838                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
 839                }
 840            }
 841
 842            if let Some(tool_calls) = delta.tool_calls.as_ref() {
 843                for tool_call in tool_calls {
 844                    let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
 845
 846                    if let Some(tool_id) = tool_call.id.clone() {
 847                        entry.id = tool_id;
 848                    }
 849
 850                    if let Some(function) = tool_call.function.as_ref() {
 851                        if let Some(name) = function.name.clone() {
 852                            entry.name = name;
 853                        }
 854
 855                        if let Some(arguments) = function.arguments.clone() {
 856                            entry.arguments.push_str(&arguments);
 857                        }
 858                    }
 859
 860                    if !entry.id.is_empty() && !entry.name.is_empty() {
 861                        if let Ok(input) = serde_json::from_str::<serde_json::Value>(
 862                            &fix_streamed_json(&entry.arguments),
 863                        ) {
 864                            events.push(Ok(LanguageModelCompletionEvent::ToolUse(
 865                                LanguageModelToolUse {
 866                                    id: entry.id.clone().into(),
 867                                    name: entry.name.as_str().into(),
 868                                    is_input_complete: false,
 869                                    input,
 870                                    raw_input: entry.arguments.clone(),
 871                                    thought_signature: None,
 872                                },
 873                            )));
 874                        }
 875                    }
 876                }
 877            }
 878        }
 879
 880        match choice.finish_reason.as_deref() {
 881            Some("stop") => {
 882                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
 883            }
 884            Some("tool_calls") => {
 885                events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
 886                    match parse_tool_arguments(&tool_call.arguments) {
 887                        Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
 888                            LanguageModelToolUse {
 889                                id: tool_call.id.clone().into(),
 890                                name: tool_call.name.as_str().into(),
 891                                is_input_complete: true,
 892                                input,
 893                                raw_input: tool_call.arguments.clone(),
 894                                thought_signature: None,
 895                            },
 896                        )),
 897                        Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
 898                            id: tool_call.id.into(),
 899                            tool_name: tool_call.name.into(),
 900                            raw_input: tool_call.arguments.clone().into(),
 901                            json_parse_error: error.to_string(),
 902                        }),
 903                    }
 904                }));
 905
 906                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
 907            }
 908            Some(stop_reason) => {
 909                log::error!("Unexpected OpenAI stop_reason: {stop_reason:?}",);
 910                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
 911            }
 912            None => {}
 913        }
 914
 915        events
 916    }
 917}
 918
 919#[derive(Default)]
 920struct RawToolCall {
 921    id: String,
 922    name: String,
 923    arguments: String,
 924}
 925
 926pub struct OpenAiResponseEventMapper {
 927    function_calls_by_item: HashMap<String, PendingResponseFunctionCall>,
 928    pending_stop_reason: Option<StopReason>,
 929}
 930
 931#[derive(Default)]
 932struct PendingResponseFunctionCall {
 933    call_id: String,
 934    name: Arc<str>,
 935    arguments: String,
 936}
 937
 938impl OpenAiResponseEventMapper {
 939    pub fn new() -> Self {
 940        Self {
 941            function_calls_by_item: HashMap::default(),
 942            pending_stop_reason: None,
 943        }
 944    }
 945
 946    pub fn map_stream(
 947        mut self,
 948        events: Pin<Box<dyn Send + Stream<Item = Result<ResponsesStreamEvent>>>>,
 949    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
 950    {
 951        events.flat_map(move |event| {
 952            futures::stream::iter(match event {
 953                Ok(event) => self.map_event(event),
 954                Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
 955            })
 956        })
 957    }
 958
 959    pub fn map_event(
 960        &mut self,
 961        event: ResponsesStreamEvent,
 962    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 963        match event {
 964            ResponsesStreamEvent::OutputItemAdded { item, .. } => {
 965                let mut events = Vec::new();
 966
 967                match &item {
 968                    ResponseOutputItem::Message(message) => {
 969                        if let Some(id) = &message.id {
 970                            events.push(Ok(LanguageModelCompletionEvent::StartMessage {
 971                                message_id: id.clone(),
 972                            }));
 973                        }
 974                    }
 975                    ResponseOutputItem::FunctionCall(function_call) => {
 976                        if let Some(item_id) = function_call.id.clone() {
 977                            let call_id = function_call
 978                                .call_id
 979                                .clone()
 980                                .or_else(|| function_call.id.clone())
 981                                .unwrap_or_else(|| item_id.clone());
 982                            let entry = PendingResponseFunctionCall {
 983                                call_id,
 984                                name: Arc::<str>::from(
 985                                    function_call.name.clone().unwrap_or_default(),
 986                                ),
 987                                arguments: function_call.arguments.clone(),
 988                            };
 989                            self.function_calls_by_item.insert(item_id, entry);
 990                        }
 991                    }
 992                    ResponseOutputItem::Reasoning(_) | ResponseOutputItem::Unknown => {}
 993                }
 994                events
 995            }
 996            ResponsesStreamEvent::ReasoningSummaryTextDelta { delta, .. } => {
 997                if delta.is_empty() {
 998                    Vec::new()
 999                } else {
1000                    vec![Ok(LanguageModelCompletionEvent::Thinking {
1001                        text: delta,
1002                        signature: None,
1003                    })]
1004                }
1005            }
1006            ResponsesStreamEvent::OutputTextDelta { delta, .. } => {
1007                if delta.is_empty() {
1008                    Vec::new()
1009                } else {
1010                    vec![Ok(LanguageModelCompletionEvent::Text(delta))]
1011                }
1012            }
1013            ResponsesStreamEvent::FunctionCallArgumentsDelta { item_id, delta, .. } => {
1014                if let Some(entry) = self.function_calls_by_item.get_mut(&item_id) {
1015                    entry.arguments.push_str(&delta);
1016                    if let Ok(input) = serde_json::from_str::<serde_json::Value>(
1017                        &fix_streamed_json(&entry.arguments),
1018                    ) {
1019                        return vec![Ok(LanguageModelCompletionEvent::ToolUse(
1020                            LanguageModelToolUse {
1021                                id: LanguageModelToolUseId::from(entry.call_id.clone()),
1022                                name: entry.name.clone(),
1023                                is_input_complete: false,
1024                                input,
1025                                raw_input: entry.arguments.clone(),
1026                                thought_signature: None,
1027                            },
1028                        ))];
1029                    }
1030                }
1031                Vec::new()
1032            }
1033            ResponsesStreamEvent::FunctionCallArgumentsDone {
1034                item_id, arguments, ..
1035            } => {
1036                if let Some(mut entry) = self.function_calls_by_item.remove(&item_id) {
1037                    if !arguments.is_empty() {
1038                        entry.arguments = arguments;
1039                    }
1040                    let raw_input = entry.arguments.clone();
1041                    self.pending_stop_reason = Some(StopReason::ToolUse);
1042                    match parse_tool_arguments(&entry.arguments) {
1043                        Ok(input) => {
1044                            vec![Ok(LanguageModelCompletionEvent::ToolUse(
1045                                LanguageModelToolUse {
1046                                    id: LanguageModelToolUseId::from(entry.call_id.clone()),
1047                                    name: entry.name.clone(),
1048                                    is_input_complete: true,
1049                                    input,
1050                                    raw_input,
1051                                    thought_signature: None,
1052                                },
1053                            ))]
1054                        }
1055                        Err(error) => {
1056                            vec![Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
1057                                id: LanguageModelToolUseId::from(entry.call_id.clone()),
1058                                tool_name: entry.name.clone(),
1059                                raw_input: Arc::<str>::from(raw_input),
1060                                json_parse_error: error.to_string(),
1061                            })]
1062                        }
1063                    }
1064                } else {
1065                    Vec::new()
1066                }
1067            }
1068            ResponsesStreamEvent::Completed { response } => {
1069                self.handle_completion(response, StopReason::EndTurn)
1070            }
1071            ResponsesStreamEvent::Incomplete { response } => {
1072                let reason = response
1073                    .status_details
1074                    .as_ref()
1075                    .and_then(|details| details.reason.as_deref());
1076                let stop_reason = match reason {
1077                    Some("max_output_tokens") => StopReason::MaxTokens,
1078                    Some("content_filter") => {
1079                        self.pending_stop_reason = Some(StopReason::Refusal);
1080                        StopReason::Refusal
1081                    }
1082                    _ => self
1083                        .pending_stop_reason
1084                        .take()
1085                        .unwrap_or(StopReason::EndTurn),
1086                };
1087
1088                let mut events = Vec::new();
1089                if self.pending_stop_reason.is_none() {
1090                    events.extend(self.emit_tool_calls_from_output(&response.output));
1091                }
1092                if let Some(usage) = response.usage.as_ref() {
1093                    events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
1094                        token_usage_from_response_usage(usage),
1095                    )));
1096                }
1097                events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
1098                events
1099            }
1100            ResponsesStreamEvent::Failed { response } => {
1101                let message = response
1102                    .status_details
1103                    .and_then(|details| details.error)
1104                    .map(|error| error.to_string())
1105                    .unwrap_or_else(|| "response failed".to_string());
1106                vec![Err(LanguageModelCompletionError::Other(anyhow!(message)))]
1107            }
1108            ResponsesStreamEvent::Error { error }
1109            | ResponsesStreamEvent::GenericError { error } => {
1110                vec![Err(LanguageModelCompletionError::Other(anyhow!(
1111                    error.message
1112                )))]
1113            }
1114            ResponsesStreamEvent::ReasoningSummaryPartAdded { summary_index, .. } => {
1115                if summary_index > 0 {
1116                    vec![Ok(LanguageModelCompletionEvent::Thinking {
1117                        text: "\n\n".to_string(),
1118                        signature: None,
1119                    })]
1120                } else {
1121                    Vec::new()
1122                }
1123            }
1124            ResponsesStreamEvent::OutputTextDone { .. }
1125            | ResponsesStreamEvent::OutputItemDone { .. }
1126            | ResponsesStreamEvent::ContentPartAdded { .. }
1127            | ResponsesStreamEvent::ContentPartDone { .. }
1128            | ResponsesStreamEvent::ReasoningSummaryTextDone { .. }
1129            | ResponsesStreamEvent::ReasoningSummaryPartDone { .. }
1130            | ResponsesStreamEvent::Created { .. }
1131            | ResponsesStreamEvent::InProgress { .. }
1132            | ResponsesStreamEvent::Unknown => Vec::new(),
1133        }
1134    }
1135
1136    fn handle_completion(
1137        &mut self,
1138        response: ResponsesSummary,
1139        default_reason: StopReason,
1140    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1141        let mut events = Vec::new();
1142
1143        if self.pending_stop_reason.is_none() {
1144            events.extend(self.emit_tool_calls_from_output(&response.output));
1145        }
1146
1147        if let Some(usage) = response.usage.as_ref() {
1148            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
1149                token_usage_from_response_usage(usage),
1150            )));
1151        }
1152
1153        let stop_reason = self.pending_stop_reason.take().unwrap_or(default_reason);
1154        events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
1155        events
1156    }
1157
1158    fn emit_tool_calls_from_output(
1159        &mut self,
1160        output: &[ResponseOutputItem],
1161    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
1162        let mut events = Vec::new();
1163        for item in output {
1164            if let ResponseOutputItem::FunctionCall(function_call) = item {
1165                let Some(call_id) = function_call
1166                    .call_id
1167                    .clone()
1168                    .or_else(|| function_call.id.clone())
1169                else {
1170                    log::error!(
1171                        "Function call item missing both call_id and id: {:?}",
1172                        function_call
1173                    );
1174                    continue;
1175                };
1176                let name: Arc<str> = Arc::from(function_call.name.clone().unwrap_or_default());
1177                let arguments = &function_call.arguments;
1178                self.pending_stop_reason = Some(StopReason::ToolUse);
1179                match parse_tool_arguments(arguments) {
1180                    Ok(input) => {
1181                        events.push(Ok(LanguageModelCompletionEvent::ToolUse(
1182                            LanguageModelToolUse {
1183                                id: LanguageModelToolUseId::from(call_id.clone()),
1184                                name: name.clone(),
1185                                is_input_complete: true,
1186                                input,
1187                                raw_input: arguments.clone(),
1188                                thought_signature: None,
1189                            },
1190                        )));
1191                    }
1192                    Err(error) => {
1193                        events.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
1194                            id: LanguageModelToolUseId::from(call_id.clone()),
1195                            tool_name: name.clone(),
1196                            raw_input: Arc::<str>::from(arguments.clone()),
1197                            json_parse_error: error.to_string(),
1198                        }));
1199                    }
1200                }
1201            }
1202        }
1203        events
1204    }
1205}
1206
1207fn token_usage_from_response_usage(usage: &ResponsesUsage) -> TokenUsage {
1208    TokenUsage {
1209        input_tokens: usage.input_tokens.unwrap_or_default(),
1210        output_tokens: usage.output_tokens.unwrap_or_default(),
1211        cache_creation_input_tokens: 0,
1212        cache_read_input_tokens: 0,
1213    }
1214}
1215
1216pub(crate) fn collect_tiktoken_messages(
1217    request: LanguageModelRequest,
1218) -> Vec<tiktoken_rs::ChatCompletionRequestMessage> {
1219    request
1220        .messages
1221        .into_iter()
1222        .map(|message| tiktoken_rs::ChatCompletionRequestMessage {
1223            role: match message.role {
1224                Role::User => "user".into(),
1225                Role::Assistant => "assistant".into(),
1226                Role::System => "system".into(),
1227            },
1228            content: Some(message.string_contents()),
1229            name: None,
1230            function_call: None,
1231        })
1232        .collect::<Vec<_>>()
1233}
1234
1235pub fn count_open_ai_tokens(
1236    request: LanguageModelRequest,
1237    model: Model,
1238    cx: &App,
1239) -> BoxFuture<'static, Result<u64>> {
1240    cx.background_spawn(async move {
1241        let messages = collect_tiktoken_messages(request);
1242        match model {
1243            Model::Custom { max_tokens, .. } => {
1244                let model = if max_tokens >= 100_000 {
1245                    // If the max tokens is 100k or more, it likely uses the o200k_base tokenizer
1246                    "gpt-4o"
1247                } else {
1248                    // Otherwise fallback to gpt-4, since only cl100k_base and o200k_base are
1249                    // supported with this tiktoken method
1250                    "gpt-4"
1251                };
1252                tiktoken_rs::num_tokens_from_messages(model, &messages)
1253            }
1254            // Currently supported by tiktoken_rs
1255            // Sometimes tiktoken-rs is behind on model support. If that is the case, make a new branch
1256            // arm with an override. We enumerate all supported models here so that we can check if new
1257            // models are supported yet or not.
1258            Model::ThreePointFiveTurbo
1259            | Model::Four
1260            | Model::FourTurbo
1261            | Model::FourOmniMini
1262            | Model::FourPointOneNano
1263            | Model::O1
1264            | Model::O3
1265            | Model::O3Mini
1266            | Model::Five
1267            | Model::FiveCodex
1268            | Model::FiveMini
1269            | Model::FiveNano => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
1270            // 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
1271            Model::FivePointOne
1272            | Model::FivePointTwo
1273            | Model::FivePointTwoCodex
1274            | Model::FivePointThreeCodex
1275            | Model::FivePointFour
1276            | Model::FivePointFourPro => tiktoken_rs::num_tokens_from_messages("gpt-5", &messages),
1277        }
1278        .map(|tokens| tokens as u64)
1279    })
1280    .boxed()
1281}
1282
1283struct ConfigurationView {
1284    api_key_editor: Entity<InputField>,
1285    state: Entity<State>,
1286    load_credentials_task: Option<Task<()>>,
1287}
1288
1289impl ConfigurationView {
1290    fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1291        let api_key_editor = cx.new(|cx| {
1292            InputField::new(
1293                window,
1294                cx,
1295                "sk-000000000000000000000000000000000000000000000000",
1296            )
1297        });
1298
1299        cx.observe(&state, |_, _, cx| {
1300            cx.notify();
1301        })
1302        .detach();
1303
1304        let load_credentials_task = Some(cx.spawn_in(window, {
1305            let state = state.clone();
1306            async move |this, cx| {
1307                if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
1308                    // We don't log an error, because "not signed in" is also an error.
1309                    let _ = task.await;
1310                }
1311                this.update(cx, |this, cx| {
1312                    this.load_credentials_task = None;
1313                    cx.notify();
1314                })
1315                .log_err();
1316            }
1317        }));
1318
1319        Self {
1320            api_key_editor,
1321            state,
1322            load_credentials_task,
1323        }
1324    }
1325
1326    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
1327        let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
1328        if api_key.is_empty() {
1329            return;
1330        }
1331
1332        // url changes can cause the editor to be displayed again
1333        self.api_key_editor
1334            .update(cx, |editor, cx| editor.set_text("", window, cx));
1335
1336        let state = self.state.clone();
1337        cx.spawn_in(window, async move |_, cx| {
1338            state
1339                .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
1340                .await
1341        })
1342        .detach_and_log_err(cx);
1343    }
1344
1345    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1346        self.api_key_editor
1347            .update(cx, |input, cx| input.set_text("", window, cx));
1348
1349        let state = self.state.clone();
1350        cx.spawn_in(window, async move |_, cx| {
1351            state
1352                .update(cx, |state, cx| state.set_api_key(None, cx))
1353                .await
1354        })
1355        .detach_and_log_err(cx);
1356    }
1357
1358    fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
1359        !self.state.read(cx).is_authenticated()
1360    }
1361}
1362
1363impl Render for ConfigurationView {
1364    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1365        let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
1366        let configured_card_label = if env_var_set {
1367            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
1368        } else {
1369            let api_url = OpenAiLanguageModelProvider::api_url(cx);
1370            if api_url == OPEN_AI_API_URL {
1371                "API key configured".to_string()
1372            } else {
1373                format!("API key configured for {}", api_url)
1374            }
1375        };
1376
1377        let api_key_section = if self.should_render_editor(cx) {
1378            v_flex()
1379                .on_action(cx.listener(Self::save_api_key))
1380                .child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:"))
1381                .child(
1382                    List::new()
1383                        .child(
1384                            ListBulletItem::new("")
1385                                .child(Label::new("Create one by visiting"))
1386                                .child(ButtonLink::new("OpenAI's console", "https://platform.openai.com/api-keys"))
1387                        )
1388                        .child(
1389                            ListBulletItem::new("Ensure your OpenAI account has credits")
1390                        )
1391                        .child(
1392                            ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
1393                        ),
1394                )
1395                .child(self.api_key_editor.clone())
1396                .child(
1397                    Label::new(format!(
1398                        "You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
1399                    ))
1400                    .size(LabelSize::Small)
1401                    .color(Color::Muted),
1402                )
1403                .child(
1404                    Label::new(
1405                        "Note that having a subscription for another service like GitHub Copilot won't work.",
1406                    )
1407                    .size(LabelSize::Small).color(Color::Muted),
1408                )
1409                .into_any_element()
1410        } else {
1411            ConfiguredApiCard::new(configured_card_label)
1412                .disabled(env_var_set)
1413                .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
1414                .when(env_var_set, |this| {
1415                    this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
1416                })
1417                .into_any_element()
1418        };
1419
1420        let compatible_api_section = h_flex()
1421            .mt_1p5()
1422            .gap_0p5()
1423            .flex_wrap()
1424            .when(self.should_render_editor(cx), |this| {
1425                this.pt_1p5()
1426                    .border_t_1()
1427                    .border_color(cx.theme().colors().border_variant)
1428            })
1429            .child(
1430                h_flex()
1431                    .gap_2()
1432                    .child(
1433                        Icon::new(IconName::Info)
1434                            .size(IconSize::XSmall)
1435                            .color(Color::Muted),
1436                    )
1437                    .child(Label::new("Zed also supports OpenAI-compatible models.")),
1438            )
1439            .child(
1440                Button::new("docs", "Learn More")
1441                    .end_icon(
1442                        Icon::new(IconName::ArrowUpRight)
1443                            .size(IconSize::Small)
1444                            .color(Color::Muted),
1445                    )
1446                    .on_click(move |_, _window, cx| {
1447                        cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible")
1448                    }),
1449            );
1450
1451        if self.load_credentials_task.is_some() {
1452            div().child(Label::new("Loading credentials…")).into_any()
1453        } else {
1454            v_flex()
1455                .size_full()
1456                .child(api_key_section)
1457                .child(compatible_api_section)
1458                .into_any()
1459        }
1460    }
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465    use futures::{StreamExt, executor::block_on};
1466    use gpui::TestAppContext;
1467    use language_model::{
1468        LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
1469    };
1470    use open_ai::responses::{
1471        ReasoningSummaryPart, ResponseFunctionToolCall, ResponseOutputItem, ResponseOutputMessage,
1472        ResponseReasoningItem, ResponseStatusDetails, ResponseSummary, ResponseUsage,
1473        StreamEvent as ResponsesStreamEvent,
1474    };
1475    use pretty_assertions::assert_eq;
1476    use serde_json::json;
1477
1478    use super::*;
1479
1480    fn map_response_events(events: Vec<ResponsesStreamEvent>) -> Vec<LanguageModelCompletionEvent> {
1481        block_on(async {
1482            OpenAiResponseEventMapper::new()
1483                .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
1484                .collect::<Vec<_>>()
1485                .await
1486                .into_iter()
1487                .map(Result::unwrap)
1488                .collect()
1489        })
1490    }
1491
1492    fn response_item_message(id: &str) -> ResponseOutputItem {
1493        ResponseOutputItem::Message(ResponseOutputMessage {
1494            id: Some(id.to_string()),
1495            role: Some("assistant".to_string()),
1496            status: Some("in_progress".to_string()),
1497            content: vec![],
1498        })
1499    }
1500
1501    fn response_item_function_call(id: &str, args: Option<&str>) -> ResponseOutputItem {
1502        ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
1503            id: Some(id.to_string()),
1504            status: Some("in_progress".to_string()),
1505            name: Some("get_weather".to_string()),
1506            call_id: Some("call_123".to_string()),
1507            arguments: args.map(|s| s.to_string()).unwrap_or_default(),
1508        })
1509    }
1510
1511    #[gpui::test]
1512    fn tiktoken_rs_support(cx: &TestAppContext) {
1513        let request = LanguageModelRequest {
1514            thread_id: None,
1515            prompt_id: None,
1516            intent: None,
1517            messages: vec![LanguageModelRequestMessage {
1518                role: Role::User,
1519                content: vec![MessageContent::Text("message".into())],
1520                cache: false,
1521                reasoning_details: None,
1522            }],
1523            tools: vec![],
1524            tool_choice: None,
1525            stop: vec![],
1526            temperature: None,
1527            thinking_allowed: true,
1528            thinking_effort: None,
1529            speed: None,
1530        };
1531
1532        // Validate that all models are supported by tiktoken-rs
1533        for model in Model::iter() {
1534            let count = cx
1535                .foreground_executor()
1536                .block_on(count_open_ai_tokens(
1537                    request.clone(),
1538                    model,
1539                    &cx.app.borrow(),
1540                ))
1541                .unwrap();
1542            assert!(count > 0);
1543        }
1544    }
1545
1546    #[test]
1547    fn responses_stream_maps_text_and_usage() {
1548        let events = vec![
1549            ResponsesStreamEvent::OutputItemAdded {
1550                output_index: 0,
1551                sequence_number: None,
1552                item: response_item_message("msg_123"),
1553            },
1554            ResponsesStreamEvent::OutputTextDelta {
1555                item_id: "msg_123".into(),
1556                output_index: 0,
1557                content_index: Some(0),
1558                delta: "Hello".into(),
1559            },
1560            ResponsesStreamEvent::Completed {
1561                response: ResponseSummary {
1562                    usage: Some(ResponseUsage {
1563                        input_tokens: Some(5),
1564                        output_tokens: Some(3),
1565                        total_tokens: Some(8),
1566                    }),
1567                    ..Default::default()
1568                },
1569            },
1570        ];
1571
1572        let mapped = map_response_events(events);
1573        assert!(matches!(
1574            mapped[0],
1575            LanguageModelCompletionEvent::StartMessage { ref message_id } if message_id == "msg_123"
1576        ));
1577        assert!(matches!(
1578            mapped[1],
1579            LanguageModelCompletionEvent::Text(ref text) if text == "Hello"
1580        ));
1581        assert!(matches!(
1582            mapped[2],
1583            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1584                input_tokens: 5,
1585                output_tokens: 3,
1586                ..
1587            })
1588        ));
1589        assert!(matches!(
1590            mapped[3],
1591            LanguageModelCompletionEvent::Stop(StopReason::EndTurn)
1592        ));
1593    }
1594
1595    #[test]
1596    fn into_open_ai_response_builds_complete_payload() {
1597        let tool_call_id = LanguageModelToolUseId::from("call-42");
1598        let tool_input = json!({ "city": "Boston" });
1599        let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1600        let tool_use = LanguageModelToolUse {
1601            id: tool_call_id.clone(),
1602            name: Arc::from("get_weather"),
1603            raw_input: tool_arguments.clone(),
1604            input: tool_input,
1605            is_input_complete: true,
1606            thought_signature: None,
1607        };
1608        let tool_result = LanguageModelToolResult {
1609            tool_use_id: tool_call_id,
1610            tool_name: Arc::from("get_weather"),
1611            is_error: false,
1612            content: LanguageModelToolResultContent::Text(Arc::from("Sunny")),
1613            output: Some(json!({ "forecast": "Sunny" })),
1614        };
1615        let user_image = LanguageModelImage {
1616            source: SharedString::from("aGVsbG8="),
1617            size: None,
1618        };
1619        let expected_image_url = user_image.to_base64_url();
1620
1621        let request = LanguageModelRequest {
1622            thread_id: Some("thread-123".into()),
1623            prompt_id: None,
1624            intent: None,
1625            messages: vec![
1626                LanguageModelRequestMessage {
1627                    role: Role::System,
1628                    content: vec![MessageContent::Text("System context".into())],
1629                    cache: false,
1630                    reasoning_details: None,
1631                },
1632                LanguageModelRequestMessage {
1633                    role: Role::User,
1634                    content: vec![
1635                        MessageContent::Text("Please check the weather.".into()),
1636                        MessageContent::Image(user_image),
1637                    ],
1638                    cache: false,
1639                    reasoning_details: None,
1640                },
1641                LanguageModelRequestMessage {
1642                    role: Role::Assistant,
1643                    content: vec![
1644                        MessageContent::Text("Looking that up.".into()),
1645                        MessageContent::ToolUse(tool_use),
1646                    ],
1647                    cache: false,
1648                    reasoning_details: None,
1649                },
1650                LanguageModelRequestMessage {
1651                    role: Role::Assistant,
1652                    content: vec![MessageContent::ToolResult(tool_result)],
1653                    cache: false,
1654                    reasoning_details: None,
1655                },
1656            ],
1657            tools: vec![LanguageModelRequestTool {
1658                name: "get_weather".into(),
1659                description: "Fetches the weather".into(),
1660                input_schema: json!({ "type": "object" }),
1661                use_input_streaming: false,
1662            }],
1663            tool_choice: Some(LanguageModelToolChoice::Any),
1664            stop: vec!["<STOP>".into()],
1665            temperature: None,
1666            thinking_allowed: false,
1667            thinking_effort: None,
1668            speed: None,
1669        };
1670
1671        let response = into_open_ai_response(
1672            request,
1673            "custom-model",
1674            true,
1675            true,
1676            Some(2048),
1677            Some(ReasoningEffort::Low),
1678        );
1679
1680        let serialized = serde_json::to_value(&response).unwrap();
1681        let expected = json!({
1682            "model": "custom-model",
1683            "input": [
1684                {
1685                    "type": "message",
1686                    "role": "system",
1687                    "content": [
1688                        { "type": "input_text", "text": "System context" }
1689                    ]
1690                },
1691                {
1692                    "type": "message",
1693                    "role": "user",
1694                    "content": [
1695                        { "type": "input_text", "text": "Please check the weather." },
1696                        { "type": "input_image", "image_url": expected_image_url }
1697                    ]
1698                },
1699                {
1700                    "type": "message",
1701                    "role": "assistant",
1702                    "content": [
1703                        { "type": "output_text", "text": "Looking that up.", "annotations": [] }
1704                    ]
1705                },
1706                {
1707                    "type": "function_call",
1708                    "call_id": "call-42",
1709                    "name": "get_weather",
1710                    "arguments": tool_arguments
1711                },
1712                {
1713                    "type": "function_call_output",
1714                    "call_id": "call-42",
1715                    "output": "Sunny"
1716                }
1717            ],
1718            "stream": true,
1719            "max_output_tokens": 2048,
1720            "parallel_tool_calls": true,
1721            "tool_choice": "required",
1722            "tools": [
1723                {
1724                    "type": "function",
1725                    "name": "get_weather",
1726                    "description": "Fetches the weather",
1727                    "parameters": { "type": "object" }
1728                }
1729            ],
1730            "prompt_cache_key": "thread-123",
1731            "reasoning": { "effort": "low", "summary": "auto" }
1732        });
1733
1734        assert_eq!(serialized, expected);
1735    }
1736
1737    #[test]
1738    fn responses_stream_maps_tool_calls() {
1739        let events = vec![
1740            ResponsesStreamEvent::OutputItemAdded {
1741                output_index: 0,
1742                sequence_number: None,
1743                item: response_item_function_call("item_fn", Some("{\"city\":\"Bos")),
1744            },
1745            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1746                item_id: "item_fn".into(),
1747                output_index: 0,
1748                delta: "ton\"}".into(),
1749                sequence_number: None,
1750            },
1751            ResponsesStreamEvent::FunctionCallArgumentsDone {
1752                item_id: "item_fn".into(),
1753                output_index: 0,
1754                arguments: "{\"city\":\"Boston\"}".into(),
1755                sequence_number: None,
1756            },
1757            ResponsesStreamEvent::Completed {
1758                response: ResponseSummary::default(),
1759            },
1760        ];
1761
1762        let mapped = map_response_events(events);
1763        assert_eq!(mapped.len(), 3);
1764        // First event is the partial tool use (from FunctionCallArgumentsDelta)
1765        assert!(matches!(
1766            mapped[0],
1767            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1768                is_input_complete: false,
1769                ..
1770            })
1771        ));
1772        // Second event is the complete tool use (from FunctionCallArgumentsDone)
1773        assert!(matches!(
1774            mapped[1],
1775            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1776                ref id,
1777                ref name,
1778                ref raw_input,
1779                is_input_complete: true,
1780                ..
1781            }) if id.to_string() == "call_123"
1782                && name.as_ref() == "get_weather"
1783                && raw_input == "{\"city\":\"Boston\"}"
1784        ));
1785        assert!(matches!(
1786            mapped[2],
1787            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1788        ));
1789    }
1790
1791    #[test]
1792    fn responses_stream_uses_max_tokens_stop_reason() {
1793        let events = vec![ResponsesStreamEvent::Incomplete {
1794            response: ResponseSummary {
1795                status_details: Some(ResponseStatusDetails {
1796                    reason: Some("max_output_tokens".into()),
1797                    r#type: Some("incomplete".into()),
1798                    error: None,
1799                }),
1800                usage: Some(ResponseUsage {
1801                    input_tokens: Some(10),
1802                    output_tokens: Some(20),
1803                    total_tokens: Some(30),
1804                }),
1805                ..Default::default()
1806            },
1807        }];
1808
1809        let mapped = map_response_events(events);
1810        assert!(matches!(
1811            mapped[0],
1812            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1813                input_tokens: 10,
1814                output_tokens: 20,
1815                ..
1816            })
1817        ));
1818        assert!(matches!(
1819            mapped[1],
1820            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1821        ));
1822    }
1823
1824    #[test]
1825    fn responses_stream_handles_multiple_tool_calls() {
1826        let events = vec![
1827            ResponsesStreamEvent::OutputItemAdded {
1828                output_index: 0,
1829                sequence_number: None,
1830                item: response_item_function_call("item_fn1", Some("{\"city\":\"NYC\"}")),
1831            },
1832            ResponsesStreamEvent::FunctionCallArgumentsDone {
1833                item_id: "item_fn1".into(),
1834                output_index: 0,
1835                arguments: "{\"city\":\"NYC\"}".into(),
1836                sequence_number: None,
1837            },
1838            ResponsesStreamEvent::OutputItemAdded {
1839                output_index: 1,
1840                sequence_number: None,
1841                item: response_item_function_call("item_fn2", Some("{\"city\":\"LA\"}")),
1842            },
1843            ResponsesStreamEvent::FunctionCallArgumentsDone {
1844                item_id: "item_fn2".into(),
1845                output_index: 1,
1846                arguments: "{\"city\":\"LA\"}".into(),
1847                sequence_number: None,
1848            },
1849            ResponsesStreamEvent::Completed {
1850                response: ResponseSummary::default(),
1851            },
1852        ];
1853
1854        let mapped = map_response_events(events);
1855        assert_eq!(mapped.len(), 3);
1856        assert!(matches!(
1857            mapped[0],
1858            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1859            if raw_input == "{\"city\":\"NYC\"}"
1860        ));
1861        assert!(matches!(
1862            mapped[1],
1863            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1864            if raw_input == "{\"city\":\"LA\"}"
1865        ));
1866        assert!(matches!(
1867            mapped[2],
1868            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1869        ));
1870    }
1871
1872    #[test]
1873    fn responses_stream_handles_mixed_text_and_tool_calls() {
1874        let events = vec![
1875            ResponsesStreamEvent::OutputItemAdded {
1876                output_index: 0,
1877                sequence_number: None,
1878                item: response_item_message("msg_123"),
1879            },
1880            ResponsesStreamEvent::OutputTextDelta {
1881                item_id: "msg_123".into(),
1882                output_index: 0,
1883                content_index: Some(0),
1884                delta: "Let me check that".into(),
1885            },
1886            ResponsesStreamEvent::OutputItemAdded {
1887                output_index: 1,
1888                sequence_number: None,
1889                item: response_item_function_call("item_fn", Some("{\"query\":\"test\"}")),
1890            },
1891            ResponsesStreamEvent::FunctionCallArgumentsDone {
1892                item_id: "item_fn".into(),
1893                output_index: 1,
1894                arguments: "{\"query\":\"test\"}".into(),
1895                sequence_number: None,
1896            },
1897            ResponsesStreamEvent::Completed {
1898                response: ResponseSummary::default(),
1899            },
1900        ];
1901
1902        let mapped = map_response_events(events);
1903        assert!(matches!(
1904            mapped[0],
1905            LanguageModelCompletionEvent::StartMessage { .. }
1906        ));
1907        assert!(matches!(
1908            mapped[1],
1909            LanguageModelCompletionEvent::Text(ref text) if text == "Let me check that"
1910        ));
1911        assert!(matches!(
1912            mapped[2],
1913            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1914            if raw_input == "{\"query\":\"test\"}"
1915        ));
1916        assert!(matches!(
1917            mapped[3],
1918            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1919        ));
1920    }
1921
1922    #[test]
1923    fn responses_stream_handles_json_parse_error() {
1924        let events = vec![
1925            ResponsesStreamEvent::OutputItemAdded {
1926                output_index: 0,
1927                sequence_number: None,
1928                item: response_item_function_call("item_fn", Some("{invalid json")),
1929            },
1930            ResponsesStreamEvent::FunctionCallArgumentsDone {
1931                item_id: "item_fn".into(),
1932                output_index: 0,
1933                arguments: "{invalid json".into(),
1934                sequence_number: None,
1935            },
1936            ResponsesStreamEvent::Completed {
1937                response: ResponseSummary::default(),
1938            },
1939        ];
1940
1941        let mapped = map_response_events(events);
1942        assert!(matches!(
1943            mapped[0],
1944            LanguageModelCompletionEvent::ToolUseJsonParseError {
1945                ref raw_input,
1946                ..
1947            } if raw_input.as_ref() == "{invalid json"
1948        ));
1949    }
1950
1951    #[test]
1952    fn responses_stream_handles_incomplete_function_call() {
1953        let events = vec![
1954            ResponsesStreamEvent::OutputItemAdded {
1955                output_index: 0,
1956                sequence_number: None,
1957                item: response_item_function_call("item_fn", Some("{\"city\":")),
1958            },
1959            ResponsesStreamEvent::FunctionCallArgumentsDelta {
1960                item_id: "item_fn".into(),
1961                output_index: 0,
1962                delta: "\"Boston\"".into(),
1963                sequence_number: None,
1964            },
1965            ResponsesStreamEvent::Incomplete {
1966                response: ResponseSummary {
1967                    status_details: Some(ResponseStatusDetails {
1968                        reason: Some("max_output_tokens".into()),
1969                        r#type: Some("incomplete".into()),
1970                        error: None,
1971                    }),
1972                    output: vec![response_item_function_call(
1973                        "item_fn",
1974                        Some("{\"city\":\"Boston\"}"),
1975                    )],
1976                    ..Default::default()
1977                },
1978            },
1979        ];
1980
1981        let mapped = map_response_events(events);
1982        assert_eq!(mapped.len(), 3);
1983        // First event is the partial tool use (from FunctionCallArgumentsDelta)
1984        assert!(matches!(
1985            mapped[0],
1986            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1987                is_input_complete: false,
1988                ..
1989            })
1990        ));
1991        // Second event is the complete tool use (from the Incomplete response output)
1992        assert!(matches!(
1993            mapped[1],
1994            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1995                ref raw_input,
1996                is_input_complete: true,
1997                ..
1998            })
1999            if raw_input == "{\"city\":\"Boston\"}"
2000        ));
2001        assert!(matches!(
2002            mapped[2],
2003            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
2004        ));
2005    }
2006
2007    #[test]
2008    fn responses_stream_incomplete_does_not_duplicate_tool_calls() {
2009        let events = vec![
2010            ResponsesStreamEvent::OutputItemAdded {
2011                output_index: 0,
2012                sequence_number: None,
2013                item: response_item_function_call("item_fn", Some("{\"city\":\"Boston\"}")),
2014            },
2015            ResponsesStreamEvent::FunctionCallArgumentsDone {
2016                item_id: "item_fn".into(),
2017                output_index: 0,
2018                arguments: "{\"city\":\"Boston\"}".into(),
2019                sequence_number: None,
2020            },
2021            ResponsesStreamEvent::Incomplete {
2022                response: ResponseSummary {
2023                    status_details: Some(ResponseStatusDetails {
2024                        reason: Some("max_output_tokens".into()),
2025                        r#type: Some("incomplete".into()),
2026                        error: None,
2027                    }),
2028                    output: vec![response_item_function_call(
2029                        "item_fn",
2030                        Some("{\"city\":\"Boston\"}"),
2031                    )],
2032                    ..Default::default()
2033                },
2034            },
2035        ];
2036
2037        let mapped = map_response_events(events);
2038        assert_eq!(mapped.len(), 2);
2039        assert!(matches!(
2040            mapped[0],
2041            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
2042            if raw_input == "{\"city\":\"Boston\"}"
2043        ));
2044        assert!(matches!(
2045            mapped[1],
2046            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
2047        ));
2048    }
2049
2050    #[test]
2051    fn responses_stream_handles_empty_tool_arguments() {
2052        // Test that tools with no arguments (empty string) are handled correctly
2053        let events = vec![
2054            ResponsesStreamEvent::OutputItemAdded {
2055                output_index: 0,
2056                sequence_number: None,
2057                item: response_item_function_call("item_fn", Some("")),
2058            },
2059            ResponsesStreamEvent::FunctionCallArgumentsDone {
2060                item_id: "item_fn".into(),
2061                output_index: 0,
2062                arguments: "".into(),
2063                sequence_number: None,
2064            },
2065            ResponsesStreamEvent::Completed {
2066                response: ResponseSummary::default(),
2067            },
2068        ];
2069
2070        let mapped = map_response_events(events);
2071        assert_eq!(mapped.len(), 2);
2072
2073        // Should produce a ToolUse event with an empty object
2074        assert!(matches!(
2075            &mapped[0],
2076            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
2077                id,
2078                name,
2079                raw_input,
2080                input,
2081                ..
2082            }) if id.to_string() == "call_123"
2083                && name.as_ref() == "get_weather"
2084                && raw_input == ""
2085                && input.is_object()
2086                && input.as_object().unwrap().is_empty()
2087        ));
2088
2089        assert!(matches!(
2090            mapped[1],
2091            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
2092        ));
2093    }
2094
2095    #[test]
2096    fn responses_stream_emits_partial_tool_use_events() {
2097        let events = vec![
2098            ResponsesStreamEvent::OutputItemAdded {
2099                output_index: 0,
2100                sequence_number: None,
2101                item: ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
2102                    id: Some("item_fn".to_string()),
2103                    status: Some("in_progress".to_string()),
2104                    name: Some("get_weather".to_string()),
2105                    call_id: Some("call_abc".to_string()),
2106                    arguments: String::new(),
2107                }),
2108            },
2109            ResponsesStreamEvent::FunctionCallArgumentsDelta {
2110                item_id: "item_fn".into(),
2111                output_index: 0,
2112                delta: "{\"city\":\"Bos".into(),
2113                sequence_number: None,
2114            },
2115            ResponsesStreamEvent::FunctionCallArgumentsDelta {
2116                item_id: "item_fn".into(),
2117                output_index: 0,
2118                delta: "ton\"}".into(),
2119                sequence_number: None,
2120            },
2121            ResponsesStreamEvent::FunctionCallArgumentsDone {
2122                item_id: "item_fn".into(),
2123                output_index: 0,
2124                arguments: "{\"city\":\"Boston\"}".into(),
2125                sequence_number: None,
2126            },
2127            ResponsesStreamEvent::Completed {
2128                response: ResponseSummary::default(),
2129            },
2130        ];
2131
2132        let mapped = map_response_events(events);
2133        // Two partial events + one complete event + Stop
2134        assert!(mapped.len() >= 3);
2135
2136        // The last complete ToolUse event should have is_input_complete: true
2137        let complete_tool_use = mapped.iter().find(|e| {
2138            matches!(
2139                e,
2140                LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
2141                    is_input_complete: true,
2142                    ..
2143                })
2144            )
2145        });
2146        assert!(
2147            complete_tool_use.is_some(),
2148            "should have a complete tool use event"
2149        );
2150
2151        // All ToolUse events before the final one should have is_input_complete: false
2152        let tool_uses: Vec<_> = mapped
2153            .iter()
2154            .filter(|e| matches!(e, LanguageModelCompletionEvent::ToolUse(_)))
2155            .collect();
2156        assert!(
2157            tool_uses.len() >= 2,
2158            "should have at least one partial and one complete event"
2159        );
2160
2161        let last = tool_uses.last().unwrap();
2162        assert!(matches!(
2163            last,
2164            LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
2165                is_input_complete: true,
2166                ..
2167            })
2168        ));
2169    }
2170
2171    #[test]
2172    fn responses_stream_maps_reasoning_summary_deltas() {
2173        let events = vec![
2174            ResponsesStreamEvent::OutputItemAdded {
2175                output_index: 0,
2176                sequence_number: None,
2177                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
2178                    id: Some("rs_123".into()),
2179                    summary: vec![],
2180                }),
2181            },
2182            ResponsesStreamEvent::ReasoningSummaryPartAdded {
2183                item_id: "rs_123".into(),
2184                output_index: 0,
2185                summary_index: 0,
2186            },
2187            ResponsesStreamEvent::ReasoningSummaryTextDelta {
2188                item_id: "rs_123".into(),
2189                output_index: 0,
2190                delta: "Thinking about".into(),
2191            },
2192            ResponsesStreamEvent::ReasoningSummaryTextDelta {
2193                item_id: "rs_123".into(),
2194                output_index: 0,
2195                delta: " the answer".into(),
2196            },
2197            ResponsesStreamEvent::ReasoningSummaryTextDone {
2198                item_id: "rs_123".into(),
2199                output_index: 0,
2200                text: "Thinking about the answer".into(),
2201            },
2202            ResponsesStreamEvent::ReasoningSummaryPartDone {
2203                item_id: "rs_123".into(),
2204                output_index: 0,
2205                summary_index: 0,
2206            },
2207            ResponsesStreamEvent::ReasoningSummaryPartAdded {
2208                item_id: "rs_123".into(),
2209                output_index: 0,
2210                summary_index: 1,
2211            },
2212            ResponsesStreamEvent::ReasoningSummaryTextDelta {
2213                item_id: "rs_123".into(),
2214                output_index: 0,
2215                delta: "Second part".into(),
2216            },
2217            ResponsesStreamEvent::ReasoningSummaryTextDone {
2218                item_id: "rs_123".into(),
2219                output_index: 0,
2220                text: "Second part".into(),
2221            },
2222            ResponsesStreamEvent::ReasoningSummaryPartDone {
2223                item_id: "rs_123".into(),
2224                output_index: 0,
2225                summary_index: 1,
2226            },
2227            ResponsesStreamEvent::OutputItemDone {
2228                output_index: 0,
2229                sequence_number: None,
2230                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
2231                    id: Some("rs_123".into()),
2232                    summary: vec![
2233                        ReasoningSummaryPart::SummaryText {
2234                            text: "Thinking about the answer".into(),
2235                        },
2236                        ReasoningSummaryPart::SummaryText {
2237                            text: "Second part".into(),
2238                        },
2239                    ],
2240                }),
2241            },
2242            ResponsesStreamEvent::OutputItemAdded {
2243                output_index: 1,
2244                sequence_number: None,
2245                item: response_item_message("msg_456"),
2246            },
2247            ResponsesStreamEvent::OutputTextDelta {
2248                item_id: "msg_456".into(),
2249                output_index: 1,
2250                content_index: Some(0),
2251                delta: "The answer is 42".into(),
2252            },
2253            ResponsesStreamEvent::Completed {
2254                response: ResponseSummary::default(),
2255            },
2256        ];
2257
2258        let mapped = map_response_events(events);
2259
2260        let thinking_events: Vec<_> = mapped
2261            .iter()
2262            .filter(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. }))
2263            .collect();
2264        assert_eq!(
2265            thinking_events.len(),
2266            4,
2267            "expected 4 thinking events (2 deltas + separator + second delta), got {:?}",
2268            thinking_events,
2269        );
2270
2271        assert!(matches!(
2272            &thinking_events[0],
2273            LanguageModelCompletionEvent::Thinking { text, .. } if text == "Thinking about"
2274        ));
2275        assert!(matches!(
2276            &thinking_events[1],
2277            LanguageModelCompletionEvent::Thinking { text, .. } if text == " the answer"
2278        ));
2279        assert!(
2280            matches!(
2281                &thinking_events[2],
2282                LanguageModelCompletionEvent::Thinking { text, .. } if text == "\n\n"
2283            ),
2284            "expected separator between summary parts"
2285        );
2286        assert!(matches!(
2287            &thinking_events[3],
2288            LanguageModelCompletionEvent::Thinking { text, .. } if text == "Second part"
2289        ));
2290
2291        assert!(mapped.iter().any(|e| matches!(
2292            e,
2293            LanguageModelCompletionEvent::Text(t) if t == "The answer is 42"
2294        )));
2295    }
2296
2297    #[test]
2298    fn responses_stream_maps_reasoning_from_done_only() {
2299        let events = vec![
2300            ResponsesStreamEvent::OutputItemAdded {
2301                output_index: 0,
2302                sequence_number: None,
2303                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
2304                    id: Some("rs_789".into()),
2305                    summary: vec![],
2306                }),
2307            },
2308            ResponsesStreamEvent::OutputItemDone {
2309                output_index: 0,
2310                sequence_number: None,
2311                item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
2312                    id: Some("rs_789".into()),
2313                    summary: vec![ReasoningSummaryPart::SummaryText {
2314                        text: "Summary without deltas".into(),
2315                    }],
2316                }),
2317            },
2318            ResponsesStreamEvent::Completed {
2319                response: ResponseSummary::default(),
2320            },
2321        ];
2322
2323        let mapped = map_response_events(events);
2324
2325        assert!(
2326            !mapped
2327                .iter()
2328                .any(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. })),
2329            "OutputItemDone reasoning should not produce Thinking events (no delta/done text events)"
2330        );
2331    }
2332}