Skip to repository content

tenant.openagents/omega

No repository description is available.

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

copilot_chat.rs

1951 lines · 76.7 KB · rust
1use std::pin::Pin;
2use std::str::FromStr as _;
3use std::sync::Arc;
4
5use anthropic::AnthropicModelMode;
6use anyhow::{Result, anyhow};
7use collections::HashMap;
8use copilot::{GlobalCopilotAuth, Status};
9use copilot_chat::responses as copilot_responses;
10use copilot_chat::{
11    ChatLocation, ChatMessage, ChatMessageContent, ChatMessagePart, CopilotChat,
12    CopilotChatConfiguration, Function, FunctionContent, ImageUrl, Model as CopilotChatModel,
13    ModelVendor, Request as CopilotChatRequest, ResponseEvent, Tool, ToolCall, ToolCallContent,
14    ToolChoice,
15};
16use futures::future::BoxFuture;
17use futures::stream::BoxStream;
18use futures::{FutureExt, Stream, StreamExt};
19use gpui::{App, AsyncApp, Entity, Subscription, Task};
20use http_client::StatusCode;
21use language::language_settings::all_language_settings;
22use language_model::{
23    AuthenticateError, CompletionIntent, IconOrSvg, LanguageModel, LanguageModelCompletionError,
24    LanguageModelCompletionEvent, LanguageModelCostInfo, LanguageModelEffortLevel, LanguageModelId,
25    LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
26    LanguageModelProviderState, LanguageModelRequest, LanguageModelRequestMessage,
27    LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
28    LanguageModelToolUse, MessageContent, ProviderSettingsView, RateLimiter, Role, StopReason,
29    TokenUsage,
30};
31use settings::SettingsStore;
32use ui::prelude::*;
33use util::debug_panic;
34
35use crate::provider::anthropic::{AnthropicEventMapper, AnthropicPromptCacheMode, into_anthropic};
36use language_model::util::{fix_streamed_json, parse_tool_arguments};
37
38const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat");
39const PROVIDER_NAME: LanguageModelProviderName =
40    LanguageModelProviderName::new("GitHub Copilot Chat");
41
42pub struct CopilotChatLanguageModelProvider {
43    state: Entity<State>,
44}
45
46pub struct State {
47    _copilot_chat_subscription: Option<Subscription>,
48    _settings_subscription: Subscription,
49}
50
51impl State {
52    fn is_authenticated(&self, cx: &App) -> bool {
53        CopilotChat::global(cx)
54            .map(|m| m.read(cx).is_authenticated())
55            .unwrap_or(false)
56    }
57}
58
59impl CopilotChatLanguageModelProvider {
60    pub fn new(cx: &mut App) -> Self {
61        let state = cx.new(|cx| {
62            let copilot_chat_subscription = CopilotChat::global(cx)
63                .map(|copilot_chat| cx.observe(&copilot_chat, |_, _, cx| cx.notify()));
64            State {
65                _copilot_chat_subscription: copilot_chat_subscription,
66                _settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
67                    if let Some(copilot_chat) = CopilotChat::global(cx) {
68                        let language_settings = all_language_settings(None, cx);
69                        let configuration = CopilotChatConfiguration {
70                            enterprise_uri: language_settings
71                                .edit_predictions
72                                .copilot
73                                .enterprise_uri
74                                .clone(),
75                        };
76                        copilot_chat.update(cx, |chat, cx| {
77                            chat.set_configuration(configuration, cx);
78                        });
79                    }
80                    cx.notify();
81                }),
82            }
83        });
84
85        Self { state }
86    }
87
88    fn create_language_model(&self, model: CopilotChatModel) -> Arc<dyn LanguageModel> {
89        Arc::new(CopilotChatLanguageModel {
90            model,
91            request_limiter: RateLimiter::new(4),
92        })
93    }
94}
95
96impl LanguageModelProviderState for CopilotChatLanguageModelProvider {
97    type ObservableEntity = State;
98
99    fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
100        Some(self.state.clone())
101    }
102}
103
104impl LanguageModelProvider for CopilotChatLanguageModelProvider {
105    fn id(&self) -> LanguageModelProviderId {
106        PROVIDER_ID
107    }
108
109    fn name(&self) -> LanguageModelProviderName {
110        PROVIDER_NAME
111    }
112
113    fn icon(&self) -> IconOrSvg {
114        IconOrSvg::Icon(IconName::Copilot)
115    }
116
117    fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
118        let models = CopilotChat::global(cx).and_then(|m| m.read(cx).models())?;
119        models
120            .first()
121            .map(|model| self.create_language_model(model.clone()))
122    }
123
124    fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
125        // The default model should be Copilot Chat's 'base model', which is likely a relatively fast
126        // model (e.g. 4o) and a sensible choice when considering premium requests
127        self.default_model(cx)
128    }
129
130    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
131        let Some(models) = CopilotChat::global(cx).and_then(|m| m.read(cx).models()) else {
132            return Vec::new();
133        };
134        models
135            .iter()
136            .map(|model| self.create_language_model(model.clone()))
137            .collect()
138    }
139
140    fn is_authenticated(&self, cx: &App) -> bool {
141        self.state.read(cx).is_authenticated(cx)
142    }
143
144    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
145        if self.is_authenticated(cx) {
146            return Task::ready(Ok(()));
147        };
148
149        let Some(copilot) = GlobalCopilotAuth::try_global(cx).cloned() else {
150            return Task::ready(Err(anyhow!(concat!(
151                "Copilot must be enabled for Copilot Chat to work. ",
152                "Please enable Copilot and try again."
153            ))
154            .into()));
155        };
156
157        let err = match copilot.0.read(cx).status() {
158            Status::Authorized => return Task::ready(Ok(())),
159            Status::Disabled => anyhow!(
160                "Copilot must be enabled for Copilot Chat to work. Please enable Copilot and try again."
161            ),
162            Status::Error(err) => anyhow!(format!(
163                "Received the following error while signing into Copilot: {err}"
164            )),
165            Status::Starting { task: _ } => anyhow!(
166                "Copilot is still starting, please wait for Copilot to start then try again"
167            ),
168            Status::Unauthorized => anyhow!(
169                "Unable to authorize with Copilot. Please make sure that you have an active Copilot and Copilot Chat subscription."
170            ),
171            Status::SignedOut { .. } => {
172                anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again.")
173            }
174            Status::SigningIn { prompt: _ } => anyhow!("Still signing into Copilot..."),
175        };
176
177        Task::ready(Err(err.into()))
178    }
179
180    fn settings_view(&self, cx: &mut App) -> Option<ProviderSettingsView> {
181        let is_authenticated = self.state.read(cx).is_authenticated(cx);
182        let title = if is_authenticated {
183            None
184        } else {
185            Some("Configure Copilot".into())
186        };
187        let description = if is_authenticated {
188            None
189        } else {
190            Some(language_model::InlineDescription::Text(
191                "Requires an active GitHub Copilot subscription.".into(),
192            ))
193        };
194
195        Some(ProviderSettingsView::Inline(
196            language_model::InlineProviderSettings {
197                title,
198                description,
199                create_view: Arc::new(|_window, cx| {
200                    cx.new(|cx| {
201                        copilot_ui::ConfigurationView::new(
202                            |cx| {
203                                CopilotChat::global(cx)
204                                    .map(|m| m.read(cx).is_authenticated())
205                                    .unwrap_or(false)
206                            },
207                            copilot_ui::ConfigurationMode::Chat,
208                            cx,
209                        )
210                        .compact()
211                    })
212                    .into()
213                }),
214            },
215        ))
216    }
217}
218
219pub struct CopilotChatLanguageModel {
220    model: CopilotChatModel,
221    request_limiter: RateLimiter,
222}
223
224impl LanguageModel for CopilotChatLanguageModel {
225    fn id(&self) -> LanguageModelId {
226        LanguageModelId::from(self.model.id().to_string())
227    }
228
229    fn name(&self) -> LanguageModelName {
230        LanguageModelName::from(self.model.display_name().to_string())
231    }
232
233    fn provider_id(&self) -> LanguageModelProviderId {
234        PROVIDER_ID
235    }
236
237    fn provider_name(&self) -> LanguageModelProviderName {
238        PROVIDER_NAME
239    }
240
241    fn supports_tools(&self) -> bool {
242        self.model.supports_tools()
243    }
244
245    fn supports_streaming_tools(&self) -> bool {
246        true
247    }
248
249    fn supports_images(&self) -> bool {
250        self.model.supports_vision()
251    }
252
253    fn supports_thinking(&self) -> bool {
254        self.model.can_think()
255    }
256
257    fn supported_effort_levels(&self) -> Vec<LanguageModelEffortLevel> {
258        let levels = self.model.reasoning_effort_levels();
259        if levels.is_empty() {
260            return vec![];
261        }
262        levels
263            .iter()
264            .map(|level| {
265                let name = match level.as_str() {
266                    "low" => "Low".into(),
267                    "medium" => "Medium".into(),
268                    "high" => "High".into(),
269                    "xhigh" => "Extra High".into(),
270                    _ => language_model::SharedString::from(level.clone()),
271                };
272                LanguageModelEffortLevel {
273                    name,
274                    value: language_model::SharedString::from(level.clone()),
275                    is_default: level == "high",
276                }
277            })
278            .collect()
279    }
280
281    fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
282        match self.model.vendor() {
283            ModelVendor::OpenAI | ModelVendor::Anthropic => {
284                LanguageModelToolSchemaFormat::JsonSchema
285            }
286            ModelVendor::Google | ModelVendor::XAI | ModelVendor::Unknown => {
287                LanguageModelToolSchemaFormat::JsonSchemaSubset
288            }
289        }
290    }
291
292    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
293        match choice {
294            LanguageModelToolChoice::Auto
295            | LanguageModelToolChoice::Any
296            | LanguageModelToolChoice::None => self.supports_tools(),
297        }
298    }
299
300    fn model_cost_info(&self) -> Option<LanguageModelCostInfo> {
301        LanguageModelCostInfo::RequestCost {
302            cost_per_request: self.model.multiplier(),
303        }
304        .into()
305    }
306
307    fn telemetry_id(&self) -> String {
308        format!("copilot_chat/{}", self.model.id())
309    }
310
311    fn max_token_count(&self) -> u64 {
312        self.model.max_token_count()
313    }
314
315    fn stream_completion(
316        &self,
317        request: LanguageModelRequest,
318        cx: &AsyncApp,
319    ) -> BoxFuture<
320        'static,
321        Result<
322            BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
323            LanguageModelCompletionError,
324        >,
325    > {
326        let is_user_initiated = request.intent.is_none_or(|intent| match intent {
327            CompletionIntent::UserPrompt
328            | CompletionIntent::ThreadContextSummarization
329            | CompletionIntent::InlineAssist
330            | CompletionIntent::TerminalInlineAssist
331            | CompletionIntent::GenerateGitCommitMessage => true,
332
333            CompletionIntent::Subagent
334            | CompletionIntent::ToolResults
335            | CompletionIntent::ThreadSummarization
336            | CompletionIntent::CreateFile
337            | CompletionIntent::EditFile => false,
338        });
339
340        if self.model.supports_messages() {
341            let location = intent_to_chat_location(request.intent);
342            let model = self.model.clone();
343            let request_limiter = self.request_limiter.clone();
344            let future = cx.spawn(async move |cx| {
345                let effort = request
346                    .thinking_effort
347                    .as_ref()
348                    .and_then(|e| anthropic::Effort::from_str(e).ok());
349
350                let mut anthropic_request = into_anthropic(
351                    request,
352                    model.id().to_string(),
353                    0.0,
354                    model.max_output_tokens() as u64,
355                    if model.supports_adaptive_thinking() {
356                        AnthropicModelMode::Thinking {
357                            budget_tokens: None,
358                        }
359                    } else if model.supports_thinking() {
360                        AnthropicModelMode::Thinking {
361                            budget_tokens: compute_thinking_budget(
362                                model.min_thinking_budget(),
363                                model.max_thinking_budget(),
364                                model.max_output_tokens() as u32,
365                            ),
366                        }
367                    } else {
368                        AnthropicModelMode::Default
369                    },
370                    AnthropicPromptCacheMode::Legacy,
371                    &PROVIDER_ID,
372                )?;
373
374                anthropic_request.temperature = None;
375
376                // The Copilot proxy doesn't support eager_input_streaming on tools.
377                for tool in &mut anthropic_request.tools {
378                    tool.eager_input_streaming = false;
379                }
380
381                if model.supports_adaptive_thinking() {
382                    if anthropic_request.thinking.is_some() {
383                        anthropic_request.thinking = Some(anthropic::Thinking::Adaptive {
384                            display: Some(anthropic::AdaptiveThinkingDisplay::Summarized),
385                        });
386                        anthropic_request.output_config =
387                            effort.map(|effort| anthropic::OutputConfig {
388                                effort: Some(effort),
389                            });
390                    }
391                }
392
393                let anthropic_beta =
394                    if !model.supports_adaptive_thinking() && model.supports_thinking() {
395                        Some("interleaved-thinking-2025-05-14".to_string())
396                    } else {
397                        None
398                    };
399
400                let body = serde_json::to_string(&anthropic::StreamingRequest {
401                    base: anthropic_request,
402                    stream: true,
403                })
404                .map_err(|e| anyhow::anyhow!(e))?;
405
406                let stream = CopilotChat::stream_messages(
407                    body,
408                    location,
409                    is_user_initiated,
410                    anthropic_beta,
411                    cx.clone(),
412                );
413
414                request_limiter
415                    .stream(async move {
416                        let events = stream.await?;
417                        let mapper = AnthropicEventMapper::new(PROVIDER_NAME, PROVIDER_ID);
418                        Ok(mapper.map_stream(events).boxed())
419                    })
420                    .await
421            });
422            return async move { Ok(future.await?.boxed()) }.boxed();
423        }
424
425        if self.model.supports_response() {
426            let location = intent_to_chat_location(request.intent);
427            let responses_request = match into_copilot_responses(&self.model, request) {
428                Ok(request) => request,
429                Err(error) => return async move { Err(error.into()) }.boxed(),
430            };
431            let request_limiter = self.request_limiter.clone();
432            let future = cx.spawn(async move |cx| {
433                let request = CopilotChat::stream_response(
434                    responses_request,
435                    location,
436                    is_user_initiated,
437                    cx.clone(),
438                );
439                request_limiter
440                    .stream(async move {
441                        let stream = request.await?;
442                        let mapper = CopilotResponsesEventMapper::new();
443                        Ok(mapper.map_stream(stream).boxed())
444                    })
445                    .await
446            });
447            return async move { Ok(future.await?.boxed()) }.boxed();
448        }
449
450        let location = intent_to_chat_location(request.intent);
451        let copilot_request = match into_copilot_chat(&self.model, request) {
452            Ok(request) => request,
453            Err(err) => return futures::future::ready(Err(err.into())).boxed(),
454        };
455        let is_streaming = copilot_request.stream;
456
457        let request_limiter = self.request_limiter.clone();
458        let future = cx.spawn(async move |cx| {
459            let request = CopilotChat::stream_completion(
460                copilot_request,
461                location,
462                is_user_initiated,
463                cx.clone(),
464            );
465            request_limiter
466                .stream(async move {
467                    let response = request.await?;
468                    Ok(map_to_language_model_completion_events(
469                        response,
470                        is_streaming,
471                    ))
472                })
473                .await
474        });
475        async move { Ok(future.await?.boxed()) }.boxed()
476    }
477}
478
479pub fn map_to_language_model_completion_events(
480    events: Pin<Box<dyn Send + Stream<Item = Result<ResponseEvent>>>>,
481    is_streaming: bool,
482) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
483    #[derive(Default)]
484    struct RawToolCall {
485        id: String,
486        name: String,
487        arguments: String,
488        thought_signature: Option<String>,
489    }
490
491    struct State {
492        events: Pin<Box<dyn Send + Stream<Item = Result<ResponseEvent>>>>,
493        tool_calls_by_index: HashMap<usize, RawToolCall>,
494        reasoning_opaque: Option<String>,
495        reasoning_text: Option<String>,
496    }
497
498    futures::stream::unfold(
499        State {
500            events,
501            tool_calls_by_index: HashMap::default(),
502            reasoning_opaque: None,
503            reasoning_text: None,
504        },
505        move |mut state| async move {
506            if let Some(event) = state.events.next().await {
507                match event {
508                    Ok(event) => {
509                        let Some(choice) = event.choices.first() else {
510                            return Some((
511                                vec![Err(anyhow!("Response contained no choices").into())],
512                                state,
513                            ));
514                        };
515
516                        let delta = if is_streaming {
517                            choice.delta.as_ref()
518                        } else {
519                            choice.message.as_ref()
520                        };
521
522                        let Some(delta) = delta else {
523                            return Some((
524                                vec![Err(anyhow!("Response contained no delta").into())],
525                                state,
526                            ));
527                        };
528
529                        let mut events = Vec::new();
530                        if let Some(content) = delta.content.clone() {
531                            events.push(Ok(LanguageModelCompletionEvent::Text(content)));
532                        }
533
534                        // Capture reasoning data from the delta (e.g. for Gemini 3)
535                        if let Some(opaque) = delta.reasoning_opaque.clone() {
536                            state.reasoning_opaque = Some(opaque);
537                        }
538                        if let Some(text) = delta.reasoning_text.clone() {
539                            state.reasoning_text = Some(text);
540                        }
541
542                        for (index, tool_call) in delta.tool_calls.iter().enumerate() {
543                            let tool_index = tool_call.index.unwrap_or(index);
544                            let entry = state.tool_calls_by_index.entry(tool_index).or_default();
545
546                            if let Some(tool_id) = tool_call.id.clone() {
547                                entry.id = tool_id;
548                            }
549
550                            if let Some(function) = tool_call.function.as_ref() {
551                                if let Some(name) = function.name.clone() {
552                                    entry.name = name;
553                                }
554
555                                if let Some(arguments) = function.arguments.clone() {
556                                    entry.arguments.push_str(&arguments);
557                                }
558
559                                if let Some(thought_signature) = function.thought_signature.clone()
560                                {
561                                    entry.thought_signature = Some(thought_signature);
562                                }
563                            }
564
565                            if !entry.id.is_empty() && !entry.name.is_empty() {
566                                if let Ok(input) = serde_json::from_str::<serde_json::Value>(
567                                    &fix_streamed_json(&entry.arguments),
568                                ) {
569                                    events.push(Ok(LanguageModelCompletionEvent::ToolUse(
570                                        LanguageModelToolUse {
571                                            id: entry.id.clone().into(),
572                                            name: entry.name.as_str().into(),
573                                            is_input_complete: false,
574                                            input: language_model::LanguageModelToolUseInput::Json(
575                                                input,
576                                            ),
577                                            raw_input: entry.arguments.clone(),
578                                            thought_signature: entry.thought_signature.clone(),
579                                        },
580                                    )));
581                                }
582                            }
583                        }
584
585                        if let Some(usage) = event.usage {
586                            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
587                                TokenUsage {
588                                    input_tokens: usage.prompt_tokens,
589                                    output_tokens: usage.completion_tokens,
590                                    cache_creation_input_tokens: 0,
591                                    cache_read_input_tokens: 0,
592                                },
593                            )));
594                        }
595
596                        match choice.finish_reason.as_deref() {
597                            Some("stop") => {
598                                events.push(Ok(LanguageModelCompletionEvent::Stop(
599                                    StopReason::EndTurn,
600                                )));
601                            }
602                            Some("tool_calls") => {
603                                // Gemini 3 models send reasoning_opaque/reasoning_text that must
604                                // be preserved and sent back in subsequent requests. Emit as
605                                // ReasoningDetails so the agent stores it in the message.
606                                if state.reasoning_opaque.is_some()
607                                    || state.reasoning_text.is_some()
608                                {
609                                    let mut details = serde_json::Map::new();
610                                    if let Some(opaque) = state.reasoning_opaque.take() {
611                                        details.insert(
612                                            "reasoning_opaque".to_string(),
613                                            serde_json::Value::String(opaque),
614                                        );
615                                    }
616                                    if let Some(text) = state.reasoning_text.take() {
617                                        details.insert(
618                                            "reasoning_text".to_string(),
619                                            serde_json::Value::String(text),
620                                        );
621                                    }
622                                    events.push(Ok(
623                                        LanguageModelCompletionEvent::ReasoningDetails(
624                                            serde_json::Value::Object(details),
625                                        ),
626                                    ));
627                                }
628
629                                events.extend(state.tool_calls_by_index.drain().map(
630                                    |(_, tool_call)| match parse_tool_arguments(
631                                        &tool_call.arguments,
632                                    ) {
633                                        Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
634                                            LanguageModelToolUse {
635                                                id: tool_call.id.into(),
636                                                name: tool_call.name.as_str().into(),
637                                                is_input_complete: true,
638                                                input:
639                                                    language_model::LanguageModelToolUseInput::Json(
640                                                        input,
641                                                    ),
642                                                raw_input: tool_call.arguments,
643                                                thought_signature: tool_call.thought_signature,
644                                            },
645                                        )),
646                                        Err(error) => Ok(
647                                            LanguageModelCompletionEvent::ToolUseJsonParseError {
648                                                id: tool_call.id.into(),
649                                                tool_name: tool_call.name.as_str().into(),
650                                                raw_input: tool_call.arguments.into(),
651                                                json_parse_error: error.to_string(),
652                                            },
653                                        ),
654                                    },
655                                ));
656
657                                events.push(Ok(LanguageModelCompletionEvent::Stop(
658                                    StopReason::ToolUse,
659                                )));
660                            }
661                            Some(stop_reason) => {
662                                log::error!("Unexpected Copilot Chat stop_reason: {stop_reason:?}");
663                                events.push(Ok(LanguageModelCompletionEvent::Stop(
664                                    StopReason::EndTurn,
665                                )));
666                            }
667                            None => {}
668                        }
669
670                        return Some((events, state));
671                    }
672                    Err(err) => return Some((vec![Err(anyhow!(err).into())], state)),
673                }
674            }
675
676            None
677        },
678    )
679    .flat_map(futures::stream::iter)
680}
681
682pub struct CopilotResponsesEventMapper {
683    pending_stop_reason: Option<StopReason>,
684    reasoning_items: Vec<copilot_responses::ResponseReasoningInputItem>,
685}
686
687impl CopilotResponsesEventMapper {
688    pub fn new() -> Self {
689        Self {
690            pending_stop_reason: None,
691            reasoning_items: Vec::new(),
692        }
693    }
694
695    pub fn map_stream(
696        mut self,
697        events: Pin<Box<dyn Send + Stream<Item = Result<copilot_responses::StreamEvent>>>>,
698    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
699    {
700        events.flat_map(move |event| {
701            futures::stream::iter(match event {
702                Ok(event) => self.map_event(event),
703                Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
704            })
705        })
706    }
707
708    fn map_event(
709        &mut self,
710        event: copilot_responses::StreamEvent,
711    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
712        match event {
713            copilot_responses::StreamEvent::OutputItemAdded { item, .. } => match item {
714                copilot_responses::ResponseOutputItem::Message { id, .. } => {
715                    vec![Ok(LanguageModelCompletionEvent::StartMessage {
716                        message_id: id,
717                    })]
718                }
719                _ => Vec::new(),
720            },
721
722            copilot_responses::StreamEvent::OutputTextDelta { delta, .. } => {
723                if delta.is_empty() {
724                    Vec::new()
725                } else {
726                    vec![Ok(LanguageModelCompletionEvent::Text(delta))]
727                }
728            }
729
730            copilot_responses::StreamEvent::OutputItemDone { item, .. } => match item {
731                copilot_responses::ResponseOutputItem::Message { .. } => Vec::new(),
732                copilot_responses::ResponseOutputItem::FunctionCall {
733                    call_id,
734                    name,
735                    arguments,
736                    thought_signature,
737                    ..
738                } => {
739                    let mut events = Vec::new();
740                    match parse_tool_arguments(&arguments) {
741                        Ok(input) => events.push(Ok(LanguageModelCompletionEvent::ToolUse(
742                            LanguageModelToolUse {
743                                id: call_id.into(),
744                                name: name.as_str().into(),
745                                is_input_complete: true,
746                                input: language_model::LanguageModelToolUseInput::Json(input),
747                                raw_input: arguments.clone(),
748                                thought_signature,
749                            },
750                        ))),
751                        Err(error) => {
752                            events.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
753                                id: call_id.into(),
754                                tool_name: name.as_str().into(),
755                                raw_input: arguments.clone().into(),
756                                json_parse_error: error.to_string(),
757                            }))
758                        }
759                    }
760                    // Record that we already emitted a tool-use stop so we can avoid duplicating
761                    // a Stop event on Completed.
762                    self.pending_stop_reason = Some(StopReason::ToolUse);
763                    events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
764                    events
765                }
766                copilot_responses::ResponseOutputItem::Reasoning {
767                    id,
768                    summary,
769                    encrypted_content,
770                } => {
771                    let mut events = Vec::new();
772
773                    if let Some(blocks) = summary.as_ref() {
774                        let mut text = String::new();
775                        for block in blocks {
776                            text.push_str(&block.text);
777                        }
778                        if !text.is_empty() {
779                            events.push(Ok(LanguageModelCompletionEvent::Thinking {
780                                text,
781                                signature: None,
782                            }));
783                        }
784                    }
785
786                    if let Some(reasoning_item) =
787                        reasoning_input_item_from_output(&id, encrypted_content)
788                    {
789                        events.extend(self.capture_reasoning_item(reasoning_item));
790                    }
791
792                    events
793                }
794            },
795
796            copilot_responses::StreamEvent::Completed { response } => {
797                let mut events = Vec::new();
798                if let Some(usage) = response.usage {
799                    events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
800                        input_tokens: usage.input_tokens.unwrap_or(0),
801                        output_tokens: usage.output_tokens.unwrap_or(0),
802                        cache_creation_input_tokens: 0,
803                        cache_read_input_tokens: 0,
804                    })));
805                }
806                if self.pending_stop_reason.take() != Some(StopReason::ToolUse) {
807                    events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
808                }
809                events
810            }
811
812            copilot_responses::StreamEvent::Incomplete { response } => {
813                let reason = response
814                    .incomplete_details
815                    .as_ref()
816                    .and_then(|details| details.reason.as_ref());
817                let stop_reason = match reason {
818                    Some(copilot_responses::IncompleteReason::MaxOutputTokens) => {
819                        StopReason::MaxTokens
820                    }
821                    Some(copilot_responses::IncompleteReason::ContentFilter) => StopReason::Refusal,
822                    _ => self
823                        .pending_stop_reason
824                        .take()
825                        .unwrap_or(StopReason::EndTurn),
826                };
827
828                let mut events = Vec::new();
829                if let Some(usage) = response.usage {
830                    events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
831                        input_tokens: usage.input_tokens.unwrap_or(0),
832                        output_tokens: usage.output_tokens.unwrap_or(0),
833                        cache_creation_input_tokens: 0,
834                        cache_read_input_tokens: 0,
835                    })));
836                }
837                events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
838                events
839            }
840
841            copilot_responses::StreamEvent::Failed { response } => {
842                let provider = PROVIDER_NAME;
843                let (status_code, message) = match response.error {
844                    Some(error) => {
845                        let status_code = StatusCode::from_str(&error.code)
846                            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
847                        (status_code, error.message)
848                    }
849                    None => (
850                        StatusCode::INTERNAL_SERVER_ERROR,
851                        "response.failed".to_string(),
852                    ),
853                };
854                vec![Err(LanguageModelCompletionError::HttpResponseError {
855                    provider,
856                    status_code,
857                    message,
858                })]
859            }
860
861            copilot_responses::StreamEvent::GenericError { error } => vec![Err(
862                LanguageModelCompletionError::Other(anyhow!(error.message)),
863            )],
864
865            copilot_responses::StreamEvent::Created { .. }
866            | copilot_responses::StreamEvent::Unknown => Vec::new(),
867        }
868    }
869
870    fn capture_reasoning_item(
871        &mut self,
872        reasoning_item: copilot_responses::ResponseReasoningInputItem,
873    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
874        if self.reasoning_items.contains(&reasoning_item) {
875            return Vec::new();
876        }
877
878        if let Some(id) = reasoning_item.id.as_ref()
879            && let Some(existing_reasoning_item) = self
880                .reasoning_items
881                .iter_mut()
882                .find(|existing_reasoning_item| existing_reasoning_item.id.as_ref() == Some(id))
883        {
884            *existing_reasoning_item = reasoning_item;
885        } else {
886            self.reasoning_items.push(reasoning_item);
887        }
888
889        self.emit_response_message_metadata()
890    }
891
892    fn emit_response_message_metadata(
893        &self,
894    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
895        let details = serde_json::to_value(CopilotResponseMessageMetadata {
896            reasoning_items: self.reasoning_items.clone(),
897        });
898
899        match details {
900            Ok(details) => vec![Ok(LanguageModelCompletionEvent::ReasoningDetails(details))],
901            Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
902        }
903    }
904}
905
906#[derive(serde::Serialize, serde::Deserialize)]
907struct CopilotResponseMessageMetadata {
908    #[serde(default, skip_serializing_if = "Vec::is_empty")]
909    reasoning_items: Vec<copilot_responses::ResponseReasoningInputItem>,
910}
911
912fn append_reasoning_details_to_response_items(
913    reasoning_details: Option<&serde_json::Value>,
914    replayed_reasoning_item_indexes: &mut HashMap<String, usize>,
915    input_items: &mut Vec<copilot_responses::ResponseInputItem>,
916) {
917    let Some(reasoning_details) = reasoning_details else {
918        return;
919    };
920
921    let Some(metadata) =
922        serde_json::from_value::<CopilotResponseMessageMetadata>(reasoning_details.clone()).ok()
923    else {
924        return;
925    };
926
927    for mut reasoning_item in metadata.reasoning_items {
928        reasoning_item.summary.clear();
929        if let Some(id) = reasoning_item.id.as_ref() {
930            if let Some(index) = replayed_reasoning_item_indexes.get(id) {
931                input_items[*index] =
932                    copilot_responses::ResponseInputItem::Reasoning(reasoning_item);
933                return;
934            }
935
936            replayed_reasoning_item_indexes.insert(id.clone(), input_items.len());
937        }
938
939        input_items.push(copilot_responses::ResponseInputItem::Reasoning(
940            reasoning_item,
941        ));
942    }
943}
944
945fn reasoning_input_item_from_output(
946    id: &str,
947    encrypted_content: Option<String>,
948) -> Option<copilot_responses::ResponseReasoningInputItem> {
949    if encrypted_content.is_none() {
950        return None;
951    }
952    Some(copilot_responses::ResponseReasoningInputItem {
953        id: Some(id.to_string()),
954        summary: Vec::new(),
955        encrypted_content,
956    })
957}
958
959fn into_copilot_chat(
960    model: &CopilotChatModel,
961    request: LanguageModelRequest,
962) -> Result<CopilotChatRequest> {
963    let temperature = request.temperature;
964    let tool_choice = request.tool_choice;
965    let thinking_allowed = request.thinking_allowed;
966
967    let mut request_messages: Vec<LanguageModelRequestMessage> = Vec::new();
968    for message in request.messages {
969        if let Some(last_message) = request_messages.last_mut() {
970            if last_message.role == message.role {
971                last_message.content.extend(message.content);
972            } else {
973                request_messages.push(message);
974            }
975        } else {
976            request_messages.push(message);
977        }
978    }
979
980    let mut messages: Vec<ChatMessage> = Vec::new();
981    for message in request_messages {
982        match message.role {
983            Role::User => {
984                for content in &message.content {
985                    if let MessageContent::ToolResult(tool_result) = content {
986                        let parts: Vec<ChatMessagePart> = tool_result
987                            .content
988                            .iter()
989                            .map(|part| match part {
990                                LanguageModelToolResultContent::Text(text) => {
991                                    ChatMessagePart::Text {
992                                        text: text.to_string(),
993                                    }
994                                }
995                                LanguageModelToolResultContent::Image(image) => {
996                                    if model.supports_vision() {
997                                        ChatMessagePart::Image {
998                                            image_url: ImageUrl {
999                                                url: image.to_base64_url(),
1000                                            },
1001                                        }
1002                                    } else {
1003                                        debug_panic!(
1004                                            "This should be caught at {} level",
1005                                            tool_result.tool_name
1006                                        );
1007                                        ChatMessagePart::Text {
1008                                            text: "[Tool responded with an image, but this model does not support vision]".to_string(),
1009                                        }
1010                                    }
1011                                }
1012                            })
1013                            .collect();
1014
1015                        let content = match parts.as_slice() {
1016                            [ChatMessagePart::Text { text }] => {
1017                                ChatMessageContent::Plain(text.clone())
1018                            }
1019                            _ => ChatMessageContent::Multipart(parts),
1020                        };
1021
1022                        messages.push(ChatMessage::Tool {
1023                            tool_call_id: tool_result.tool_use_id.to_string(),
1024                            content,
1025                        });
1026                    }
1027                }
1028
1029                let mut content_parts = Vec::new();
1030                for content in &message.content {
1031                    match content {
1032                        MessageContent::Text(text) | MessageContent::Thinking { text, .. }
1033                            if !text.is_empty() =>
1034                        {
1035                            if let Some(ChatMessagePart::Text { text: text_content }) =
1036                                content_parts.last_mut()
1037                            {
1038                                text_content.push_str(text);
1039                            } else {
1040                                content_parts.push(ChatMessagePart::Text {
1041                                    text: text.to_string(),
1042                                });
1043                            }
1044                        }
1045                        MessageContent::Image(image) if model.supports_vision() => {
1046                            content_parts.push(ChatMessagePart::Image {
1047                                image_url: ImageUrl {
1048                                    url: image.to_base64_url(),
1049                                },
1050                            });
1051                        }
1052                        _ => {}
1053                    }
1054                }
1055
1056                if !content_parts.is_empty() {
1057                    messages.push(ChatMessage::User {
1058                        content: content_parts.into(),
1059                    });
1060                }
1061            }
1062            Role::Assistant => {
1063                let mut tool_calls = Vec::new();
1064                for content in &message.content {
1065                    if let MessageContent::ToolUse(tool_use) = content {
1066                        let input = tool_use.input.as_json().ok_or_else(|| {
1067                            anyhow!("Copilot Chat does not support custom tool calls")
1068                        })?;
1069                        tool_calls.push(ToolCall {
1070                            id: tool_use.id.to_string(),
1071                            content: ToolCallContent::Function {
1072                                function: FunctionContent {
1073                                    name: tool_use.name.to_string(),
1074                                    arguments: serde_json::to_string(input)?,
1075                                    thought_signature: tool_use.thought_signature.clone(),
1076                                },
1077                            },
1078                        });
1079                    }
1080                }
1081
1082                let text_content = {
1083                    let mut buffer = String::new();
1084                    for string in message.content.iter().filter_map(|content| match content {
1085                        MessageContent::Text(text) => Some(text.as_str()),
1086                        MessageContent::Thinking { .. }
1087                        | MessageContent::ToolUse(_)
1088                        | MessageContent::RedactedThinking(_)
1089                        | MessageContent::ToolResult(_)
1090                        | MessageContent::Image(_)
1091                        | MessageContent::Compaction(_) => None,
1092                    }) {
1093                        buffer.push_str(string);
1094                    }
1095
1096                    buffer
1097                };
1098
1099                // Extract reasoning_opaque and reasoning_text from reasoning_details
1100                let (reasoning_opaque, reasoning_text) =
1101                    if let Some(details) = &message.reasoning_details {
1102                        let opaque = details
1103                            .get("reasoning_opaque")
1104                            .and_then(|v| v.as_str())
1105                            .map(|s| s.to_string());
1106                        let text = details
1107                            .get("reasoning_text")
1108                            .and_then(|v| v.as_str())
1109                            .map(|s| s.to_string());
1110                        (opaque, text)
1111                    } else {
1112                        (None, None)
1113                    };
1114
1115                messages.push(ChatMessage::Assistant {
1116                    content: if text_content.is_empty() {
1117                        ChatMessageContent::empty()
1118                    } else {
1119                        text_content.into()
1120                    },
1121                    tool_calls,
1122                    reasoning_opaque,
1123                    reasoning_text,
1124                });
1125            }
1126            Role::System => messages.push(ChatMessage::System {
1127                content: message.string_contents(),
1128            }),
1129        }
1130    }
1131
1132    let tools = request
1133        .tools
1134        .iter()
1135        .map(|tool| match &tool.input {
1136            language_model::LanguageModelRequestToolInput::Function { input_schema, .. } => {
1137                Ok(Tool::Function {
1138                    function: Function {
1139                        name: tool.name.clone(),
1140                        description: tool.description.clone(),
1141                        parameters: input_schema.clone(),
1142                    },
1143                })
1144            }
1145            language_model::LanguageModelRequestToolInput::Custom { .. } => Err(anyhow::anyhow!(
1146                "Copilot Chat does not support custom tools"
1147            )),
1148        })
1149        .collect::<Result<Vec<_>>>()?;
1150
1151    Ok(CopilotChatRequest {
1152        n: 1,
1153        stream: model.uses_streaming(),
1154        temperature: temperature.unwrap_or(0.1),
1155        model: model.id().to_string(),
1156        messages,
1157        tools,
1158        tool_choice: tool_choice.map(|choice| match choice {
1159            LanguageModelToolChoice::Auto => ToolChoice::Auto,
1160            LanguageModelToolChoice::Any => ToolChoice::Required,
1161            LanguageModelToolChoice::None => ToolChoice::None,
1162        }),
1163        thinking_budget: if thinking_allowed && model.supports_thinking() {
1164            compute_thinking_budget(
1165                model.min_thinking_budget(),
1166                model.max_thinking_budget(),
1167                model.max_output_tokens() as u32,
1168            )
1169        } else {
1170            None
1171        },
1172    })
1173}
1174
1175fn compute_thinking_budget(
1176    min_budget: Option<u32>,
1177    max_budget: Option<u32>,
1178    max_output_tokens: u32,
1179) -> Option<u32> {
1180    let configured_budget: u32 = 16000;
1181    let min_budget = min_budget.unwrap_or(1024);
1182    let max_budget = max_budget.unwrap_or(max_output_tokens.saturating_sub(1));
1183    let normalized = configured_budget.max(min_budget);
1184    Some(
1185        normalized
1186            .min(max_budget)
1187            .min(max_output_tokens.saturating_sub(1)),
1188    )
1189}
1190
1191fn intent_to_chat_location(intent: Option<CompletionIntent>) -> ChatLocation {
1192    match intent {
1193        Some(CompletionIntent::UserPrompt) => ChatLocation::Agent,
1194        Some(CompletionIntent::Subagent) => ChatLocation::Agent,
1195        Some(CompletionIntent::ToolResults) => ChatLocation::Agent,
1196        Some(CompletionIntent::ThreadSummarization) => ChatLocation::Panel,
1197        Some(CompletionIntent::ThreadContextSummarization) => ChatLocation::Panel,
1198        Some(CompletionIntent::CreateFile) => ChatLocation::Agent,
1199        Some(CompletionIntent::EditFile) => ChatLocation::Agent,
1200        Some(CompletionIntent::InlineAssist) => ChatLocation::Editor,
1201        Some(CompletionIntent::TerminalInlineAssist) => ChatLocation::Terminal,
1202        Some(CompletionIntent::GenerateGitCommitMessage) => ChatLocation::Other,
1203        None => ChatLocation::Panel,
1204    }
1205}
1206
1207fn into_copilot_responses(
1208    model: &CopilotChatModel,
1209    request: LanguageModelRequest,
1210) -> Result<copilot_responses::Request> {
1211    use copilot_responses as responses;
1212
1213    let LanguageModelRequest {
1214        thread_id: _,
1215        prompt_id: _,
1216        intent: _,
1217        messages,
1218        tools,
1219        tool_choice,
1220        stop: _,
1221        temperature,
1222        thinking_allowed,
1223        thinking_effort,
1224        speed: _,
1225        compact_at_tokens: _,
1226    } = request;
1227
1228    let mut input_items: Vec<responses::ResponseInputItem> = Vec::new();
1229    let mut replayed_reasoning_item_indexes = HashMap::default();
1230
1231    for message in messages {
1232        match message.role {
1233            Role::User => {
1234                for content in &message.content {
1235                    if let MessageContent::ToolResult(tool_result) = content {
1236                        let output = match tool_result.content.as_slice() {
1237                            [LanguageModelToolResultContent::Text(text)] => {
1238                                responses::ResponseFunctionOutput::Text(text.to_string())
1239                            }
1240                            _ => {
1241                                let parts = tool_result
1242                                    .content
1243                                    .iter()
1244                                    .map(|part| match part {
1245                                        LanguageModelToolResultContent::Text(text) => {
1246                                            responses::ResponseInputContent::InputText {
1247                                                text: text.to_string(),
1248                                            }
1249                                        }
1250                                        LanguageModelToolResultContent::Image(image) => {
1251                                            if model.supports_vision() {
1252                                                responses::ResponseInputContent::InputImage {
1253                                                    image_url: Some(image.to_base64_url()),
1254                                                    detail: Default::default(),
1255                                                }
1256                                            } else {
1257                                                debug_panic!(
1258                                                    "This should be caught at {} level",
1259                                                    tool_result.tool_name
1260                                                );
1261                                                responses::ResponseInputContent::InputText {
1262                                                    text: "[Tool responded with an image, but this model does not support vision]".to_string(),
1263                                                }
1264                                            }
1265                                        }
1266                                    })
1267                                    .collect();
1268                                responses::ResponseFunctionOutput::Content(parts)
1269                            }
1270                        };
1271
1272                        input_items.push(responses::ResponseInputItem::FunctionCallOutput {
1273                            call_id: tool_result.tool_use_id.to_string(),
1274                            output,
1275                            status: None,
1276                        });
1277                    }
1278                }
1279
1280                let mut parts: Vec<responses::ResponseInputContent> = Vec::new();
1281                for content in &message.content {
1282                    match content {
1283                        MessageContent::Text(text) => {
1284                            parts.push(responses::ResponseInputContent::InputText {
1285                                text: text.clone(),
1286                            });
1287                        }
1288
1289                        MessageContent::Image(image) => {
1290                            if model.supports_vision() {
1291                                parts.push(responses::ResponseInputContent::InputImage {
1292                                    image_url: Some(image.to_base64_url()),
1293                                    detail: Default::default(),
1294                                });
1295                            }
1296                        }
1297                        _ => {}
1298                    }
1299                }
1300
1301                if !parts.is_empty() {
1302                    input_items.push(responses::ResponseInputItem::Message {
1303                        role: "user".into(),
1304                        content: Some(parts),
1305                        status: None,
1306                    });
1307                }
1308            }
1309
1310            Role::Assistant => {
1311                append_reasoning_details_to_response_items(
1312                    message.reasoning_details.as_deref(),
1313                    &mut replayed_reasoning_item_indexes,
1314                    &mut input_items,
1315                );
1316
1317                for content in &message.content {
1318                    if let MessageContent::ToolUse(tool_use) = content {
1319                        input_items.push(responses::ResponseInputItem::FunctionCall {
1320                            call_id: tool_use.id.to_string(),
1321                            name: tool_use.name.to_string(),
1322                            arguments: tool_use.raw_input.clone(),
1323                            status: None,
1324                            thought_signature: tool_use.thought_signature.clone(),
1325                        });
1326                    }
1327                }
1328
1329                let mut parts: Vec<responses::ResponseInputContent> = Vec::new();
1330                for content in &message.content {
1331                    match content {
1332                        MessageContent::Text(text) => {
1333                            parts.push(responses::ResponseInputContent::OutputText {
1334                                text: text.clone(),
1335                            });
1336                        }
1337                        MessageContent::Image(_) => {
1338                            parts.push(responses::ResponseInputContent::OutputText {
1339                                text: "[image omitted]".to_string(),
1340                            });
1341                        }
1342                        _ => {}
1343                    }
1344                }
1345
1346                if !parts.is_empty() {
1347                    input_items.push(responses::ResponseInputItem::Message {
1348                        role: "assistant".into(),
1349                        content: Some(parts),
1350                        status: Some("completed".into()),
1351                    });
1352                }
1353            }
1354
1355            Role::System => {
1356                let mut parts: Vec<responses::ResponseInputContent> = Vec::new();
1357                for content in &message.content {
1358                    if let MessageContent::Text(text) = content {
1359                        parts.push(responses::ResponseInputContent::InputText {
1360                            text: text.clone(),
1361                        });
1362                    }
1363                }
1364
1365                if !parts.is_empty() {
1366                    input_items.push(responses::ResponseInputItem::Message {
1367                        role: "system".into(),
1368                        content: Some(parts),
1369                        status: None,
1370                    });
1371                }
1372            }
1373        }
1374    }
1375
1376    let converted_tools: Vec<responses::ToolDefinition> = tools
1377        .into_iter()
1378        .map(|tool| match tool.input {
1379            language_model::LanguageModelRequestToolInput::Function { input_schema, .. } => {
1380                Ok(responses::ToolDefinition::Function {
1381                    name: tool.name,
1382                    description: Some(tool.description),
1383                    parameters: Some(input_schema),
1384                    strict: None,
1385                })
1386            }
1387            language_model::LanguageModelRequestToolInput::Custom { .. } => Err(anyhow::anyhow!(
1388                "Copilot Chat does not support custom tools"
1389            )),
1390        })
1391        .collect::<Result<_>>()?;
1392
1393    let mapped_tool_choice = tool_choice.map(|choice| match choice {
1394        LanguageModelToolChoice::Auto => responses::ToolChoice::Auto,
1395        LanguageModelToolChoice::Any => responses::ToolChoice::Required,
1396        LanguageModelToolChoice::None => responses::ToolChoice::None,
1397    });
1398
1399    Ok(responses::Request {
1400        model: model.id().to_string(),
1401        input: input_items,
1402        stream: model.uses_streaming(),
1403        temperature,
1404        tools: converted_tools,
1405        tool_choice: mapped_tool_choice,
1406        reasoning: if thinking_allowed {
1407            let effort = thinking_effort
1408                .as_deref()
1409                .and_then(|e| e.parse::<copilot_responses::ReasoningEffort>().ok())
1410                .unwrap_or(copilot_responses::ReasoningEffort::Medium);
1411            Some(copilot_responses::ReasoningConfig {
1412                effort,
1413                summary: Some(copilot_responses::ReasoningSummary::Detailed),
1414            })
1415        } else {
1416            None
1417        },
1418        include: Some(vec![
1419            copilot_responses::ResponseIncludable::ReasoningEncryptedContent,
1420        ]),
1421        store: false,
1422    })
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427    use super::*;
1428    use copilot_chat::responses;
1429    use futures::StreamExt;
1430    use serde_json::json;
1431
1432    fn map_events(events: Vec<responses::StreamEvent>) -> Vec<LanguageModelCompletionEvent> {
1433        futures::executor::block_on(async {
1434            CopilotResponsesEventMapper::new()
1435                .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
1436                .collect::<Vec<_>>()
1437                .await
1438                .into_iter()
1439                .map(Result::unwrap)
1440                .collect()
1441        })
1442    }
1443
1444    fn test_responses_model() -> CopilotChatModel {
1445        serde_json::from_value(json!({
1446            "billing": {
1447                "is_premium": false,
1448                "multiplier": 1.0
1449            },
1450            "capabilities": {
1451                "family": "test",
1452                "limits": {
1453                    "max_context_window_tokens": 128000,
1454                    "max_output_tokens": 4096
1455                },
1456                "supports": {
1457                    "streaming": true,
1458                    "tool_calls": true,
1459                    "parallel_tool_calls": false,
1460                    "vision": false
1461                },
1462                "type": "chat"
1463            },
1464            "id": "test-model",
1465            "is_chat_default": false,
1466            "is_chat_fallback": false,
1467            "model_picker_enabled": true,
1468            "name": "Test Model",
1469            "vendor": "OpenAI",
1470            "supported_endpoints": ["/responses"]
1471        }))
1472        .expect("valid test model")
1473    }
1474
1475    #[test]
1476    fn responses_stream_maps_text_and_usage() {
1477        let events = vec![
1478            responses::StreamEvent::OutputItemAdded {
1479                output_index: 0,
1480                sequence_number: None,
1481                item: responses::ResponseOutputItem::Message {
1482                    id: "msg_1".into(),
1483                    role: "assistant".into(),
1484                    content: Some(Vec::new()),
1485                },
1486            },
1487            responses::StreamEvent::OutputTextDelta {
1488                item_id: "msg_1".into(),
1489                output_index: 0,
1490                delta: "Hello".into(),
1491            },
1492            responses::StreamEvent::Completed {
1493                response: responses::Response {
1494                    usage: Some(responses::ResponseUsage {
1495                        input_tokens: Some(5),
1496                        output_tokens: Some(3),
1497                        total_tokens: Some(8),
1498                    }),
1499                    ..Default::default()
1500                },
1501            },
1502        ];
1503
1504        let mapped = map_events(events);
1505        assert!(matches!(
1506            mapped[0],
1507            LanguageModelCompletionEvent::StartMessage { ref message_id } if message_id == "msg_1"
1508        ));
1509        assert!(matches!(
1510            mapped[1],
1511            LanguageModelCompletionEvent::Text(ref text) if text == "Hello"
1512        ));
1513        assert!(matches!(
1514            mapped[2],
1515            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1516                input_tokens: 5,
1517                output_tokens: 3,
1518                ..
1519            })
1520        ));
1521        assert!(matches!(
1522            mapped[3],
1523            LanguageModelCompletionEvent::Stop(StopReason::EndTurn)
1524        ));
1525    }
1526
1527    #[test]
1528    fn responses_stream_maps_tool_calls() {
1529        let events = vec![responses::StreamEvent::OutputItemDone {
1530            output_index: 0,
1531            sequence_number: None,
1532            item: responses::ResponseOutputItem::FunctionCall {
1533                id: Some("fn_1".into()),
1534                call_id: "call_1".into(),
1535                name: "do_it".into(),
1536                arguments: "{\"x\":1}".into(),
1537                status: None,
1538                thought_signature: None,
1539            },
1540        }];
1541
1542        let mapped = map_events(events);
1543        assert!(matches!(
1544            mapped[0],
1545            LanguageModelCompletionEvent::ToolUse(ref use_) if use_.id.to_string() == "call_1" && use_.name.as_ref() == "do_it"
1546        ));
1547        assert!(matches!(
1548            mapped[1],
1549            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1550        ));
1551    }
1552
1553    #[test]
1554    fn responses_stream_handles_json_parse_error() {
1555        let events = vec![responses::StreamEvent::OutputItemDone {
1556            output_index: 0,
1557            sequence_number: None,
1558            item: responses::ResponseOutputItem::FunctionCall {
1559                id: Some("fn_1".into()),
1560                call_id: "call_1".into(),
1561                name: "do_it".into(),
1562                arguments: "{not json}".into(),
1563                status: None,
1564                thought_signature: None,
1565            },
1566        }];
1567
1568        let mapped = map_events(events);
1569        assert!(matches!(
1570            mapped[0],
1571            LanguageModelCompletionEvent::ToolUseJsonParseError { ref id, ref tool_name, .. }
1572                if id.to_string() == "call_1" && tool_name.as_ref() == "do_it"
1573        ));
1574        assert!(matches!(
1575            mapped[1],
1576            LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1577        ));
1578    }
1579
1580    #[test]
1581    fn responses_stream_maps_reasoning_summary_and_encrypted_content() {
1582        let events = vec![responses::StreamEvent::OutputItemDone {
1583            output_index: 0,
1584            sequence_number: None,
1585            item: responses::ResponseOutputItem::Reasoning {
1586                id: "r1".into(),
1587                summary: Some(vec![responses::ResponseReasoningItem {
1588                    kind: "summary_text".into(),
1589                    text: "Chain".into(),
1590                }]),
1591                encrypted_content: Some("ENC".into()),
1592            },
1593        }];
1594
1595        let mapped = map_events(events);
1596        assert!(matches!(
1597            mapped[0],
1598            LanguageModelCompletionEvent::Thinking { ref text, signature: None } if text == "Chain"
1599        ));
1600        match &mapped[1] {
1601            LanguageModelCompletionEvent::ReasoningDetails(details) => assert_eq!(
1602                details,
1603                &json!({
1604                    "reasoning_items": [
1605                        {
1606                            "id": "r1",
1607                            "summary": [],
1608                            "encrypted_content": "ENC"
1609                        }
1610                    ]
1611                })
1612            ),
1613            other => panic!("expected reasoning details, got {other:?}"),
1614        }
1615    }
1616
1617    #[test]
1618    fn responses_stream_ignores_reasoning_items_repeated_in_completed_output() {
1619        let events = vec![
1620            responses::StreamEvent::OutputItemDone {
1621                output_index: 0,
1622                sequence_number: None,
1623                item: responses::ResponseOutputItem::Reasoning {
1624                    id: "r1".into(),
1625                    summary: Some(Vec::new()),
1626                    encrypted_content: Some("ENC1".into()),
1627                },
1628            },
1629            responses::StreamEvent::Completed {
1630                response: responses::Response {
1631                    output: vec![
1632                        responses::ResponseOutputItem::Reasoning {
1633                            id: "r1".into(),
1634                            summary: Some(Vec::new()),
1635                            encrypted_content: Some("ENC1".into()),
1636                        },
1637                        responses::ResponseOutputItem::Reasoning {
1638                            id: "r2".into(),
1639                            summary: Some(Vec::new()),
1640                            encrypted_content: Some("ENC2".into()),
1641                        },
1642                    ],
1643                    ..Default::default()
1644                },
1645            },
1646        ];
1647
1648        let mapped = map_events(events);
1649        let reasoning_details = mapped
1650            .iter()
1651            .filter_map(|event| match event {
1652                LanguageModelCompletionEvent::ReasoningDetails(details) => Some(details),
1653                _ => None,
1654            })
1655            .collect::<Vec<_>>();
1656
1657        assert_eq!(
1658            reasoning_details,
1659            vec![&json!({
1660                "reasoning_items": [
1661                    {
1662                        "id": "r1",
1663                        "summary": [],
1664                        "encrypted_content": "ENC1"
1665                    }
1666                ]
1667            })]
1668        );
1669    }
1670
1671    #[test]
1672    fn into_copilot_responses_replays_reasoning_details() {
1673        let model = test_responses_model();
1674        let request = LanguageModelRequest {
1675            messages: vec![LanguageModelRequestMessage {
1676                role: Role::Assistant,
1677                content: vec![
1678                    MessageContent::RedactedThinking("legacy-redacted".into()),
1679                    MessageContent::Text("Done".into()),
1680                ],
1681                cache: false,
1682                reasoning_details: Some(Arc::new(json!({
1683                    "reasoning_items": [
1684                        {
1685                            "id": "r1",
1686                            "summary": [
1687                                {
1688                                    "type": "summary_text",
1689                                    "text": "Chain"
1690                                }
1691                            ],
1692                            "encrypted_content": "ENC"
1693                        }
1694                    ]
1695                }))),
1696            }],
1697            ..Default::default()
1698        };
1699
1700        let serialized = serde_json::to_value(into_copilot_responses(&model, request).unwrap())
1701            .expect("serialized request");
1702        let input = serialized["input"].as_array().expect("input items");
1703
1704        assert_eq!(
1705            input.first(),
1706            Some(&json!({
1707                "type": "reasoning",
1708                "id": "r1",
1709                "summary": [],
1710                "encrypted_content": "ENC"
1711            }))
1712        );
1713        assert_eq!(
1714            input.get(1),
1715            Some(&json!({
1716                "type": "message",
1717                "role": "assistant",
1718                "content": [
1719                    {
1720                        "type": "output_text",
1721                        "text": "Done"
1722                    }
1723                ],
1724                "status": "completed"
1725            }))
1726        );
1727        assert!(!serialized.to_string().contains("legacy-redacted"));
1728    }
1729
1730    #[test]
1731    fn responses_stream_handles_incomplete_max_tokens() {
1732        let events = vec![responses::StreamEvent::Incomplete {
1733            response: responses::Response {
1734                usage: Some(responses::ResponseUsage {
1735                    input_tokens: Some(10),
1736                    output_tokens: Some(0),
1737                    total_tokens: Some(10),
1738                }),
1739                incomplete_details: Some(responses::IncompleteDetails {
1740                    reason: Some(responses::IncompleteReason::MaxOutputTokens),
1741                }),
1742                ..Default::default()
1743            },
1744        }];
1745
1746        let mapped = map_events(events);
1747        assert!(matches!(
1748            mapped[0],
1749            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1750                input_tokens: 10,
1751                output_tokens: 0,
1752                ..
1753            })
1754        ));
1755        assert!(matches!(
1756            mapped[1],
1757            LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1758        ));
1759    }
1760
1761    #[test]
1762    fn responses_stream_handles_incomplete_content_filter() {
1763        let events = vec![responses::StreamEvent::Incomplete {
1764            response: responses::Response {
1765                usage: None,
1766                incomplete_details: Some(responses::IncompleteDetails {
1767                    reason: Some(responses::IncompleteReason::ContentFilter),
1768                }),
1769                ..Default::default()
1770            },
1771        }];
1772
1773        let mapped = map_events(events);
1774        assert!(matches!(
1775            mapped.last().unwrap(),
1776            LanguageModelCompletionEvent::Stop(StopReason::Refusal)
1777        ));
1778    }
1779
1780    #[test]
1781    fn responses_stream_completed_no_duplicate_after_tool_use() {
1782        let events = vec![
1783            responses::StreamEvent::OutputItemDone {
1784                output_index: 0,
1785                sequence_number: None,
1786                item: responses::ResponseOutputItem::FunctionCall {
1787                    id: Some("fn_1".into()),
1788                    call_id: "call_1".into(),
1789                    name: "do_it".into(),
1790                    arguments: "{}".into(),
1791                    status: None,
1792                    thought_signature: None,
1793                },
1794            },
1795            responses::StreamEvent::Completed {
1796                response: responses::Response::default(),
1797            },
1798        ];
1799
1800        let mapped = map_events(events);
1801
1802        let mut stop_count = 0usize;
1803        let mut saw_tool_use_stop = false;
1804        for event in mapped {
1805            if let LanguageModelCompletionEvent::Stop(reason) = event {
1806                stop_count += 1;
1807                if matches!(reason, StopReason::ToolUse) {
1808                    saw_tool_use_stop = true;
1809                }
1810            }
1811        }
1812        assert_eq!(stop_count, 1, "should emit exactly one Stop event");
1813        assert!(saw_tool_use_stop, "Stop reason should be ToolUse");
1814    }
1815
1816    #[test]
1817    fn responses_stream_failed_maps_http_response_error() {
1818        let events = vec![responses::StreamEvent::Failed {
1819            response: responses::Response {
1820                error: Some(responses::ResponseError {
1821                    code: "429".into(),
1822                    message: "too many requests".into(),
1823                }),
1824                ..Default::default()
1825            },
1826        }];
1827
1828        let mapped_results = futures::executor::block_on(async {
1829            CopilotResponsesEventMapper::new()
1830                .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
1831                .collect::<Vec<_>>()
1832                .await
1833        });
1834
1835        assert_eq!(mapped_results.len(), 1);
1836        match &mapped_results[0] {
1837            Err(LanguageModelCompletionError::HttpResponseError {
1838                status_code,
1839                message,
1840                ..
1841            }) => {
1842                assert_eq!(*status_code, http_client::StatusCode::TOO_MANY_REQUESTS);
1843                assert_eq!(message, "too many requests");
1844            }
1845            other => panic!("expected HttpResponseError, got {:?}", other),
1846        }
1847    }
1848
1849    #[test]
1850    fn chat_completions_stream_maps_reasoning_data() {
1851        use copilot_chat::{
1852            FunctionChunk, ResponseChoice, ResponseDelta, ResponseEvent, Role, ToolCallChunk,
1853        };
1854
1855        let events = vec![
1856            ResponseEvent {
1857                choices: vec![ResponseChoice {
1858                    index: Some(0),
1859                    finish_reason: None,
1860                    delta: Some(ResponseDelta {
1861                        content: None,
1862                        role: Some(Role::Assistant),
1863                        tool_calls: vec![ToolCallChunk {
1864                            index: Some(0),
1865                            id: Some("call_abc123".to_string()),
1866                            function: Some(FunctionChunk {
1867                                name: Some("list_directory".to_string()),
1868                                arguments: Some("{\"path\":\"test\"}".to_string()),
1869                                thought_signature: None,
1870                            }),
1871                        }],
1872                        reasoning_opaque: Some("encrypted_reasoning_token_xyz".to_string()),
1873                        reasoning_text: Some("Let me check the directory".to_string()),
1874                    }),
1875                    message: None,
1876                }],
1877                id: "chatcmpl-123".to_string(),
1878                usage: None,
1879            },
1880            ResponseEvent {
1881                choices: vec![ResponseChoice {
1882                    index: Some(0),
1883                    finish_reason: Some("tool_calls".to_string()),
1884                    delta: Some(ResponseDelta {
1885                        content: None,
1886                        role: None,
1887                        tool_calls: vec![],
1888                        reasoning_opaque: None,
1889                        reasoning_text: None,
1890                    }),
1891                    message: None,
1892                }],
1893                id: "chatcmpl-123".to_string(),
1894                usage: None,
1895            },
1896        ];
1897
1898        let mapped = futures::executor::block_on(async {
1899            map_to_language_model_completion_events(
1900                Box::pin(futures::stream::iter(events.into_iter().map(Ok))),
1901                true,
1902            )
1903            .collect::<Vec<_>>()
1904            .await
1905        });
1906
1907        let mut has_reasoning_details = false;
1908        let mut has_tool_use = false;
1909        let mut reasoning_opaque_value: Option<String> = None;
1910        let mut reasoning_text_value: Option<String> = None;
1911
1912        for event_result in mapped {
1913            match event_result {
1914                Ok(LanguageModelCompletionEvent::ReasoningDetails(details)) => {
1915                    has_reasoning_details = true;
1916                    reasoning_opaque_value = details
1917                        .get("reasoning_opaque")
1918                        .and_then(|v| v.as_str())
1919                        .map(|s| s.to_string());
1920                    reasoning_text_value = details
1921                        .get("reasoning_text")
1922                        .and_then(|v| v.as_str())
1923                        .map(|s| s.to_string());
1924                }
1925                Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => {
1926                    has_tool_use = true;
1927                    assert_eq!(tool_use.id.to_string(), "call_abc123");
1928                    assert_eq!(tool_use.name.as_ref(), "list_directory");
1929                }
1930                _ => {}
1931            }
1932        }
1933
1934        assert!(
1935            has_reasoning_details,
1936            "Should emit ReasoningDetails event for Gemini 3 reasoning"
1937        );
1938        assert!(has_tool_use, "Should emit ToolUse event");
1939        assert_eq!(
1940            reasoning_opaque_value,
1941            Some("encrypted_reasoning_token_xyz".to_string()),
1942            "Should capture reasoning_opaque"
1943        );
1944        assert_eq!(
1945            reasoning_text_value,
1946            Some("Let me check the directory".to_string()),
1947            "Should capture reasoning_text"
1948        );
1949    }
1950}
1951
Served at tenant.openagents/omega Member data and write actions are omitted.