mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +08:00
feat(claude-code): role-based model mapping with display names and 1M flag
- Replace the four flat env inputs with a Sonnet/Opus/Haiku role table. Each row exposes ANTHROPIC_DEFAULT_*_MODEL plus a new display name field ANTHROPIC_DEFAULT_*_MODEL_NAME, and Sonnet/Opus gain a "Declare 1M" checkbox that toggles the [1M] suffix. - Strip the [1M] context-capability marker before forwarding non-Copilot requests upstream. Copilot keeps its existing [1m]->-1m normalization. - Claude Desktop import now consumes ANTHROPIC_DEFAULT_*_MODEL_NAME as label_override, closing the Claude Code -> Claude Desktop displayName pipeline; add_route's merge logic is shared between hashmap branches. - Unify the [1M] marker as ONE_M_CONTEXT_MARKER across claude_desktop_config and proxy::model_mapper; rename the strip helper to strip_one_m_suffix_for_upstream. - Collapse useModelState's seven duplicated useState initializers and the useEffect parse block into a single parseModelsFromConfig call. - Add tests/hooks/useModelState.test.tsx and a Claude Desktop import test covering Kimi K2 -> label_override. i18n (en/ja/zh) updated.
This commit is contained in:
@@ -803,6 +803,14 @@ impl RequestForwarder {
|
||||
.and_then(|meta| meta.is_full_url)
|
||||
.unwrap_or(false);
|
||||
|
||||
// GitHub Copilot API 使用 /chat/completions(无 /v1 前缀)
|
||||
let is_copilot = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.provider_type.as_deref())
|
||||
== Some("github_copilot")
|
||||
|| base_url.contains("githubcopilot.com");
|
||||
|
||||
// 应用模型映射(独立于格式转换)
|
||||
// Claude Desktop proxy 模式必须先把 Desktop 可见的 claude-* route
|
||||
// 映射成真实上游模型名,并且未知 route 要直接报错,不能使用默认模型兜底。
|
||||
@@ -818,20 +826,14 @@ impl RequestForwarder {
|
||||
// 与 CCH 对齐:请求前不做 thinking 主动改写(仅保留兼容入口)
|
||||
let mut mapped_body = normalize_thinking_type(mapped_body);
|
||||
|
||||
// 确定有效端点
|
||||
// GitHub Copilot API 使用 /chat/completions(无 /v1 前缀)
|
||||
let is_copilot = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.provider_type.as_deref())
|
||||
== Some("github_copilot")
|
||||
|| base_url.contains("githubcopilot.com");
|
||||
|
||||
if is_copilot {
|
||||
mapped_body =
|
||||
super::providers::copilot_model_map::apply_copilot_model_normalization(mapped_body);
|
||||
self.apply_copilot_live_model_resolution(provider, &mut mapped_body)
|
||||
.await;
|
||||
} else {
|
||||
mapped_body =
|
||||
super::model_mapper::strip_one_m_suffix_for_upstream_from_body(mapped_body);
|
||||
}
|
||||
|
||||
// --- Copilot 优化器:分类 + 请求体优化(在格式转换之前执行) ---
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! 在请求转发前,根据 Provider 配置替换请求中的模型名称
|
||||
|
||||
use crate::claude_desktop_config::ONE_M_CONTEXT_MARKER;
|
||||
use crate::provider::Provider;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -112,6 +113,33 @@ pub fn apply_model_mapping(
|
||||
(body, original_model, None)
|
||||
}
|
||||
|
||||
/// Claude Code 通过 `[1M]` 后缀声明 100 万上下文能力;上游 API
|
||||
/// 通常不接受这个本地能力标记,转发前需要剥离。
|
||||
pub fn strip_one_m_suffix_for_upstream(model: &str) -> &str {
|
||||
let trimmed = model.trim_end();
|
||||
let marker = ONE_M_CONTEXT_MARKER.as_bytes();
|
||||
let bytes = trimmed.as_bytes();
|
||||
if bytes.len() >= marker.len()
|
||||
&& bytes[bytes.len() - marker.len()..].eq_ignore_ascii_case(marker)
|
||||
{
|
||||
return trimmed[..trimmed.len() - marker.len()].trim_end();
|
||||
}
|
||||
model
|
||||
}
|
||||
|
||||
pub fn strip_one_m_suffix_for_upstream_from_body(mut body: Value) -> Value {
|
||||
let Some(model) = body.get("model").and_then(Value::as_str) else {
|
||||
return body;
|
||||
};
|
||||
|
||||
let stripped = strip_one_m_suffix_for_upstream(model);
|
||||
if stripped != model {
|
||||
log::debug!("[ModelMapper] 去除本地 1M 标记: {model} → {stripped}");
|
||||
body["model"] = serde_json::json!(stripped);
|
||||
}
|
||||
body
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -251,4 +279,34 @@ mod tests {
|
||||
assert_eq!(result["model"], "sonnet-mapped");
|
||||
assert_eq!(mapped, Some("sonnet-mapped".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_one_m_suffix_before_upstream() {
|
||||
let body = json!({"model": "deepseek-v4-pro[1M]"});
|
||||
let result = strip_one_m_suffix_for_upstream_from_body(body);
|
||||
assert_eq!(result["model"], "deepseek-v4-pro");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_one_m_suffix_after_mapping() {
|
||||
let mut provider = create_provider_with_mapping();
|
||||
provider.settings_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro [1M]"
|
||||
}
|
||||
});
|
||||
|
||||
let body = json!({"model": "claude-sonnet-4-6"});
|
||||
let (mapped, _, _) = apply_model_mapping(body, &provider);
|
||||
let result = strip_one_m_suffix_for_upstream_from_body(mapped);
|
||||
|
||||
assert_eq!(result["model"], "deepseek-v4-pro");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_model_without_one_m_suffix() {
|
||||
let body = json!({"model": "deepseek-v4-pro"});
|
||||
let result = strip_one_m_suffix_for_upstream_from_body(body);
|
||||
assert_eq!(result["model"], "deepseek-v4-pro");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user