mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-12 23:23:20 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 543d0df5c1 | |||
| 6d7ec14644 | |||
| 8f1ad6e057 | |||
| 2b3c80703c | |||
| eef328d2a4 | |||
| 4eb983c58f | |||
| cef3812745 | |||
| 6c9a7ef949 | |||
| 48c434a20a | |||
| 4f6dfff179 | |||
| 4b9cca12d3 | |||
| 8db44a78b2 | |||
| efad0c0f91 | |||
| b536bd0366 | |||
| 783bc60329 | |||
| 924e5386f9 | |||
| 74f67bc1ee | |||
| ae5d05b08c | |||
| 4edb08cd53 | |||
| a8ea99c3fe | |||
| 22c0e7bb5c | |||
| 8af31c3e61 |
@@ -561,12 +561,19 @@ impl RequestForwarder {
|
||||
// 检查是否需要格式转换
|
||||
let needs_transform = adapter.needs_transform(provider);
|
||||
|
||||
let effective_endpoint =
|
||||
if needs_transform && adapter.name() == "Claude" && endpoint == "/v1/messages" {
|
||||
// 确定有效端点:
|
||||
// - 如果需要转换且是 Claude 的 /v1/messages 端点,改写为 /v1/chat/completions
|
||||
// - 但如果 base_url 已包含 chat/completions,则不再自动添加
|
||||
let effective_endpoint = if needs_transform && adapter.name() == "Claude" {
|
||||
let base_has_chat_completions = base_url.contains("chat/completions");
|
||||
if endpoint == "/v1/messages" && !base_has_chat_completions {
|
||||
"/v1/chat/completions"
|
||||
} else {
|
||||
endpoint
|
||||
};
|
||||
}
|
||||
} else {
|
||||
endpoint
|
||||
};
|
||||
|
||||
// 使用适配器构建 URL
|
||||
let url = adapter.build_url(&base_url, effective_endpoint);
|
||||
|
||||
@@ -23,10 +23,16 @@ impl ClaudeAdapter {
|
||||
/// 获取供应商类型
|
||||
///
|
||||
/// 根据 base_url 和 auth_mode 检测具体的供应商类型:
|
||||
/// - ChatCompletions: chat_completions_mode 为 true(优先级最高)
|
||||
/// - OpenRouter: base_url 包含 openrouter.ai
|
||||
/// - ClaudeAuth: auth_mode 为 bearer_only
|
||||
/// - Claude: 默认 Anthropic 官方
|
||||
pub fn provider_type(&self, provider: &Provider) -> ProviderType {
|
||||
// 检测 ChatCompletions 模式(优先级最高)
|
||||
if self.is_chat_completions_mode(provider) {
|
||||
return ProviderType::ChatCompletions;
|
||||
}
|
||||
|
||||
// 检测 OpenRouter
|
||||
if self.is_openrouter(provider) {
|
||||
return ProviderType::OpenRouter;
|
||||
@@ -48,6 +54,20 @@ impl ClaudeAdapter {
|
||||
false
|
||||
}
|
||||
|
||||
/// 检测是否启用 ChatCompletions 兼容模式
|
||||
fn is_chat_completions_mode(&self, provider: &Provider) -> bool {
|
||||
let raw = provider.settings_config.get("chat_completions_mode");
|
||||
match raw {
|
||||
Some(serde_json::Value::Bool(enabled)) => *enabled,
|
||||
Some(serde_json::Value::Number(num)) => num.as_i64().unwrap_or(0) != 0,
|
||||
Some(serde_json::Value::String(value)) => {
|
||||
let normalized = value.trim().to_lowercase();
|
||||
normalized == "true" || normalized == "1"
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检测 OpenRouter 是否启用兼容模式
|
||||
fn is_openrouter_compat_enabled(&self, provider: &Provider) -> bool {
|
||||
if !self.is_openrouter(provider) {
|
||||
@@ -253,13 +273,9 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_transform(&self, _provider: &Provider) -> bool {
|
||||
// NOTE:
|
||||
// OpenRouter 已推出 Claude Code 兼容接口(可直接处理 `/v1/messages`),默认不再启用
|
||||
// Anthropic ↔ OpenAI 的格式转换。
|
||||
//
|
||||
// 如果未来需要回退到旧的 OpenAI Chat Completions 方案,可恢复下面这行:
|
||||
self.is_openrouter_compat_enabled(_provider)
|
||||
fn needs_transform(&self, provider: &Provider) -> bool {
|
||||
// ChatCompletions 模式或 OpenRouter 兼容模式都需要转换
|
||||
self.is_chat_completions_mode(provider) || self.is_openrouter_compat_enabled(provider)
|
||||
}
|
||||
|
||||
fn transform_request(
|
||||
|
||||
@@ -50,6 +50,8 @@ pub enum ProviderType {
|
||||
GeminiCli,
|
||||
/// OpenRouter(已支持 Claude Code 兼容接口,默认透传;保留旧转换逻辑备用)
|
||||
OpenRouter,
|
||||
/// ChatCompletions 兼容模式(Anthropic ↔ OpenAI 格式转换,不限制上游地址)
|
||||
ChatCompletions,
|
||||
}
|
||||
|
||||
impl ProviderType {
|
||||
@@ -75,6 +77,7 @@ impl ProviderType {
|
||||
"https://generativelanguage.googleapis.com"
|
||||
}
|
||||
ProviderType::OpenRouter => "https://openrouter.ai/api",
|
||||
ProviderType::ChatCompletions => "https://api.openai.com",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +151,7 @@ impl ProviderType {
|
||||
ProviderType::Gemini => "gemini",
|
||||
ProviderType::GeminiCli => "gemini_cli",
|
||||
ProviderType::OpenRouter => "openrouter",
|
||||
ProviderType::ChatCompletions => "chat_completions",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +173,9 @@ impl std::str::FromStr for ProviderType {
|
||||
"gemini" => Ok(ProviderType::Gemini),
|
||||
"gemini_cli" | "gemini-cli" => Ok(ProviderType::GeminiCli),
|
||||
"openrouter" => Ok(ProviderType::OpenRouter),
|
||||
"chat_completions" | "chat-completions" | "chatcompletions" => {
|
||||
Ok(ProviderType::ChatCompletions)
|
||||
}
|
||||
_ => Err(format!("Invalid provider type: {s}")),
|
||||
}
|
||||
}
|
||||
@@ -191,9 +198,10 @@ pub fn get_adapter(app_type: &AppType) -> Box<dyn ProviderAdapter> {
|
||||
#[allow(dead_code)]
|
||||
pub fn get_adapter_for_provider_type(provider_type: &ProviderType) -> Box<dyn ProviderAdapter> {
|
||||
match provider_type {
|
||||
ProviderType::Claude | ProviderType::ClaudeAuth | ProviderType::OpenRouter => {
|
||||
Box::new(ClaudeAdapter::new())
|
||||
}
|
||||
ProviderType::Claude
|
||||
| ProviderType::ClaudeAuth
|
||||
| ProviderType::OpenRouter
|
||||
| ProviderType::ChatCompletions => Box::new(ClaudeAdapter::new()),
|
||||
ProviderType::Codex => Box::new(CodexAdapter::new()),
|
||||
ProviderType::Gemini | ProviderType::GeminiCli => Box::new(GeminiAdapter::new()),
|
||||
}
|
||||
|
||||
@@ -115,16 +115,22 @@ pub fn create_anthropic_sse_stream(
|
||||
|
||||
if let Some(choice) = chunk.choices.first() {
|
||||
if !has_sent_message_start {
|
||||
// 构建完整的 message_start 事件,与原生 Anthropic 格式一致
|
||||
let event = json!({
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"id": message_id.clone().unwrap_or_default(),
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [],
|
||||
"model": current_model.clone().unwrap_or_default(),
|
||||
"stop_reason": null,
|
||||
"stop_sequence": null,
|
||||
"usage": {
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0
|
||||
"input_tokens": chunk.usage.as_ref().map(|u| u.prompt_tokens).unwrap_or(1),
|
||||
"output_tokens": 0,
|
||||
"cache_creation_input_tokens": 0,
|
||||
"cache_read_input_tokens": 0
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -272,18 +278,17 @@ pub fn create_anthropic_sse_stream(
|
||||
}
|
||||
|
||||
let stop_reason = map_stop_reason(Some(finish_reason));
|
||||
// 构建 usage 信息,包含 input_tokens 和 output_tokens
|
||||
let usage_json = chunk.usage.as_ref().map(|u| json!({
|
||||
"input_tokens": u.prompt_tokens,
|
||||
"output_tokens": u.completion_tokens
|
||||
}));
|
||||
// 构建 usage 信息,Anthropic 格式只需要 output_tokens
|
||||
let output_tokens = chunk.usage.as_ref().map(|u| u.completion_tokens).unwrap_or(0);
|
||||
let event = json!({
|
||||
"type": "message_delta",
|
||||
"delta": {
|
||||
"stop_reason": stop_reason,
|
||||
"stop_sequence": null
|
||||
},
|
||||
"usage": usage_json
|
||||
"usage": {
|
||||
"output_tokens": output_tokens
|
||||
}
|
||||
});
|
||||
let sse_data = format!("event: message_delta\ndata: {}\n\n",
|
||||
serde_json::to_string(&event).unwrap_or_default());
|
||||
|
||||
@@ -479,6 +479,20 @@ export const providerPresets: ProviderPreset[] = [
|
||||
icon: "openrouter",
|
||||
iconColor: "#6566F1",
|
||||
},
|
||||
{
|
||||
name: "ChatCompletions",
|
||||
websiteUrl: "",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
},
|
||||
chat_completions_mode: true,
|
||||
},
|
||||
category: "third_party",
|
||||
icon: "openai",
|
||||
iconColor: "#10A37F",
|
||||
},
|
||||
{
|
||||
name: "Xiaomi MiMo",
|
||||
websiteUrl: "https://platform.xiaomimimo.com",
|
||||
|
||||
Reference in New Issue
Block a user