Skip to content

Commit 92c3127

Browse files
authored
Add support for GitHub Copilot /responses endpoint (#40762)
Add support for GithubCopilot /responses endpoint. This gives the copilot chat provider the ability to use the new GPT-5 codex model and any other model that lacks support for /chat/copmletions endpoint. Closes #38858 Release Notes: - Add support for GithubCopilot /responses endpoint. # Added 1. copilot_response.rs that has the /response endpoint types 2. uses response endpoint if model does not support /chat/completions. 3. new into_copilot_response() to map LanguageCompletionEvents to Request. 4. new map_stream() to map response stream event to LanguageCompletionEvents and tests. 5. Fixed a bug where trying to parse response for non streaming for /chat/completion was failing # Notes There is a pr open - #39989 for adding /response support for OpenAi and OpenAi compatible API. Altough they share some similarities (copilot api seems to mirror openAi directly) ive simplified some stuff and tried to keep it the same with the vscode-chat implementation where possible. There might be a case for code reuse but i think keeping them separate for now should be ok. # Tool Calls <img width="716" height="670" alt="Screenshot from 2025-10-15 17-12-30" src="https://github.com/user-attachments/assets/14e88a52-ba8b-4209-8f78-73d15034b1e0" /> # Image <img width="923" height="494" alt="Screenshot from 2025-10-21 02-02-26" src="https://github.com/user-attachments/assets/b96ce97c-331e-45cb-b5b1-7aa10ed387b4" />
1 parent a7c5b8d commit 92c3127

4 files changed

Lines changed: 1155 additions & 24 deletions

File tree

crates/copilot/src/copilot.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod copilot_chat;
22
mod copilot_completion_provider;
3+
pub mod copilot_responses;
34
pub mod request;
45
mod sign_in;
56

crates/copilot/src/copilot_chat.rs

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
1515
use itertools::Itertools;
1616
use paths::home_dir;
1717
use serde::{Deserialize, Serialize};
18+
19+
use crate::copilot_responses as responses;
1820
use settings::watch_config_dir;
1921

2022
pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
@@ -42,10 +44,14 @@ impl CopilotChatConfiguration {
4244
}
4345
}
4446

