mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +08:00
fix(proxy): strip OpenRouter hop-by-hop request headers
Scope the guard to Codex OpenRouter chat/responses requests only. Refs #1888
This commit is contained in:
@@ -939,6 +939,10 @@ impl RequestForwarder {
|
||||
.parse::<http::Uri>()
|
||||
.ok()
|
||||
.and_then(|u| u.authority().map(|a| a.to_string()));
|
||||
let apply_openrouter_codex_header_guard =
|
||||
should_guard_openrouter_codex_headers(adapter.name(), &base_url, &effective_endpoint);
|
||||
let connection_header_tokens =
|
||||
apply_openrouter_codex_header_guard.then(|| collect_connection_header_tokens(headers));
|
||||
|
||||
// 预计算 anthropic-beta 值(仅 Claude)
|
||||
let anthropic_beta_value = if adapter.name() == "Claude" {
|
||||
@@ -1016,6 +1020,13 @@ impl RequestForwarder {
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- OpenRouter chat/responses 专用:补充剥离 hop-by-hop 请求头 ---
|
||||
if let Some(connection_header_tokens) = connection_header_tokens.as_ref() {
|
||||
if should_strip_openrouter_codex_request_header(key_str, connection_header_tokens) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 认证类 — 用 adapter 提供的认证头替换(在原始位置) ---
|
||||
if key_str.eq_ignore_ascii_case("authorization")
|
||||
|| key_str.eq_ignore_ascii_case("x-api-key")
|
||||
@@ -1318,6 +1329,61 @@ fn is_bedrock_provider(provider: &Provider) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
const OPENROUTER_CODEX_HOP_BY_HOP_REQUEST_HEADERS: &[&str] = &[
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"proxy-connection",
|
||||
"te",
|
||||
"trailer",
|
||||
"trailers",
|
||||
"upgrade",
|
||||
];
|
||||
|
||||
fn should_guard_openrouter_codex_headers(
|
||||
adapter_name: &str,
|
||||
base_url: &str,
|
||||
endpoint: &str,
|
||||
) -> bool {
|
||||
if adapter_name != "Codex" || !base_url.contains("openrouter.ai") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let path = endpoint
|
||||
.split_once('?')
|
||||
.map(|(path, _)| path)
|
||||
.unwrap_or(endpoint);
|
||||
|
||||
matches!(
|
||||
path,
|
||||
"/chat/completions" | "/responses" | "/responses/compact"
|
||||
)
|
||||
}
|
||||
|
||||
fn collect_connection_header_tokens(
|
||||
headers: &axum::http::HeaderMap,
|
||||
) -> std::collections::HashSet<String> {
|
||||
headers
|
||||
.get_all("connection")
|
||||
.iter()
|
||||
.filter_map(|value| value.to_str().ok())
|
||||
.flat_map(|value| value.split(','))
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(|name| name.to_ascii_lowercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn should_strip_openrouter_codex_request_header(
|
||||
key_str: &str,
|
||||
connection_header_tokens: &std::collections::HashSet<String>,
|
||||
) -> bool {
|
||||
let lower_key = key_str.to_ascii_lowercase();
|
||||
OPENROUTER_CODEX_HOP_BY_HOP_REQUEST_HEADERS.contains(&lower_key.as_str())
|
||||
|| connection_header_tokens.contains(lower_key.as_str())
|
||||
}
|
||||
|
||||
fn build_retryable_failure_log(
|
||||
provider_name: &str,
|
||||
attempted_providers: usize,
|
||||
@@ -1700,6 +1766,73 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_connection_header_tokens_tracks_dynamic_hop_by_hop_headers() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"connection",
|
||||
HeaderValue::from_static("keep-alive, x-custom-hop, Upgrade"),
|
||||
);
|
||||
|
||||
let tokens = collect_connection_header_tokens(&headers);
|
||||
|
||||
assert!(tokens.contains("keep-alive"));
|
||||
assert!(tokens.contains("x-custom-hop"));
|
||||
assert!(tokens.contains("upgrade"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_guard_openrouter_codex_headers_only_for_openrouter_chat_and_responses() {
|
||||
assert!(should_guard_openrouter_codex_headers(
|
||||
"Codex",
|
||||
"https://openrouter.ai/api/v1",
|
||||
"/chat/completions"
|
||||
));
|
||||
assert!(should_guard_openrouter_codex_headers(
|
||||
"Codex",
|
||||
"https://openrouter.ai/api/v1",
|
||||
"/responses?stream=true"
|
||||
));
|
||||
assert!(should_guard_openrouter_codex_headers(
|
||||
"Codex",
|
||||
"https://openrouter.ai/api/v1",
|
||||
"/responses/compact"
|
||||
));
|
||||
assert!(!should_guard_openrouter_codex_headers(
|
||||
"Claude",
|
||||
"https://openrouter.ai/api",
|
||||
"/v1/messages"
|
||||
));
|
||||
assert!(!should_guard_openrouter_codex_headers(
|
||||
"Codex",
|
||||
"https://api.openai.com/v1",
|
||||
"/responses"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_strip_openrouter_codex_request_header_covers_static_and_dynamic_hop_by_hop_headers() {
|
||||
let mut connection_tokens = std::collections::HashSet::new();
|
||||
connection_tokens.insert("x-custom-hop".to_string());
|
||||
|
||||
assert!(should_strip_openrouter_codex_request_header(
|
||||
"connection",
|
||||
&connection_tokens
|
||||
));
|
||||
assert!(should_strip_openrouter_codex_request_header(
|
||||
"proxy-connection",
|
||||
&connection_tokens
|
||||
));
|
||||
assert!(should_strip_openrouter_codex_request_header(
|
||||
"x-custom-hop",
|
||||
&connection_tokens
|
||||
));
|
||||
assert!(!should_strip_openrouter_codex_request_header(
|
||||
"anthropic-version",
|
||||
&connection_tokens
|
||||
));
|
||||
}
|
||||
|
||||
// ==================== Copilot 动态 endpoint 路由相关测试 ====================
|
||||
|
||||
/// 验证 is_copilot 检测逻辑:通过 provider_type 判断
|
||||
|
||||
Reference in New Issue
Block a user