Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions interface/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ export interface ProviderStatus {
minimax_cn: boolean;
moonshot: boolean;
zai_coding_plan: boolean;
github_copilot: boolean;
}

export interface ProvidersResponse {
Expand Down
2 changes: 2 additions & 0 deletions interface/src/components/ModelSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const PROVIDER_LABELS: Record<string, string> = {
"opencode-go": "OpenCode Go",
minimax: "MiniMax",
"minimax-cn": "MiniMax CN",
"github-copilot": "GitHub Copilot",
};

function formatContextWindow(tokens: number | null): string {
Expand Down Expand Up @@ -134,6 +135,7 @@ export function ModelSelect({
"anthropic",
"openai",
"openai-chatgpt",
"github-copilot",
"ollama",
"deepseek",
"xai",
Expand Down
2 changes: 2 additions & 0 deletions interface/src/lib/providerIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ZAI from "@lobehub/icons/es/ZAI";
import Minimax from "@lobehub/icons/es/Minimax";
import Kimi from "@lobehub/icons/es/Kimi";
import Google from "@lobehub/icons/es/Google";
import GithubCopilot from "@lobehub/icons/es/GithubCopilot";

interface IconProps {
size?: number;
Expand Down Expand Up @@ -138,6 +139,7 @@ export function ProviderIcon({ provider, className = "text-ink-faint", size = 24
minimax: Minimax,
"minimax-cn": Minimax,
moonshot: Kimi, // Kimi is Moonshot AI's product brand
"github-copilot": GithubCopilot,
};

const IconComponent = iconMap[provider.toLowerCase()];
Expand Down
8 changes: 8 additions & 0 deletions interface/src/routes/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ const PROVIDERS = [
envVar: "MOONSHOT_API_KEY",
defaultModel: "moonshot/kimi-k2.5",
},
{
id: "github-copilot",
name: "GitHub Copilot",
description: "GitHub Copilot API (uses GitHub PAT for token exchange)",
placeholder: "ghp_... or gh auth token",
envVar: "GITHUB_COPILOT_API_KEY",
defaultModel: "github-copilot/claude-sonnet-4",
},
{
id: "ollama",
name: "Ollama",
Expand Down
25 changes: 24 additions & 1 deletion src/api/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub(super) struct ProviderStatus {
minimax_cn: bool,
moonshot: bool,
zai_coding_plan: bool,
github_copilot: bool,
}

#[derive(Serialize)]
Expand Down Expand Up @@ -146,6 +147,7 @@ fn provider_toml_key(provider: &str) -> Option<&'static str> {
"minimax-cn" => Some("minimax_cn_key"),
"moonshot" => Some("moonshot_key"),
"zai-coding-plan" => Some("zai_coding_plan_key"),
"github-copilot" => Some("github_copilot_key"),
_ => None,
}
}
Expand Down Expand Up @@ -214,6 +216,7 @@ fn build_test_llm_config(provider: &str, credential: &str) -> crate::config::Llm
minimax_cn_key: (provider == "minimax-cn").then(|| credential.to_string()),
moonshot_key: (provider == "moonshot").then(|| credential.to_string()),
zai_coding_plan_key: (provider == "zai-coding-plan").then(|| credential.to_string()),
github_copilot_key: (provider == "github-copilot").then(|| credential.to_string()),
providers,
}
}
Expand Down Expand Up @@ -369,6 +372,7 @@ pub(super) async fn get_providers(
minimax_cn,
moonshot,
zai_coding_plan,
github_copilot,
) = if config_path.exists() {
let content = tokio::fs::read_to_string(&config_path)
.await
Expand Down Expand Up @@ -413,6 +417,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"),
)
} else {
(
Expand All @@ -437,6 +442,7 @@ pub(super) async fn get_providers(
std::env::var("MINIMAX_CN_API_KEY").is_ok(),
std::env::var("MOONSHOT_API_KEY").is_ok(),
std::env::var("ZAI_CODING_PLAN_API_KEY").is_ok(),
std::env::var("GITHUB_COPILOT_API_KEY").is_ok(),
)
};

Expand All @@ -462,6 +468,7 @@ pub(super) async fn get_providers(
minimax_cn,
moonshot,
zai_coding_plan,
github_copilot,
};
let has_any = providers.anthropic
|| providers.openai
Expand All @@ -483,7 +490,8 @@ pub(super) async fn get_providers(
|| providers.minimax
|| providers.minimax_cn
|| providers.moonshot
|| providers.zai_coding_plan;
|| providers.zai_coding_plan
|| providers.github_copilot;

Ok(Json(ProvidersResponse { providers, has_any }))
}
Expand Down Expand Up @@ -914,6 +922,21 @@ pub(super) async fn delete_provider(
}));
}

