fix(proxy): 修复 Copilot/Codex OAuth 模块绕过全局代理导致 Claude 模型 400 错误 (#4583)

* fix(copilot): 修复 GitHub Copilot 出站请求不走全局代理导致 model not supported

* fix(proxy): 修复全局代理客户端配置未被应用到 Copilot/Codex OAuth 模块

两个关键问题:

1. CopilotAuthManager 在构造时写死 Client::new(),使得拉取 /models 列表、
   换取 token 等认证流程无视全局代理配置(global_proxy_url),直连目标服务。
   结果:直连时 /models 返回 0 个 Claude 模型 → live resolution 失效 →
   模型 ID 无法正确归一化/匹配 → Copilot 上游返回 400 model_not_supported。

2. CodexOAuthManager 也有完全相同的问题,导致 Codex OAuth 认证请求绕过代理。

改动:删除两个模块的自持 http_client 字段,改为每次请求时从全局客户端现取
(crate::proxy::http_client::get())。这样:
- 遵循全局代理 URL 配置
- 支持运行时热更新代理设置
- 符合代码库设计意图(http_client.rs 注释明确说"所有 HTTP 请求应使用此模块")

修复覆盖范围:
- copilot_auth.rs: 7 处调用(token 获取、/models 拉取、model vendor 判断)
- codex_oauth_auth.rs: 4 处调用(device code、OAuth token 刷新)

Fixes #2016 #2931
This commit is contained in:
zhaoyuan
2026-06-25 16:05:23 +08:00
committed by GitHub
parent 6fd4e6f462
commit 524b9d9825
2 changed files with 12 additions and 29 deletions
@@ -16,7 +16,7 @@
//! - 通过 JWT id_token 提取 chatgpt_account_id 作为账号唯一标识
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
@@ -230,7 +230,6 @@ pub struct CodexOAuthManager {
/// 进行中的 Device Code 流程:device_auth_id -> {user_code, expires_at_ms}
/// 过期条目会在 start_device_flow 时被清理,防止放弃的登录流程导致无界增长
pending_device_codes: Arc<RwLock<HashMap<String, PendingDeviceCode>>>,
http_client: Client,
storage_path: PathBuf,
}
@@ -244,7 +243,6 @@ impl CodexOAuthManager {
access_tokens: Arc::new(RwLock::new(HashMap::new())),
refresh_locks: Arc::new(RwLock::new(HashMap::new())),
pending_device_codes: Arc::new(RwLock::new(HashMap::new())),
http_client: Client::new(),
storage_path,
};
@@ -266,8 +264,7 @@ impl CodexOAuthManager {
pub async fn start_device_flow(&self) -> Result<GitHubDeviceCodeResponse, CodexOAuthError> {
log::info!("[CodexOAuth] 启动 Device Code 流程");
let response = self
.http_client
let response = crate::proxy::http_client::get()
.post(DEVICE_AUTH_USERCODE_URL)
.header("Content-Type", "application/json")
.header("User-Agent", CODEX_USER_AGENT)
@@ -349,8 +346,7 @@ impl CodexOAuthManager {
log::debug!("[CodexOAuth] 轮询 Device Code");
let poll_response = self
.http_client
let poll_response = crate::proxy::http_client::get()
.post(DEVICE_AUTH_TOKEN_URL)
.header("Content-Type", "application/json")
.header("User-Agent", CODEX_USER_AGENT)
@@ -431,8 +427,7 @@ impl CodexOAuthManager {
code: &str,
code_verifier: &str,
) -> Result<OAuthTokenResponse, CodexOAuthError> {
let response = self
.http_client
let response = crate::proxy::http_client::get()
.post(OAUTH_TOKEN_URL)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("User-Agent", CODEX_USER_AGENT)
@@ -465,8 +460,7 @@ impl CodexOAuthManager {
&self,
refresh_token: &str,
) -> Result<OAuthTokenResponse, CodexOAuthError> {
let response = self
.http_client
let response = crate::proxy::http_client::get()
.post(OAUTH_TOKEN_URL)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("User-Agent", CODEX_USER_AGENT)
+7 -18
View File
@@ -15,7 +15,6 @@
//! - Provider 通过 meta.authBinding 关联账号
//! - 自动迁移 v1 单账号格式到 v3 多账号 + 默认账号格式
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
@@ -424,8 +423,6 @@ pub struct CopilotAuthManager {
api_endpoints: Arc<RwLock<HashMap<String, String>>>,
/// 每个账号的端点拉取锁,避免并发拉取重复打 GitHub API
endpoint_locks: Arc<RwLock<HashMap<String, Arc<Mutex<()>>>>>,
/// HTTP 客户端
http_client: Client,
/// 存储路径
storage_path: PathBuf,
/// 待迁移的旧格式 token
@@ -447,7 +444,6 @@ impl CopilotAuthManager {
copilot_models: Arc::new(RwLock::new(HashMap::new())),
api_endpoints: Arc::new(RwLock::new(HashMap::new())),
endpoint_locks: Arc::new(RwLock::new(HashMap::new())),
http_client: Client::new(),
storage_path,
pending_migration: Arc::new(RwLock::new(None)),
migration_error: Arc::new(RwLock::new(None)),
@@ -602,8 +598,7 @@ impl CopilotAuthManager {
};
log::info!("[CopilotAuth] 启动设备码流程 (domain: {domain})");
let response = self
.http_client
let response = crate::proxy::http_client::get()
.post(github_device_code_url(&domain))
.header("Accept", "application/json")
.header("User-Agent", COPILOT_USER_AGENT)
@@ -647,8 +642,7 @@ impl CopilotAuthManager {
};
log::debug!("[CopilotAuth] 轮询 OAuth Token (domain: {domain})");
let response = self
.http_client
let response = crate::proxy::http_client::get()
.post(github_oauth_token_url(&domain))
.header("Accept", "application/json")
.header("User-Agent", COPILOT_USER_AGENT)
@@ -831,8 +825,7 @@ impl CopilotAuthManager {
log::info!("[CopilotAuth] 获取账号 {account_id} 的 Copilot 可用模型");
let response = self
.http_client
let response = crate::proxy::http_client::get()
.get(&models_url)
.header("Authorization", format!("Bearer {copilot_token}"))
.header("Content-Type", "application/json")
@@ -919,8 +912,7 @@ impl CopilotAuthManager {
log::info!("[CopilotAuth] 获取账号 {account_id} 的 Copilot 使用量");
let response = self
.http_client
let response = crate::proxy::http_client::get()
.get(copilot_usage_url(&domain))
.header("Authorization", format!("token {github_token}"))
.header("Content-Type", "application/json")
@@ -1034,8 +1026,7 @@ impl CopilotAuthManager {
log::debug!("[CopilotAuth] 为账号 {account_id} 惰性拉取动态 API 端点");
let response = self
.http_client
let response = crate::proxy::http_client::get()
.get(copilot_usage_url(&domain))
.header("Authorization", format!("token {github_token}"))
.header("Content-Type", "application/json")
@@ -1312,8 +1303,7 @@ impl CopilotAuthManager {
github_token: &str,
domain: &str,
) -> Result<GitHubUser, CopilotAuthError> {
let response = self
.http_client
let response = crate::proxy::http_client::get()
.get(github_user_url(domain))
.header("Authorization", format!("token {github_token}"))
.header("User-Agent", COPILOT_USER_AGENT)
@@ -1345,8 +1335,7 @@ impl CopilotAuthManager {
) -> Result<(), CopilotAuthError> {
log::debug!("[CopilotAuth] 获取账号 {account_id} 的 Copilot Token (domain: {domain})");
let response = self
.http_client
let response = crate::proxy::http_client::get()
.get(copilot_token_url(domain))
.header("Authorization", format!("token {github_token}"))
.header("User-Agent", COPILOT_USER_AGENT)