- 支持 Claude Desktop 使用 Copilot/Codex OAuth 供应商

- 放开本地路由托管 OAuth 供应商校验,允许动态 Token
- 新增 Claude Desktop Copilot/Codex 预设与账号选择
- 添加 OAuth proxy 回归测试
This commit is contained in:
Jason
2026-05-12 11:30:11 +08:00
parent 953b7cdcf9
commit 6a3c2fe0ba
5 changed files with 209 additions and 60 deletions
+62 -11
View File
@@ -370,17 +370,6 @@ pub fn validate_proxy_provider(provider: &Provider) -> Result<(), AppError> {
}
if let Some(meta) = provider.meta.as_ref() {
if matches!(
meta.provider_type.as_deref(),
Some("github_copilot") | Some("codex_oauth")
) {
return Err(AppError::localized(
"claude_desktop.provider.type_unsupported",
"Claude Desktop 本地路由模式暂不支持 Copilot 或 Codex OAuth 供应商",
"Claude Desktop proxy mode does not support Copilot or Codex OAuth providers yet",
));
}
if let Some(api_format) = meta.api_format.as_deref() {
if !matches!(
api_format,
@@ -419,6 +408,10 @@ fn has_proxy_base_url_and_key(provider: &Provider) -> bool {
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if is_managed_oauth_proxy_provider(provider) {
return has_base_url;
}
let has_key = env
.and_then(|value| {
[
@@ -440,6 +433,14 @@ fn has_proxy_base_url_and_key(provider: &Provider) -> bool {
has_base_url && has_key
}
fn is_managed_oauth_proxy_provider(provider: &Provider) -> bool {
provider
.meta
.as_ref()
.and_then(|meta| meta.provider_type.as_deref())
.is_some_and(|provider_type| matches!(provider_type, "github_copilot" | "codex_oauth"))
}
pub fn validate_provider(provider: &Provider) -> Result<(), AppError> {
if is_official_provider(provider) {
return Ok(());
@@ -1066,6 +1067,33 @@ mod tests {
provider
}
fn oauth_proxy_provider(id: &str, provider_type: &str, api_format: &str) -> Provider {
let mut provider = Provider::with_id(
id.to_string(),
"OAuth Proxy".to_string(),
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://oauth-upstream.example.com"
}
}),
Some("https://example.com".to_string()),
);
provider.meta = Some(ProviderMeta {
claude_desktop_mode: Some(ClaudeDesktopMode::Proxy),
api_format: Some(api_format.to_string()),
provider_type: Some(provider_type.to_string()),
claude_desktop_model_routes: std::collections::HashMap::from([(
"claude-gpt-5-4".to_string(),
ClaudeDesktopModelRoute {
model: "gpt-5.4".to_string(),
supports_1m: Some(false),
},
)]),
..Default::default()
});
provider
}
fn direct_provider_with_models(id: &str) -> Provider {
let mut provider = direct_provider(id);
provider.meta = Some(ProviderMeta {
@@ -1163,6 +1191,29 @@ mod tests {
assert!(!profile.to_string().contains("kimi-k2"));
}
#[test]
fn claude_desktop_proxy_accepts_managed_oauth_providers_without_static_key() {
for (provider_type, api_format) in [
("github_copilot", "openai_chat"),
("codex_oauth", "openai_responses"),
] {
let provider = oauth_proxy_provider(provider_type, provider_type, api_format);
validate_proxy_provider(&provider).expect("oauth proxy provider should validate");
let temp = TempDir::new().expect("tempdir");
let paths = test_paths(temp.path());
let db = test_db();
apply_provider_to_paths(&db, &provider, &paths).expect("apply oauth proxy provider");
let profile: Value = read_json_file(&paths.profile_path).expect("read profile");
assert_eq!(
profile["inferenceGatewayBaseUrl"],
json!("http://127.0.0.1:15721/claude-desktop")
);
assert_eq!(profile["inferenceModels"], json!(["claude-gpt-5-4"]));
}
}
#[test]
fn claude_desktop_proxy_maps_known_route_and_rejects_unknown_route() {
let provider = proxy_provider("proxy");
+17 -13
View File
@@ -184,16 +184,6 @@ pub fn import_claude_desktop_providers_from_claude(
continue;
}
if matches!(
provider
.meta
.as_ref()
.and_then(|meta| meta.provider_type.as_deref()),
Some("github_copilot") | Some("codex_oauth")
) {
continue;
}
let mut desktop_provider = provider.clone();
desktop_provider.in_failover_queue = false;
let meta = desktop_provider.meta.get_or_insert_with(Default::default);
@@ -249,12 +239,20 @@ fn suggested_claude_desktop_routes(
.get("env")
.and_then(|value| value.as_object())?;
let mut routes = std::collections::HashMap::new();
let supports_1m = !matches!(
provider
.meta
.as_ref()
.and_then(|meta| meta.provider_type.as_deref()),
Some("github_copilot") | Some("codex_oauth")
);
fn add_route(
routes: &mut std::collections::HashMap<String, crate::provider::ClaudeDesktopModelRoute>,
env: &serde_json::Map<String, serde_json::Value>,
route_id: &str,
env_key: &str,
supports_1m: bool,
) {
if let Some(model) = env
.get(env_key)
@@ -266,19 +264,25 @@ fn suggested_claude_desktop_routes(
route_id.to_string(),
crate::provider::ClaudeDesktopModelRoute {
model: model.to_string(),
supports_1m: Some(true),
supports_1m: Some(supports_1m),
},
);
}
}
for spec in crate::claude_desktop_config::DEFAULT_PROXY_ROUTES {
add_route(&mut routes, env, spec.route_id, spec.env_key);
add_route(&mut routes, env, spec.route_id, spec.env_key, supports_1m);
}
let primary_route = crate::claude_desktop_config::DEFAULT_PROXY_ROUTES[0];
if !routes.contains_key(primary_route.route_id) {
add_route(&mut routes, env, primary_route.route_id, "ANTHROPIC_MODEL");
add_route(
&mut routes,
env,
primary_route.route_id,
"ANTHROPIC_MODEL",
supports_1m,
);
}
(!routes.is_empty()).then_some(routes)