// GitHub Copilot has a cached token file alongside the TOML key.
// Remove both the TOML key and the cached token.
if provider == "github-copilot" {
let instance_dir = (**state.instance_dir.load()).clone();
let token_path = crate::github_copilot_auth::credentials_path(&instance_dir);
if token_path.exists() {
tokio::fs::remove_file(&token_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
if let Some(manager) = state.llm_manager.read().await.as_ref() {
manager.clear_copilot_token().await;
}
}

let Some(key_name) = provider_toml_key(&provider) else {
return Ok(Json(ProviderUpdateResponse {
success: false,
Expand Down
42 changes: 35 additions & 7 deletions src/config/load.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use super::providers::{
ANTHROPIC_PROVIDER_BASE_URL, DEEPSEEK_PROVIDER_BASE_URL, FIREWORKS_PROVIDER_BASE_URL,
GEMINI_PROVIDER_BASE_URL, GROQ_PROVIDER_BASE_URL, KILO_PROVIDER_BASE_URL,
MINIMAX_CN_PROVIDER_BASE_URL, MINIMAX_PROVIDER_BASE_URL, MISTRAL_PROVIDER_BASE_URL,
MOONSHOT_PROVIDER_BASE_URL, NVIDIA_PROVIDER_BASE_URL, OLLAMA_PROVIDER_BASE_URL,
OPENAI_PROVIDER_BASE_URL, OPENCODE_GO_PROVIDER_BASE_URL, OPENCODE_ZEN_PROVIDER_BASE_URL,
OPENROUTER_PROVIDER_BASE_URL, TOGETHER_PROVIDER_BASE_URL, XAI_PROVIDER_BASE_URL,
ZAI_CODING_PLAN_BASE_URL, ZHIPU_PROVIDER_BASE_URL, add_shorthand_provider,
infer_routing_from_providers, openrouter_extra_headers, resolve_routing,
GEMINI_PROVIDER_BASE_URL, GITHUB_COPILOT_DEFAULT_BASE_URL, GROQ_PROVIDER_BASE_URL,
KILO_PROVIDER_BASE_URL, MINIMAX_CN_PROVIDER_BASE_URL, MINIMAX_PROVIDER_BASE_URL,
MISTRAL_PROVIDER_BASE_URL, MOONSHOT_PROVIDER_BASE_URL, NVIDIA_PROVIDER_BASE_URL,
OLLAMA_PROVIDER_BASE_URL, OPENAI_PROVIDER_BASE_URL, OPENCODE_GO_PROVIDER_BASE_URL,
OPENCODE_ZEN_PROVIDER_BASE_URL, OPENROUTER_PROVIDER_BASE_URL, TOGETHER_PROVIDER_BASE_URL,
XAI_PROVIDER_BASE_URL, ZAI_CODING_PLAN_BASE_URL, ZHIPU_PROVIDER_BASE_URL,
add_shorthand_provider, infer_routing_from_providers, openrouter_extra_headers,
resolve_routing,
};
use super::toml_schema::*;
use super::{
Expand Down Expand Up @@ -430,6 +431,7 @@ impl Config {
minimax_cn_key: std::env::var("MINIMAX_CN_API_KEY").ok(),
moonshot_key: std::env::var("MOONSHOT_API_KEY").ok(),
zai_coding_plan_key: std::env::var("ZAI_CODING_PLAN_API_KEY").ok(),
github_copilot_key: std::env::var("GITHUB_COPILOT_API_KEY").ok(),
providers: HashMap::new(),
};

Expand Down Expand Up @@ -510,6 +512,16 @@ impl Config {
false,
);

add_shorthand_provider(
&mut llm.providers,
"github-copilot",
llm.github_copilot_key.clone(),
ApiType::OpenAiChatCompletions,
GITHUB_COPILOT_DEFAULT_BASE_URL,
Some("GitHub Copilot"),
true,
);

if let Some(minimax_key) = llm.minimax_key.clone() {
llm.providers
.entry("minimax".to_string())
Expand Down Expand Up @@ -1060,6 +1072,12 @@ impl Config {
.as_deref()
.and_then(resolve_env_value)
.or_else(|| std::env::var("ZAI_CODING_PLAN_API_KEY").ok()),
github_copilot_key: toml
.llm
.github_copilot_key
.as_deref()
.and_then(resolve_env_value)
.or_else(|| std::env::var("GITHUB_COPILOT_API_KEY").ok()),
providers: toml
.llm
.providers
Expand Down Expand Up @@ -1186,6 +1204,16 @@ impl Config {
false,
);

add_shorthand_provider(
&mut llm.providers,
"github-copilot",
llm.github_copilot_key.clone(),
ApiType::OpenAiChatCompletions,
GITHUB_COPILOT_DEFAULT_BASE_URL,
Some("GitHub Copilot"),
true,
);

if let Some(minimax_key) = llm.minimax_key.clone() {
llm.providers
.entry("minimax".to_string())
Expand Down
5 changes: 5 additions & 0 deletions src/config/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +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";

/// App attribution headers sent with every OpenRouter API request.
/// See <https://openrouter.ai/docs/app-attribution>.
Expand Down Expand Up @@ -213,6 +214,9 @@ pub(crate) fn default_provider_config(
use_bearer_auth: false,
extra_headers: vec![],
},
// GitHub Copilot requires token exchange and dynamic base URL derivation.
// The test path should use LlmManager::get_github_copilot_provider() instead.
"github-copilot" => return None,
_ => return None,
})
}
Expand Down Expand Up @@ -274,6 +278,7 @@ pub(super) fn infer_routing_from_providers(
"minimax-cn",
"moonshot",
"zai-coding-plan",
"github-copilot",
];

for &name in PRIORITY {
Expand Down
3 changes: 3 additions & 0 deletions src/config/toml_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ pub(super) struct TomlLlmConfigFields {
pub(super) minimax_cn_key: Option<String>,
pub(super) moonshot_key: Option<String>,
pub(super) zai_coding_plan_key: Option<String>,
pub(super) github_copilot_key: Option<String>,
#[serde(default)]
pub(super) providers: HashMap<String, TomlProviderConfig>,
#[serde(default)]
Expand Down Expand Up @@ -206,6 +207,7 @@ pub(super) struct TomlLlmConfig {
pub(super) minimax_cn_key: Option<String>,
pub(super) moonshot_key: Option<String>,
pub(super) zai_coding_plan_key: Option<String>,
pub(super) github_copilot_key: Option<String>,
pub(super) providers: HashMap<String, TomlProviderConfig>,
}

Expand Down Expand Up @@ -261,6 +263,7 @@ impl<'de> Deserialize<'de> for TomlLlmConfig {
minimax_cn_key: fields.minimax_cn_key,
moonshot_key: fields.moonshot_key,
zai_coding_plan_key: fields.zai_coding_plan_key,
github_copilot_key: fields.github_copilot_key,
providers: fields.providers,
})
}
Expand Down
11 changes: 11 additions & 0 deletions src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ pub struct LlmConfig {
pub minimax_cn_key: Option<String>,
pub moonshot_key: Option<String>,
pub zai_coding_plan_key: Option<String>,
pub github_copilot_key: Option<String>,
pub providers: HashMap<String, ProviderConfig>,
}

Expand Down Expand Up @@ -340,6 +341,10 @@ impl std::fmt::Debug for LlmConfig {
"zai_coding_plan_key",
&self.zai_coding_plan_key.as_ref().map(|_| "[REDACTED]"),
)
.field(
"github_copilot_key",
&self.github_copilot_key.as_ref().map(|_| "[REDACTED]"),
)
.field("providers", &self.providers)
.finish()
}
Expand Down Expand Up @@ -369,6 +374,7 @@ impl LlmConfig {
|| self.minimax_cn_key.is_some()
|| self.moonshot_key.is_some()
|| self.zai_coding_plan_key.is_some()
|| self.github_copilot_key.is_some()
|| !self.providers.is_empty()
}
}
Expand Down Expand Up @@ -500,6 +506,11 @@ impl SystemSecrets for LlmConfig {
secret_name: "SAMBANOVA_API_KEY",
instance_pattern: None,
},
SecretField {
toml_key: "github_copilot_key",
secret_name: "GITHUB_COPILOT_API_KEY",
instance_pattern: None,
},
]
}
}
Expand Down
Loading
Loading