45-
pub fn api_url_from_endpoint(&self, endpoint: &str) -> String {
47+
pub fn chat_completions_url_from_endpoint(&self, endpoint: &str) -> String {
4648
format!("{}/chat/completions", endpoint)
4749
}
4850

51+
pub fn responses_url_from_endpoint(&self, endpoint: &str) -> String {
52+
format!("{}/responses", endpoint)
53+
}
54+
4955
pub fn models_url_from_endpoint(&self, endpoint: &str) -> String {
5056
format!("{}/models", endpoint)
5157
}
@@ -71,6 +77,14 @@ pub enum Role {
7177
System,
7278
}
7379

80+
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
81+
pub enum ModelSupportedEndpoint {
82+
#[serde(rename = "/chat/completions")]
83+
ChatCompletions,
84+
#[serde(rename = "/responses")]
85+
Responses,
86+
}
87+
7488
#[derive(Deserialize)]
7589
struct ModelSchema {
7690
#[serde(deserialize_with = "deserialize_models_skip_errors")]
@@ -109,6 +123,8 @@ pub struct Model {
109123
// reached. Zed does not currently implement this behaviour
110124
is_chat_fallback: bool,
111125
model_picker_enabled: bool,
126+
#[serde(default)]
127+
supported_endpoints: Vec<ModelSupportedEndpoint>,
112128
}
113129

114130
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
@@ -224,6 +240,16 @@ impl Model {
224240
pub fn tokenizer(&self) -> Option<&str> {
225241
self.capabilities.tokenizer.as_deref()
226242
}
243+
244+
pub fn supports_response(&self) -> bool {
245+
self.supported_endpoints.len() > 0
246+
&& !self
247+
.supported_endpoints
248+
.contains(&ModelSupportedEndpoint::ChatCompletions)
249+
&& self
250+
.supported_endpoints
251+
.contains(&ModelSupportedEndpoint::Responses)
252+
}
227253
}
228254

229255
#[derive(Serialize, Deserialize)]
@@ -253,7 +279,7 @@ pub enum Tool {
253279
Function { function: Function },
254280
}
255281

256-
#[derive(Serialize, Deserialize)]
282+
#[derive(Serialize, Deserialize, Debug)]
257283
#[serde(rename_all = "lowercase")]
258284
pub enum ToolChoice {
259285
Auto,
@@ -346,7 +372,7 @@ pub struct Usage {
346372

347373
#[derive(Debug, Deserialize)]
348374
pub struct ResponseChoice {
349-
pub index: usize,
375+
pub index: Option<usize>,
350376
pub finish_reason: Option<String>,
351377
pub delta: Option<ResponseDelta>,
352378
pub message: Option<ResponseDelta>,
@@ -359,10 +385,9 @@ pub struct ResponseDelta {
359385
#[serde(default)]
360386
pub tool_calls: Vec<ToolCallChunk>,
361387
}
362-
363388
#[derive(Deserialize, Debug, Eq, PartialEq)]
364389
pub struct ToolCallChunk {
365-
pub index: usize,
390+
pub index: Option<usize>,
366391
pub id: Option<String>,
367392
pub function: Option<FunctionChunk>,
368393
}
@@ -554,13 +579,47 @@ impl CopilotChat {
554579
is_user_initiated: bool,
555580
mut cx: AsyncApp,
556581
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
582+
let (client, token, configuration) = Self::get_auth_details(&mut cx).await?;
583+
584+
let api_url = configuration.chat_completions_url_from_endpoint(&token.api_endpoint);
585+
stream_completion(
586+
client.clone(),
587+
token.api_key,
588+
api_url.into(),
589+
request,
590+
is_user_initiated,
591+
)
592+
.await
593+
}
594+
595+
pub async fn stream_response(
596+
request: responses::Request,
597+
is_user_initiated: bool,
598+
mut cx: AsyncApp,
599+
) -> Result<BoxStream<'static, Result<responses::StreamEvent>>> {
600+
let (client, token, configuration) = Self::get_auth_details(&mut cx).await?;
601+
602+
let api_url = configuration.responses_url_from_endpoint(&token.api_endpoint);
603+
responses::stream_response(
604+
client.clone(),
605+
token.api_key,
606+
api_url,
607+
request,
608+
is_user_initiated,
609+
)
610+
.await
611+
}
612+
613+
async fn get_auth_details(
614+
cx: &mut AsyncApp,
615+
) -> Result<(Arc<dyn HttpClient>, ApiToken, CopilotChatConfiguration)> {
557616
let this = cx
558617
.update(|cx| Self::global(cx))
559618
.ok()
560619
.flatten()
561620
.context("Copilot chat is not enabled")?;
562621

563-
let (oauth_token, api_token, client, configuration) = this.read_with(&cx, |this, _| {
622+
let (oauth_token, api_token, client, configuration) = this.read_with(cx, |this, _| {
564623
(
565624
this.oauth_token.clone(),
566625
this.api_token.clone(),
@@ -572,28 +631,20 @@ impl CopilotChat {
572631
let oauth_token = oauth_token.context("No OAuth token available")?;
573632

574633
let token = match api_token {
575-
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
634+
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token,
576635
_ => {
577636
let token_url = configuration.token_url();
578637
let token =
579638
request_api_token(&oauth_token, token_url.into(), client.clone()).await?;
580-
this.update(&mut cx, |this, cx| {
639+
this.update(cx, |this, cx| {
581640
this.api_token = Some(token.clone());
582641
cx.notify();
583642
})?;
584643
token
585644
}
586645
};
587646

588-
let api_url = configuration.api_url_from_endpoint(&token.api_endpoint);
589-
stream_completion(
590-
client.clone(),
591-
token.api_key,
592-
api_url.into(),
593-
request,
594-
is_user_initiated,
595-
)
596-
.await
647+
Ok((client, token, configuration))
597648
}
598649

599650
pub fn set_configuration(

0 commit comments

Comments
 (0)