From 0904a720b4d9d686e37aeea8921759a101f23214 Mon Sep 17 00:00:00 2001 From: bhagwan Date: Sun, 29 Mar 2026 13:54:54 -0400 Subject: [PATCH] fix(api): provider endpoint mismatches preventing Copilot save, test, and remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #415 Three endpoint mismatches between frontend and backend caused the GitHub Copilot provider's save, test, and remove buttons to fail with 405 Method Not Allowed. These affected all providers for save. - change update_provider annotation from post to put to match frontend - fix test button URL from /providers/test to /providers/test-model - add github-copilot entry in build_test_llm_config since default_provider_config returns None for providers that require token exchange - widen GITHUB_COPILOT_DEFAULT_BASE_URL visibility to pub(crate) - add unit test for build_test_llm_config with github-copilot fix(api): Copilot provider shows as available after remove when env var is set get_providers fell back to the GITHUB_COPILOT_API_KEY env var when the TOML key was absent, so the provider stayed visible in settings after a remove — the env var can't be unset from a running process. Only check the TOML key for Copilot status in the config-exists path. The env var fallback remains for the no-config-file case (fresh install). --- interface/src/api/client.ts | 2 +- src/api/providers.rs | 48 +++++++++++++++++++++++++++++++++++-- src/config.rs | 1 + src/config/providers.rs | 2 +- 4 files changed, 49 insertions(+), 4 deletions(-) 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 .