mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-20 20:33:22 +08:00
fix(proxy): passthrough client query params instead of forcing beta=true
This commit is contained in:
@@ -561,15 +561,17 @@ impl RequestForwarder {
|
||||
// 检查是否需要格式转换
|
||||
let needs_transform = adapter.needs_transform(provider);
|
||||
|
||||
let effective_endpoint =
|
||||
if needs_transform && adapter.name() == "Claude" && endpoint == "/v1/messages" {
|
||||
"/v1/chat/completions"
|
||||
} else {
|
||||
endpoint
|
||||
};
|
||||
let effective_endpoint = if needs_transform
|
||||
&& adapter.name() == "Claude"
|
||||
&& endpoint.starts_with("/v1/messages")
|
||||
{
|
||||
endpoint.replacen("/v1/messages", "/v1/chat/completions", 1)
|
||||
} else {
|
||||
endpoint.to_string()
|
||||
};
|
||||
|
||||
// 使用适配器构建 URL
|
||||
let url = adapter.build_url(&base_url, effective_endpoint);
|
||||
let url = adapter.build_url(&base_url, &effective_endpoint);
|
||||
|
||||
// 应用模型映射(独立于格式转换)
|
||||
let (mapped_body, _original_model, _mapped_model) =
|
||||
|
||||
@@ -56,6 +56,7 @@ pub async fn get_status(State(state): State<ProxyState>) -> Result<Json<ProxySta
|
||||
/// - 现在 OpenRouter 已推出 Claude Code 兼容接口,默认不再启用该转换(逻辑保留以备回退)
|
||||
pub async fn handle_messages(
|
||||
State(state): State<ProxyState>,
|
||||
uri: axum::http::Uri,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(body): Json<Value>,
|
||||
) -> Result<axum::response::Response, ProxyError> {
|
||||
@@ -67,12 +68,18 @@ pub async fn handle_messages(
|
||||
.and_then(|s| s.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
// 透传客户端 query 参数(例如 ?beta=true),路径统一为 /v1/messages
|
||||
let endpoint = uri
|
||||
.query()
|
||||
.map(|q| format!("/v1/messages?{q}"))
|
||||
.unwrap_or_else(|| "/v1/messages".to_string());
|
||||
|
||||
// 转发请求
|
||||
let forwarder = ctx.create_forwarder(&state);
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Claude,
|
||||
"/v1/messages",
|
||||
&endpoint,
|
||||
body.clone(),
|
||||
headers,
|
||||
ctx.get_providers(),
|
||||
|
||||
@@ -280,17 +280,7 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
// 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致)
|
||||
let base = dedup_v1_v1_boundary_safe(base);
|
||||
|
||||
// 为 Claude Messages 端点添加 ?beta=true 参数
|
||||
// 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数
|
||||
// 仅对 /v1/messages 生效,不对 /v1/chat/completions 生效
|
||||
let needs_beta =
|
||||
base.contains("/v1/messages") && !base.contains('?') && !suffix.starts_with('?');
|
||||
|
||||
if needs_beta {
|
||||
format!("{base}?beta=true{suffix}")
|
||||
} else {
|
||||
format!("{base}{suffix}")
|
||||
}
|
||||
format!("{base}{suffix}")
|
||||
}
|
||||
|
||||
fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder {
|
||||
@@ -502,23 +492,21 @@ mod tests {
|
||||
#[test]
|
||||
fn test_build_url_anthropic() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// /v1/messages 端点会自动添加 ?beta=true 参数
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/messages");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?beta=true");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_openrouter() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// /v1/messages 端点会自动添加 ?beta=true 参数
|
||||
let url = adapter.build_url("https://openrouter.ai/api", "/v1/messages");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/messages?beta=true");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_no_beta_for_other_endpoints() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// 非 /v1/messages 端点不添加 ?beta=true
|
||||
// 非 /v1/messages 端点保持原样
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/complete");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/complete");
|
||||
}
|
||||
@@ -536,7 +524,7 @@ mod tests {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// base_url 已包含完整路径 /v1/messages,不再追加
|
||||
let url = adapter.build_url("https://example.com/api/v1/messages", "/v1/messages");
|
||||
assert_eq!(url, "https://example.com/api/v1/messages?beta=true");
|
||||
assert_eq!(url, "https://example.com/api/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -574,14 +562,11 @@ mod tests {
|
||||
"https://integrate.api.nvidia.com/v1",
|
||||
"/v1/chat/completions",
|
||||
);
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://integrate.api.nvidia.com/v1/chat/completions"
|
||||
);
|
||||
assert_eq!(url, "https://integrate.api.nvidia.com/v1/chat/completions");
|
||||
|
||||
// 另一个场景:/v1 + /v1/messages
|
||||
let url2 = adapter.build_url("https://api.example.com/v1", "/v1/messages");
|
||||
assert_eq!(url2, "https://api.example.com/v1/messages?beta=true");
|
||||
assert_eq!(url2, "https://api.example.com/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -144,7 +144,7 @@ fn build_runtime_like_url(
|
||||
is_proxy: bool,
|
||||
) -> String {
|
||||
match app_type {
|
||||
// Claude 代理预览需要展示运行时附加参数(如 ?beta=true)
|
||||
// Claude 代理预览需要展示与运行时一致的 URL 归一化结果
|
||||
AppType::Claude if is_proxy => ClaudeAdapter::new().build_url(base_url, endpoint),
|
||||
// Codex/OpenCode 预览复用运行时 /v1 归一化规则
|
||||
AppType::Codex | AppType::OpenCode => CodexAdapter::new().build_url(base_url, endpoint),
|
||||
@@ -226,7 +226,7 @@ pub fn check_proxy_requirement(
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直连地址和代理地址路径不同,需要代理(忽略查询参数差异,如 Claude ?beta=true)
|
||||
// 如果直连地址和代理地址路径不同,需要代理(忽略查询参数差异)
|
||||
let (direct_base, _) = split_url_suffix(&preview.direct_url);
|
||||
let (proxy_base, _) = split_url_suffix(&preview.proxy_url);
|
||||
if direct_base != proxy_base {
|
||||
@@ -248,10 +248,7 @@ mod tests {
|
||||
Some("anthropic"),
|
||||
);
|
||||
assert_eq!(preview.direct_url, "https://api.example.com/v1/messages");
|
||||
assert_eq!(
|
||||
preview.proxy_url,
|
||||
"https://api.example.com/v1/messages?beta=true"
|
||||
);
|
||||
assert_eq!(preview.proxy_url, "https://api.example.com/v1/messages");
|
||||
assert!(!preview.is_full_url);
|
||||
}
|
||||
|
||||
@@ -282,10 +279,7 @@ mod tests {
|
||||
preview.direct_url,
|
||||
"https://api.example.com/v1/messages/v1/messages"
|
||||
);
|
||||
assert_eq!(
|
||||
preview.proxy_url,
|
||||
"https://api.example.com/v1/messages?beta=true"
|
||||
);
|
||||
assert_eq!(preview.proxy_url, "https://api.example.com/v1/messages");
|
||||
assert!(preview.is_full_url);
|
||||
}
|
||||
|
||||
@@ -397,11 +391,8 @@ mod tests {
|
||||
Some("anthropic"),
|
||||
);
|
||||
assert_eq!(preview.direct_url, "https://api.example.com/v1/v1/messages");
|
||||
// 代理地址按运行时规则构建,会去重并附加 ?beta=true
|
||||
assert_eq!(
|
||||
preview.proxy_url,
|
||||
"https://api.example.com/v1/messages?beta=true"
|
||||
);
|
||||
// 代理地址按运行时规则构建,会进行路径去重
|
||||
assert_eq!(preview.proxy_url, "https://api.example.com/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -431,7 +422,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
preview.proxy_url,
|
||||
"https://api.example.com/v1/messages?beta=true#frag"
|
||||
"https://api.example.com/v1/messages#frag"
|
||||
);
|
||||
assert!(preview.is_full_url);
|
||||
}
|
||||
|
||||
@@ -285,11 +285,11 @@ impl StreamCheckService {
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<(u16, String), AppError> {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
// URL 必须包含 ?beta=true 参数(某些中转服务依赖此参数验证请求来源)
|
||||
// 健康检查不强制附加查询参数,保持与默认 Claude 路径一致
|
||||
let url = if base.ends_with("/v1") {
|
||||
format!("{base}/messages?beta=true")
|
||||
format!("{base}/messages")
|
||||
} else {
|
||||
format!("{base}/v1/messages?beta=true")
|
||||
format!("{base}/v1/messages")
|
||||
};
|
||||
|
||||
let body = json!({
|
||||
|
||||
Reference in New Issue
Block a user