use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::provider::Provider;
pub const DEFAULT_API_BASE: &str = "https://api.githubcopilot.com";
const API_BASE_ENV_VAR: &str = "GITHUB_COPILOT_API_BASE";
const MODEL_PREFIX: &str = "github_copilot/";
const COPILOT_VERSION: &str = "0.26.7";
const EDITOR_VERSION: &str = "vscode/1.95.0";
const API_VERSION: &str = "2025-04-01";
static COPILOT_EXTRA_HEADERS: &[(&str, &str)] = &[
("copilot-integration-id", "vscode-chat"),
("editor-version", EDITOR_VERSION),
("editor-plugin-version", "copilot-chat/0.26.7"),
("user-agent", "GitHubCopilotChat/0.26.7"),
("openai-intent", "conversation-panel"),
("x-github-api-version", API_VERSION),
("x-vscode-user-agent-library-version", "electron-fetch"),
];
const _: () = {
let _ = COPILOT_VERSION;
};
fn generate_request_id() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let thread_id = std::thread::current().id();
let mut hasher = DefaultHasher::new();
nanos.hash(&mut hasher);
thread_id.hash(&mut hasher);
let h1 = hasher.finish();
h1.hash(&mut hasher);
let h2 = hasher.finish();
let b1 = h1.to_le_bytes();
let b2 = h2.to_le_bytes();
let time_low = u32::from_le_bytes([b1[0], b1[1], b1[2], b1[3]]);
let time_mid = u16::from_le_bytes([b1[4], b1[5]]);
let version_hi = u16::from_le_bytes([b1[6], b1[7]]) & 0x0FFF;
let variant_clock = (u16::from_le_bytes([b2[0], b2[1]]) & 0x3FFF) | 0x8000;
let node = u64::from_le_bytes([b2[2], b2[3], b2[4], b2[5], b2[6], b2[7], 0, 0]) & 0x0000_FFFF_FFFF_FFFF;
format!("{time_low:08x}-{time_mid:04x}-4{version_hi:03x}-{variant_clock:04x}-{node:012x}")
}
pub struct GithubCopilotProvider {
api_base: String,
}
impl GithubCopilotProvider {
#[must_use]
pub fn new() -> Self {
Self {
api_base: DEFAULT_API_BASE.to_owned(),
}
}
#[must_use]
#[allow(dead_code)] pub fn with_api_base(base: String) -> Self {
Self { api_base: base }
}
#[must_use]
pub fn from_env() -> Self {
let api_base = std::env::var(API_BASE_ENV_VAR)
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_API_BASE.to_owned());
Self { api_base }
}
}
impl Default for GithubCopilotProvider {
fn default() -> Self {
Self::new()
}
}
impl Provider for GithubCopilotProvider {
fn name(&self) -> &str {
"github_copilot"
}
fn base_url(&self) -> &str {
&self.api_base
}
fn auth_header<'a>(&'a self, api_key: &'a str) -> Option<(Cow<'static, str>, Cow<'a, str>)> {
Some((Cow::Borrowed("Authorization"), Cow::Owned(format!("Bearer {api_key}"))))
}
fn extra_headers(&self) -> &'static [(&'static str, &'static str)] {
COPILOT_EXTRA_HEADERS
}
fn dynamic_headers(&self, body: &serde_json::Value) -> Vec<(String, String)> {
let request_id = generate_request_id();
let initiator = determine_initiator(body);
vec![
("x-request-id".to_owned(), request_id),
("X-Initiator".to_owned(), initiator.to_owned()),
]
}
fn matches_model(&self, model: &str) -> bool {
model.starts_with(MODEL_PREFIX)
}
fn strip_model_prefix<'m>(&self, model: &'m str) -> &'m str {
model.strip_prefix(MODEL_PREFIX).unwrap_or(model)
}
}
fn determine_initiator(body: &serde_json::Value) -> &'static str {
let messages = match body.get("messages").and_then(|m| m.as_array()) {
Some(msgs) => msgs,
None => return "user",
};
for message in messages {
let role = message.get("role").and_then(|r| r.as_str()).unwrap_or("");
if role == "tool" || role == "assistant" {
return "agent";
}
}
"user"
}
#[cfg(test)]
mod tests {
use serde_json::json;
use serial_test::serial;
use super::*;
#[test]
fn test_name() {
let provider = GithubCopilotProvider::new();
assert_eq!(provider.name(), "github_copilot");
}
#[test]
fn test_base_url_default() {
let provider = GithubCopilotProvider::new();
assert_eq!(provider.base_url(), "https://api.githubcopilot.com");
}
#[test]
fn test_base_url_custom() {
let provider = GithubCopilotProvider::with_api_base("https://proxy.example.com".to_owned());
assert_eq!(provider.base_url(), "https://proxy.example.com");
}
#[test]
#[serial]
fn test_from_env_uses_default_when_unset() {
unsafe { std::env::remove_var("GITHUB_COPILOT_API_BASE") };
let provider = GithubCopilotProvider::from_env();
assert_eq!(provider.base_url(), DEFAULT_API_BASE);
}
#[test]
#[serial]
fn test_from_env_reads_custom_base_url() {
unsafe { std::env::set_var("GITHUB_COPILOT_API_BASE", "https://custom.copilot.test") };
let provider = GithubCopilotProvider::from_env();
unsafe { std::env::remove_var("GITHUB_COPILOT_API_BASE") };
assert_eq!(provider.base_url(), "https://custom.copilot.test");
}
#[test]
#[serial]
fn test_from_env_falls_back_on_empty_value() {
unsafe { std::env::set_var("GITHUB_COPILOT_API_BASE", "") };
let provider = GithubCopilotProvider::from_env();
unsafe { std::env::remove_var("GITHUB_COPILOT_API_BASE") };
assert_eq!(provider.base_url(), DEFAULT_API_BASE);
}
#[test]
fn test_matches_model() {
let provider = GithubCopilotProvider::new();
assert!(provider.matches_model("github_copilot/gpt-4o"));
assert!(provider.matches_model("github_copilot/claude-3.5-sonnet"));
assert!(provider.matches_model("github_copilot/o3-mini"));
}
#[test]
fn test_does_not_match_other_providers() {
let provider = GithubCopilotProvider::new();
assert!(!provider.matches_model("openai/gpt-4o"));
assert!(!provider.matches_model("gpt-4o"));
assert!(!provider.matches_model("claude-3.5-sonnet"));
assert!(!provider.matches_model("anthropic/claude-3.5-sonnet"));
}
#[test]
fn test_strip_model_prefix() {
let provider = GithubCopilotProvider::new();
assert_eq!(provider.strip_model_prefix("github_copilot/gpt-4o"), "gpt-4o");
}
#[test]
fn test_strip_model_prefix_no_prefix() {
let provider = GithubCopilotProvider::new();
assert_eq!(provider.strip_model_prefix("gpt-4o"), "gpt-4o");
}
#[test]
fn test_auth_header() {
let provider = GithubCopilotProvider::new();
let (name, value) = provider
.auth_header("ghs_test_token_123")
.expect("should return an auth header");
assert_eq!(name, "Authorization");
assert_eq!(value, "Bearer ghs_test_token_123");
}
#[test]
fn test_extra_headers() {
let provider = GithubCopilotProvider::new();
let headers = provider.extra_headers();
let find = |key: &str| headers.iter().find(|(k, _)| *k == key).map(|(_, v)| *v);
assert_eq!(find("copilot-integration-id"), Some("vscode-chat"));
assert_eq!(find("editor-version"), Some("vscode/1.95.0"));
assert_eq!(find("editor-plugin-version"), Some("copilot-chat/0.26.7"));
assert_eq!(find("user-agent"), Some("GitHubCopilotChat/0.26.7"));
assert_eq!(find("openai-intent"), Some("conversation-panel"));
assert_eq!(find("x-github-api-version"), Some("2025-04-01"));
assert_eq!(find("x-vscode-user-agent-library-version"), Some("electron-fetch"));
assert_eq!(headers.len(), 7, "expected exactly 7 static headers");
}
#[test]
fn test_dynamic_headers_user() {
let provider = GithubCopilotProvider::new();
let body = json!({
"model": "github_copilot/gpt-4o",
"messages": [
{"role": "user", "content": "Hello!"}
]
});
let headers = provider.dynamic_headers(&body);
let initiator = headers
.iter()
.find(|(k, _)| k == "X-Initiator")
.map(|(_, v)| v.as_str());
assert_eq!(initiator, Some("user"));
}
#[test]
fn test_dynamic_headers_agent_with_tool_role() {
let provider = GithubCopilotProvider::new();
let body = json!({
"model": "github_copilot/gpt-4o",
"messages": [
{"role": "user", "content": "Run the tool."},
{"role": "assistant", "content": null, "tool_calls": []},
{"role": "tool", "content": "tool result", "tool_call_id": "abc"}
]
});
let headers = provider.dynamic_headers(&body);
let initiator = headers
.iter()
.find(|(k, _)| k == "X-Initiator")
.map(|(_, v)| v.as_str());
assert_eq!(initiator, Some("agent"));
}
#[test]
fn test_dynamic_headers_agent_with_assistant_role() {
let provider = GithubCopilotProvider::new();
let body = json!({
"messages": [
{"role": "user", "content": "Hi"},
{"role": "assistant", "content": "Hello"}
]
});
let headers = provider.dynamic_headers(&body);
let initiator = headers
.iter()
.find(|(k, _)| k == "X-Initiator")
.map(|(_, v)| v.as_str());
assert_eq!(initiator, Some("agent"));
}
#[test]
fn test_dynamic_headers_user_when_no_messages() {
let provider = GithubCopilotProvider::new();
let body = json!({ "model": "github_copilot/gpt-4o" });
let headers = provider.dynamic_headers(&body);
let initiator = headers
.iter()
.find(|(k, _)| k == "X-Initiator")
.map(|(_, v)| v.as_str());
assert_eq!(initiator, Some("user"));
}
#[test]
fn test_dynamic_headers_request_id_present_and_valid_uuid() {
let provider = GithubCopilotProvider::new();
let body = json!({ "messages": [{"role": "user", "content": "hi"}] });
let headers = provider.dynamic_headers(&body);
let request_id = headers
.iter()
.find(|(k, _)| k == "x-request-id")
.map(|(_, v)| v.as_str())
.expect("x-request-id header must be present");
assert_eq!(request_id.len(), 36, "request id must be 36 characters");
let parts: Vec<&str> = request_id.split('-').collect();
assert_eq!(parts.len(), 5, "UUID must have 5 dash-separated groups");
assert_eq!(parts[0].len(), 8);
assert_eq!(parts[1].len(), 4);
assert_eq!(parts[2].len(), 4);
assert_eq!(parts[3].len(), 4);
assert_eq!(parts[4].len(), 12);
assert_eq!(&parts[2][0..1], "4", "third group must start with '4' (UUID version 4)");
let variant_nibble = parts[3].chars().next().expect("fourth group is non-empty");
assert!(
matches!(variant_nibble, '8' | '9' | 'a' | 'b'),
"fourth group must start with 8, 9, a or b (RFC 4122 variant); got '{variant_nibble}'"
);
for ch in request_id.chars() {
assert!(
ch.is_ascii_hexdigit() || ch == '-',
"unexpected character '{ch}' in request id"
);
}
}
#[test]
fn test_dynamic_headers_request_id_unique_per_call() {
let provider = GithubCopilotProvider::new();
let body = json!({ "messages": [{"role": "user", "content": "hi"}] });
let id1 = provider
.dynamic_headers(&body)
.into_iter()
.find(|(k, _)| k == "x-request-id")
.map(|(_, v)| v)
.expect("x-request-id must be present");
std::thread::sleep(std::time::Duration::from_nanos(100));
let id2 = provider
.dynamic_headers(&body)
.into_iter()
.find(|(k, _)| k == "x-request-id")
.map(|(_, v)| v)
.expect("x-request-id must be present");
assert_ne!(id1, id2, "consecutive request IDs must differ");
}
#[test]
fn test_endpoint_paths_are_openai_compatible() {
let provider = GithubCopilotProvider::new();
assert_eq!(provider.chat_completions_path(), "/chat/completions");
assert_eq!(provider.embeddings_path(), "/embeddings");
assert_eq!(provider.models_path(), "/models");
}
}