Skip to main content

litellm_rs/core/providers/github_copilot/
config.rs

1//! GitHub Copilot Provider Configuration
2//!
3//! Configuration for GitHub Copilot API access including authentication and model settings.
4
5use crate::core::traits::provider::ProviderConfig;
6use serde::{Deserialize, Serialize};
7
8/// Default API base URL for GitHub Copilot
9pub const GITHUB_COPILOT_API_BASE: &str = "https://api.githubcopilot.com";
10
11/// Editor plugin version for headers
12pub const EDITOR_PLUGIN_VERSION: &str = "copilot-chat/0.26.7";
13
14/// User agent for headers
15pub const USER_AGENT: &str = "GitHubCopilotChat/0.26.7";
16
17/// API version for headers
18pub const API_VERSION: &str = "2025-04-01";
19
20/// GitHub Copilot provider configuration
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GitHubCopilotConfig {
23    /// Token directory for storing access tokens
24    pub token_dir: Option<String>,
25
26    /// Access token file name
27    pub access_token_file: Option<String>,
28
29    /// API key file name
30    pub api_key_file: Option<String>,
31
32    /// API base URL (default: <https://api.githubcopilot.com>)
33    pub api_base: Option<String>,
34
35    /// Request timeout in seconds
36    #[serde(default = "default_timeout")]
37    pub timeout: u64,
38
39    /// Maximum number of retries for failed requests
40    #[serde(default = "default_max_retries")]
41    pub max_retries: u32,
42
43    /// Whether to disable converting system messages to assistant messages
44    #[serde(default)]
45    pub disable_system_to_assistant: bool,
46
47    /// Whether to enable debug logging
48    #[serde(default)]
49    pub debug: bool,
50}
51
52impl Default for GitHubCopilotConfig {
53    fn default() -> Self {
54        Self {
55            token_dir: None,
56            access_token_file: None,
57            api_key_file: None,
58            api_base: None,
59            timeout: default_timeout(),
60            max_retries: default_max_retries(),
61            disable_system_to_assistant: false,
62            debug: false,
63        }
64    }
65}
66
67impl ProviderConfig for GitHubCopilotConfig {
68    fn validate(&self) -> Result<(), String> {
69        // Validation is done during authentication
70        // No API key is needed upfront - it's obtained via OAuth
71
72        // Validate timeout
73        if self.timeout == 0 {
74            return Err("Timeout must be greater than 0".to_string());
75        }
76
77        Ok(())
78    }
79
80    fn api_key(&self) -> Option<&str> {
81        // API key is obtained dynamically via OAuth
82        None
83    }
84
85    fn api_base(&self) -> Option<&str> {
86        self.api_base.as_deref()
87    }
88
89    fn timeout(&self) -> std::time::Duration {
90        std::time::Duration::from_secs(self.timeout)
91    }
92
93    fn max_retries(&self) -> u32 {
94        self.max_retries
95    }
96}
97
98impl GitHubCopilotConfig {
99    /// Get token directory with environment variable fallback
100    pub fn get_token_dir(&self) -> String {
101        self.token_dir
102            .clone()
103            .or_else(|| std::env::var("GITHUB_COPILOT_TOKEN_DIR").ok())
104            .unwrap_or_else(|| {
105                let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
106                format!("{}/.config/litellm/github_copilot", home)
107            })
108    }
109
110    /// Get access token file name with environment variable fallback
111    pub fn get_access_token_file(&self) -> String {
112        self.access_token_file
113            .clone()
114            .or_else(|| std::env::var("GITHUB_COPILOT_ACCESS_TOKEN_FILE").ok())
115            .unwrap_or_else(|| "access-token".to_string())
116    }
117
118    /// Get API key file name with environment variable fallback
119    pub fn get_api_key_file(&self) -> String {
120        self.api_key_file
121            .clone()
122            .or_else(|| std::env::var("GITHUB_COPILOT_API_KEY_FILE").ok())
123            .unwrap_or_else(|| "api-key.json".to_string())
124    }
125
126    /// Get API base with environment variable fallback
127    pub fn get_api_base(&self) -> String {
128        self.api_base
129            .clone()
130            .or_else(|| std::env::var("GITHUB_COPILOT_API_BASE").ok())
131            .unwrap_or_else(|| GITHUB_COPILOT_API_BASE.to_string())
132    }
133}
134
135/// Get default headers for GitHub Copilot API
136pub fn get_copilot_default_headers(api_key: &str) -> std::collections::HashMap<String, String> {
137    let mut headers = std::collections::HashMap::new();
138    headers.insert("Authorization".to_string(), format!("Bearer {}", api_key));
139    headers.insert("Content-Type".to_string(), "application/json".to_string());
140    headers.insert(
141        "copilot-integration-id".to_string(),
142        "vscode-chat".to_string(),
143    );
144    headers.insert("editor-version".to_string(), "vscode/1.95.0".to_string());
145    headers.insert(
146        "editor-plugin-version".to_string(),
147        EDITOR_PLUGIN_VERSION.to_string(),
148    );
149    headers.insert("user-agent".to_string(), USER_AGENT.to_string());
150    headers.insert(
151        "openai-intent".to_string(),
152        "conversation-panel".to_string(),
153    );
154    headers.insert("x-github-api-version".to_string(), API_VERSION.to_string());
155    headers.insert("x-request-id".to_string(), uuid::Uuid::new_v4().to_string());
156    headers.insert(
157        "x-vscode-user-agent-library-version".to_string(),
158        "electron-fetch".to_string(),
159    );
160    headers
161}
162
163fn default_timeout() -> u64 {
164    60
165}
166
167fn default_max_retries() -> u32 {
168    3
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_github_copilot_config_default() {
177        let config = GitHubCopilotConfig::default();
178        assert!(config.token_dir.is_none());
179        assert!(config.access_token_file.is_none());
180        assert!(config.api_key_file.is_none());
181        assert!(config.api_base.is_none());
182        assert_eq!(config.timeout, 60);
183        assert_eq!(config.max_retries, 3);
184        assert!(!config.disable_system_to_assistant);
185        assert!(!config.debug);
186    }
187
188    #[test]
189    fn test_github_copilot_config_get_api_base_default() {
190        let config = GitHubCopilotConfig::default();
191        assert_eq!(config.get_api_base(), "https://api.githubcopilot.com");
192    }
193
194    #[test]
195    fn test_github_copilot_config_get_api_base_custom() {
196        let config = GitHubCopilotConfig {
197            api_base: Some("https://custom.copilot.com".to_string()),
198            ..Default::default()
199        };
200        assert_eq!(config.get_api_base(), "https://custom.copilot.com");
201    }
202
203    #[test]
204    fn test_github_copilot_config_provider_config_trait() {
205        let config = GitHubCopilotConfig {
206            api_base: Some("https://custom.copilot.com".to_string()),
207            timeout: 60,
208            max_retries: 5,
209            ..Default::default()
210        };
211
212        assert!(config.api_key().is_none()); // No static API key
213        assert_eq!(config.api_base(), Some("https://custom.copilot.com"));
214        assert_eq!(config.timeout(), std::time::Duration::from_secs(60));
215        assert_eq!(config.max_retries(), 5);
216    }
217
218    #[test]
219    fn test_github_copilot_config_validation() {
220        let config = GitHubCopilotConfig::default();
221        assert!(config.validate().is_ok());
222    }
223
224    #[test]
225    fn test_github_copilot_config_validation_zero_timeout() {
226        let config = GitHubCopilotConfig {
227            timeout: 0,
228            ..Default::default()
229        };
230        assert!(config.validate().is_err());
231    }
232
233    #[test]
234    fn test_github_copilot_config_serialization() {
235        let config = GitHubCopilotConfig {
236            token_dir: Some("/custom/path".to_string()),
237            api_base: Some("https://custom.copilot.com".to_string()),
238            timeout: 45,
239            max_retries: 2,
240            disable_system_to_assistant: true,
241            debug: true,
242            ..Default::default()
243        };
244
245        let json = serde_json::to_value(&config).unwrap();
246        assert_eq!(json["token_dir"], "/custom/path");
247        assert_eq!(json["api_base"], "https://custom.copilot.com");
248        assert_eq!(json["timeout"], 45);
249        assert_eq!(json["max_retries"], 2);
250        assert!(json["disable_system_to_assistant"].as_bool().unwrap());
251        assert!(json["debug"].as_bool().unwrap());
252    }
253
254    #[test]
255    fn test_github_copilot_config_deserialization() {
256        let json = r#"{
257            "api_base": "https://custom.copilot.com",
258            "timeout": 60,
259            "debug": true
260        }"#;
261
262        let config: GitHubCopilotConfig = serde_json::from_str(json).unwrap();
263        assert_eq!(
264            config.api_base,
265            Some("https://custom.copilot.com".to_string())
266        );
267        assert_eq!(config.timeout, 60);
268        assert!(config.debug);
269    }
270
271    #[test]
272    fn test_get_copilot_default_headers() {
273        let headers = get_copilot_default_headers("test-api-key");
274
275        assert!(
276            headers
277                .get("Authorization")
278                .unwrap()
279                .contains("Bearer test-api-key")
280        );
281        assert_eq!(headers.get("Content-Type").unwrap(), "application/json");
282        assert_eq!(
283            headers.get("copilot-integration-id").unwrap(),
284            "vscode-chat"
285        );
286        assert!(headers.contains_key("x-request-id"));
287    }
288}