feat: GitHub Copilot stream support (fixes gpt-4.1, claude) (#2576)

Co-authored-by: Yingjie He <yingjiehe@squareup.com>
This commit is contained in:
Antonio Cheong
2025-05-19 17:23:04 +00:00
committed by GitHub
parent c817800f30
commit 4ae5e4264c
5 changed files with 454 additions and 4 deletions
+48 -4
View File
@@ -15,6 +15,7 @@ use super::base::{Provider, ProviderMetadata, ProviderUsage, Usage};
use super::errors::ProviderError;
use super::formats::openai::{create_request, get_usage, response_to_message};
use super::utils::{emit_debug_trace, get_model, handle_response_openai_compat, ImageFormat};
use crate::config::{Config, ConfigError};
use crate::message::Message;
use crate::model::ModelConfig;
@@ -25,10 +26,13 @@ pub const GITHUB_COPILOT_KNOWN_MODELS: &[&str] = &[
"gpt-4o",
"o1",
"o3-mini",
"claude-3-7-sonnet",
"claude-3-5-sonnet",
"claude-3.7-sonnet",
"claude-3.5-sonnet",
];
pub const GITHUB_COPILOT_STREAM_MODELS: &[&str] =
&["gpt-4.1", "claude-3.7-sonnet", "claude-3.5-sonnet"];
const GITHUB_COPILOT_DOC_URL: &str =
"https://docs.github.com/en/copilot/using-github-copilot/ai-models";
const GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98";
@@ -132,7 +136,20 @@ impl GithubCopilotProvider {
})
}
async fn post(&self, payload: Value) -> Result<Value, ProviderError> {
async fn post(&self, mut payload: Value) -> Result<Value, ProviderError> {
use crate::providers::utils_universal_openai_stream::{OAIStreamChunk, OAIStreamCollector};
use futures_util::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));
}
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}")))?;
@@ -144,7 +161,34 @@ impl GithubCopilotProvider {
.json(&payload)
.send()
.await?;
handle_response_openai_compat(response).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 payload = &tline[6..];
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
}
}
async fn get_api_info(&self) -> Result<(String, String)> {