mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 11:01:22 +08:00
feat(proxy): add smart URL path detection for full API endpoints
- Detect when base URL already contains API path suffix (/v1/messages, /v1/chat/completions, etc.) - Show direct and proxy request URL previews in endpoint field - Block provider switch when proxy is required but not active - Add API format selector for Codex providers (Responses/Chat)
This commit is contained in:
@@ -252,11 +252,28 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
// 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。
|
||||
// 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。
|
||||
|
||||
let mut base = format!(
|
||||
"{}/{}",
|
||||
base_url.trim_end_matches('/'),
|
||||
endpoint.trim_start_matches('/')
|
||||
);
|
||||
let base_trimmed = base_url.trim_end_matches('/');
|
||||
let endpoint_trimmed = endpoint.trim_start_matches('/');
|
||||
|
||||
// 检测 base_url 是否已经以 API 路径结尾(用户填写了完整路径)
|
||||
// 支持的 API 路径模式:/v1/messages, /messages, /v1/chat/completions, /chat/completions
|
||||
let api_path_patterns = [
|
||||
"/v1/messages",
|
||||
"/messages",
|
||||
"/v1/chat/completions",
|
||||
"/chat/completions",
|
||||
];
|
||||
|
||||
let base_ends_with_api_path = api_path_patterns
|
||||
.iter()
|
||||
.any(|pattern| base_trimmed.to_lowercase().ends_with(pattern));
|
||||
|
||||
// 如果 base_url 已经以 API 路径结尾,直接使用 base_url,不再追加 endpoint
|
||||
let mut base = if base_ends_with_api_path {
|
||||
base_trimmed.to_string()
|
||||
} else {
|
||||
format!("{base_trimmed}/{endpoint_trimmed}")
|
||||
};
|
||||
|
||||
// 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致)
|
||||
while base.contains("/v1/v1") {
|
||||
@@ -266,9 +283,11 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
// 为 Claude 相关端点添加 ?beta=true 参数
|
||||
// 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数
|
||||
// 注:openai_chat 模式下会转发到 /v1/chat/completions,此处也需要保持一致
|
||||
if (endpoint.contains("/v1/messages") || endpoint.contains("/v1/chat/completions"))
|
||||
&& !endpoint.contains('?')
|
||||
{
|
||||
// 检查最终 URL 是否包含需要 beta 参数的路径,且没有查询参数
|
||||
let needs_beta = (base.contains("/v1/messages") || base.contains("/v1/chat/completions"))
|
||||
&& !base.contains('?');
|
||||
|
||||
if needs_beta {
|
||||
format!("{base}?beta=true")
|
||||
} else {
|
||||
base
|
||||
@@ -513,6 +532,59 @@ mod tests {
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?foo=bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_full_path_messages() {
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_full_path_chat_completions() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// base_url 已包含完整路径 /v1/chat/completions,不再追加
|
||||
let url = adapter.build_url(
|
||||
"https://opencode.ai/zen/v1/chat/completions",
|
||||
"/v1/chat/completions",
|
||||
);
|
||||
assert_eq!(url, "https://opencode.ai/zen/v1/chat/completions?beta=true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_full_path_short_suffix() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// base_url 以 /messages 结尾(无 /v1 前缀)
|
||||
let url = adapter.build_url("https://example.com/api/messages", "/v1/messages");
|
||||
assert_eq!(url, "https://example.com/api/messages");
|
||||
|
||||
// base_url 以 /chat/completions 结尾(无 /v1 前缀)
|
||||
let url2 = adapter.build_url(
|
||||
"https://example.com/api/chat/completions",
|
||||
"/v1/chat/completions",
|
||||
);
|
||||
assert_eq!(url2, "https://example.com/api/chat/completions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_v1_base_dedup() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// base_url 以 /v1 结尾,endpoint 也以 /v1 开头,应该去重
|
||||
// 场景:https://integrate.api.nvidia.com/v1 + /v1/chat/completions
|
||||
let url = adapter.build_url(
|
||||
"https://integrate.api.nvidia.com/v1",
|
||||
"/v1/chat/completions",
|
||||
);
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://integrate.api.nvidia.com/v1/chat/completions?beta=true"
|
||||
);
|
||||
|
||||
// 另一个场景:/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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_needs_transform() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
@@ -141,6 +141,24 @@ impl ProviderAdapter for CodexAdapter {
|
||||
let base_trimmed = base_url.trim_end_matches('/');
|
||||
let endpoint_trimmed = endpoint.trim_start_matches('/');
|
||||
|
||||
// 检测 base_url 是否已经以 API 路径结尾(用户填写了完整路径)
|
||||
// 支持的 API 路径模式:/v1/responses, /responses, /v1/chat/completions, /chat/completions
|
||||
let api_path_patterns = [
|
||||
"/v1/responses",
|
||||
"/responses",
|
||||
"/v1/chat/completions",
|
||||
"/chat/completions",
|
||||
];
|
||||
|
||||
let base_ends_with_api_path = api_path_patterns
|
||||
.iter()
|
||||
.any(|pattern| base_trimmed.to_lowercase().ends_with(pattern));
|
||||
|
||||
// 如果 base_url 已经以 API 路径结尾,直接使用 base_url,不再追加 endpoint
|
||||
if base_ends_with_api_path {
|
||||
return base_trimmed.to_string();
|
||||
}
|
||||
|
||||
// OpenAI/Codex 的 base_url 可能是:
|
||||
// - 纯 origin: https://api.openai.com (需要自动补 /v1)
|
||||
// - 已含 /v1: https://api.openai.com/v1 (直接拼接)
|
||||
@@ -268,6 +286,49 @@ mod tests {
|
||||
assert_eq!(url, "https://www.packyapi.com/v1/responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_full_path_responses() {
|
||||
let adapter = CodexAdapter::new();
|
||||
// base_url 已包含完整路径 /v1/responses,不再追加
|
||||
let url = adapter.build_url("https://example.com/v1/responses", "/responses");
|
||||
assert_eq!(url, "https://example.com/v1/responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_full_path_chat_completions() {
|
||||
let adapter = CodexAdapter::new();
|
||||
// base_url 已包含完整路径 /v1/chat/completions,不再追加
|
||||
let url = adapter.build_url(
|
||||
"https://example.com/v1/chat/completions",
|
||||
"/chat/completions",
|
||||
);
|
||||
assert_eq!(url, "https://example.com/v1/chat/completions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_full_path_short_suffix() {
|
||||
let adapter = CodexAdapter::new();
|
||||
// base_url 以 /responses 结尾(无 /v1 前缀)
|
||||
let url = adapter.build_url("https://example.com/api/responses", "/responses");
|
||||
assert_eq!(url, "https://example.com/api/responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_v1_base_dedup() {
|
||||
let adapter = CodexAdapter::new();
|
||||
// base_url 以 /v1 结尾,endpoint 也以 /v1 开头,应该去重
|
||||
// 场景:https://integrate.api.nvidia.com/v1 + /v1/responses
|
||||
let url = adapter.build_url("https://integrate.api.nvidia.com/v1", "/v1/responses");
|
||||
assert_eq!(url, "https://integrate.api.nvidia.com/v1/responses");
|
||||
|
||||
// 另一个场景:/v1 + /v1/chat/completions
|
||||
let url2 = adapter.build_url(
|
||||
"https://integrate.api.nvidia.com/v1",
|
||||
"/v1/chat/completions",
|
||||
);
|
||||
assert_eq!(url2, "https://integrate.api.nvidia.com/v1/chat/completions");
|
||||
}
|
||||
|
||||
// 官方客户端检测测试
|
||||
#[test]
|
||||
fn test_is_official_client_vscode() {
|
||||
|
||||
Reference in New Issue
Block a user