diff --git a/interface/src/api/client.ts b/interface/src/api/client.ts index 6390f6ba3..d88eae00d 100644 --- a/interface/src/api/client.ts +++ b/interface/src/api/client.ts @@ -1596,7 +1596,7 @@ export const api = { return response.json() as Promise; }, testProviderModel: async (provider: string, apiKey: string, model: string) => { - const response = await fetch(`${getApiBase()}/providers/test`, { + const response = await fetch(`${getApiBase()}/providers/test-model`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, api_key: apiKey, model }), diff --git a/src/api/providers.rs b/src/api/providers.rs index 3b923e9b5..319da4fb3 100644 --- a/src/api/providers.rs +++ b/src/api/providers.rs @@ -192,6 +192,21 @@ fn build_test_llm_config(provider: &str, credential: &str) -> crate::config::Llm let mut providers = HashMap::new(); if let Some(provider_config) = crate::config::default_provider_config(provider, credential) { providers.insert(provider.to_string(), provider_config); + } else if provider == "github-copilot" { + // GitHub Copilot uses token exchange, so default_provider_config returns None. + // For testing, add a provider entry with the PAT as the api_key — + // LlmManager::get_copilot_token() will exchange it for a real Copilot token. + providers.insert( + provider.to_string(), + crate::config::ProviderConfig { + api_type: crate::config::ApiType::OpenAiChatCompletions, + base_url: crate::config::GITHUB_COPILOT_DEFAULT_BASE_URL.to_string(), + api_key: credential.to_string(), + name: Some("GitHub Copilot".to_string()), + use_bearer_auth: true, + extra_headers: vec![], + }, + ); } crate::config::LlmConfig { @@ -431,6 +446,15 @@ pub(super) async fn get_providers( env_set(env_var) }; + // Copilot: only check TOML key, not env var. The env var GITHUB_COPILOT_API_KEY + // is a fallback for config loading but shouldn't keep the provider visible in + // the settings UI after a remove — the env var can't be unset from the process. + let copilot_configured = doc + .get("llm") + .and_then(|llm| llm.get("github_copilot_key")) + .and_then(|val| val.as_str()) + .is_some_and(|s| resolve_value(s).is_some()); + ( has_value("anthropic_key", "ANTHROPIC_API_KEY"), has_value("openai_key", "OPENAI_API_KEY"), @@ -454,7 +478,7 @@ pub(super) async fn get_providers( has_value("minimax_cn_key", "MINIMAX_CN_API_KEY"), has_value("moonshot_key", "MOONSHOT_API_KEY"), has_value("zai_coding_plan_key", "ZAI_CODING_PLAN_API_KEY"), - has_value("github_copilot_key", "GITHUB_COPILOT_API_KEY"), + copilot_configured, ) } else { ( @@ -794,7 +818,7 @@ pub(super) async fn openai_browser_oauth_status( } #[utoipa::path( - post, + put, path = "/providers", request_body = ProviderUpdateRequest, responses( @@ -1083,4 +1107,24 @@ mod tests { assert_eq!(provider.base_url, "http://remote-ollama.local:11434"); assert_eq!(provider.api_key, ""); } + + #[test] + fn build_test_llm_config_registers_github_copilot_provider() { + let config = build_test_llm_config("github-copilot", "ghp_test_pat_token"); + let provider = config + .providers + .get("github-copilot") + .expect("github-copilot provider should be registered"); + + assert_eq!( + provider.base_url, + crate::config::GITHUB_COPILOT_DEFAULT_BASE_URL + ); + assert_eq!(provider.api_key, "ghp_test_pat_token"); + assert!(provider.use_bearer_auth); + assert_eq!( + config.github_copilot_key.as_deref(), + Some("ghp_test_pat_token") + ); + } } diff --git a/src/config.rs b/src/config.rs index db0e51b98..80a30aac7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,7 @@ pub use permissions::{ DiscordPermissions, MattermostPermissions, SignalPermissions, SlackPermissions, TelegramPermissions, TwitchPermissions, }; +pub(crate) use providers::GITHUB_COPILOT_DEFAULT_BASE_URL; pub(crate) use providers::default_provider_config; pub use runtime::RuntimeConfig; pub use types::*; diff --git a/src/config/providers.rs b/src/config/providers.rs index 091ca71cc..4216aa152 100644 --- a/src/config/providers.rs +++ b/src/config/providers.rs @@ -26,7 +26,7 @@ pub(super) const NVIDIA_PROVIDER_BASE_URL: &str = "https://integrate.api.nvidia. pub(super) const FIREWORKS_PROVIDER_BASE_URL: &str = "https://api.fireworks.ai/inference"; pub(crate) const GEMINI_PROVIDER_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta/openai"; -pub(super) const GITHUB_COPILOT_DEFAULT_BASE_URL: &str = "https://api.individual.githubcopilot.com"; +pub(crate) const GITHUB_COPILOT_DEFAULT_BASE_URL: &str = "https://api.individual.githubcopilot.com"; /// App attribution headers sent with every OpenRouter API request. /// See .