Skip to content
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 51 additions & 60 deletions crates/goose/src/providers/githubcopilot.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::config::paths::Paths;
use crate::providers::api_client::{ApiClient, AuthMethod};
use crate::providers::utils::{handle_status_openai_compat, stream_openai_compat};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use axum::http;
use chrono::{DateTime, Utc};
use reqwest::Client;
use reqwest::{Client, Response};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::cell::RefCell;
Expand All @@ -21,7 +23,7 @@ use crate::config::{Config, ConfigError};
use crate::conversation::message::Message;

use crate::model::ModelConfig;
use crate::providers::base::ConfigKey;
use crate::providers::base::{ConfigKey, MessageStream};
use rmcp::model::Tool;

pub const GITHUB_COPILOT_DEFAULT_MODEL: &str = "gpt-4.1";
Expand Down Expand Up @@ -170,68 +172,19 @@ impl GithubCopilotProvider {
})
}

async fn post(&self, payload: &mut Value) -> Result<Value, ProviderError> {
use crate::providers::utils_universal_openai_stream::{OAIStreamChunk, OAIStreamCollector};
use futures::StreamExt;
// Detect gpt-4.1 and stream
let model_name = payload.get("model").and_then(|v| v.as_str()).unwrap_or("");
let stream_only_model = GITHUB_COPILOT_STREAM_MODELS
.iter()
.any(|prefix| model_name.starts_with(prefix));
if stream_only_model {
payload
.as_object_mut()
.unwrap()
.insert("stream".to_string(), serde_json::Value::Bool(true));
}
async fn post(&self, payload: &mut Value) -> Result<Response, ProviderError> {
let (endpoint, token) = self.get_api_info().await?;
let url = url::Url::parse(&format!("{}/chat/completions", endpoint))
.map_err(|e| ProviderError::RequestFailed(format!("Invalid base URL: {e}")))?;

let headers = self.get_github_headers();

let mut request = self
.client
.post(url)
.headers(headers)
.header("Authorization", format!("Bearer {}", token));

let auth = AuthMethod::BearerToken(token);
let mut headers = self.get_github_headers();
if Self::payload_contains_image(payload) {
request = request.header("Copilot-Vision-Request", "true");
headers.insert("Copilot-Vision-Request", "true".parse().unwrap());
}
let api_client = ApiClient::new(endpoint.clone(), auth)?.with_headers(headers)?;

let response = request.json(payload).send().await?;

if stream_only_model {
let mut collector = OAIStreamCollector::new();
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| ProviderError::RequestFailed(e.to_string()))?;
let text = String::from_utf8_lossy(&chunk);
for line in text.lines() {
let tline = line.trim();
if !tline.starts_with("data: ") {
continue;
}
let Some(payload) = tline.get(6..) else {
continue;
};
if payload == "[DONE]" {
break;
}
match serde_json::from_str::<OAIStreamChunk>(payload) {
Ok(ch) => collector.add_chunk(&ch),
Err(_) => continue,
}
}
}
let final_response = collector.build_response();
let value = serde_json::to_value(final_response)
.map_err(|e| ProviderError::RequestFailed(e.to_string()))?;
Ok(value)
} else {
handle_response_openai_compat(response).await
}
api_client
.response_post("chat/completions", payload)
.await
.map_err(|e| e.into())
}

async fn get_api_info(&self) -> Result<(String, String)> {
Expand Down Expand Up @@ -449,6 +402,12 @@ impl Provider for GithubCopilotProvider {
self.model.clone()
}

fn supports_streaming(&self) -> bool {
GITHUB_COPILOT_STREAM_MODELS
.iter()
.any(|prefix| self.model.model_name.starts_with(prefix))
}

#[tracing::instrument(
skip(self, model_config, system, messages, tools),
fields(model_config, input, output, input_tokens, output_tokens, total_tokens)
Expand Down Expand Up @@ -478,6 +437,8 @@ impl Provider for GithubCopilotProvider {
})
.await?;

let response = handle_response_openai_compat(response).await?;

// Parse response
let message = response_to_message(&response)?;
let usage = response.get("usage").map(get_usage).unwrap_or_else(|| {
Expand All @@ -489,6 +450,36 @@ impl Provider for GithubCopilotProvider {
Ok((message, ProviderUsage::new(response_model, usage)))
}

async fn stream(
&self,
system: &str,
messages: &[Message],
tools: &[Tool],
) -> Result<MessageStream, ProviderError> {
let payload = create_request(
&self.model,
system,
messages,
tools,
&ImageFormat::OpenAi,
true,
)?;
let mut log = RequestLog::start(&self.model, &payload)?;

let response = self
.with_retry(|| async {
let mut payload_clone = payload.clone();
let resp = self.post(&mut payload_clone).await?;
handle_status_openai_compat(resp).await
})
.await
.inspect_err(|e| {
let _ = log.error(e);
})?;

stream_openai_compat(response, log)
}

/// Fetch supported models from GitHub Copliot; returns Err on failure, Ok(None) if not present

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "Copliot" should be "Copilot".

Suggested change
/// Fetch supported models from GitHub Copliot; returns Err on failure, Ok(None) if not present
/// Fetch supported models from GitHub Copilot; returns Err on failure, Ok(None) if not present

Copilot uses AI. Check for mistakes.
async fn fetch_supported_models(&self) -> Result<Option<Vec<String>>, ProviderError> {
let (endpoint, token) = self.get_api_info().await?;
Expand Down