mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
fix(claude-desktop): strip [1m] suffix before proxy route lookup
Claude Desktop appends a local [1m] marker to the model name when the 1M-context beta is active (e.g. claude-opus-4-8[1m]). The proxy route matcher compared this raw name against clean route IDs, so every match tier failed and the is_claude_safe_model_id guard also blocked the role keyword fallback, surfacing as route_unknown (HTTP 400) when switching to a 1M-capable model mid-conversation. Strip the [1m] suffix inside map_proxy_request_model before lookup so exact/alias/legacy/role matching all see the clean ID, while keeping the original name in the route_unknown error for diagnostics. The upstream request still carries the mapped real model; 1M capability is negotiated via the anthropic-beta header, not the model-name suffix. Fixes #3588
This commit is contained in:
@@ -664,7 +664,7 @@ pub fn model_list_response(provider: &Provider) -> Result<Value, AppError> {
|
||||
}
|
||||
|
||||
pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result<Value, AppError> {
|
||||
let requested = body
|
||||
let requested_raw = body
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
@@ -677,6 +677,7 @@ pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result<V
|
||||
"Claude Desktop request is missing the model field",
|
||||
)
|
||||
})?;
|
||||
let requested = strip_one_m_suffix_for_route_lookup(&requested_raw);
|
||||
|
||||
let routes = proxy_model_routes(provider)?;
|
||||
let upstream_model = routes
|
||||
@@ -685,20 +686,21 @@ pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result<V
|
||||
.or_else(|| {
|
||||
routes
|
||||
.iter()
|
||||
.find(|r| is_compatible_opus_route_alias(&r.route_id, &requested))
|
||||
.find(|r| is_compatible_opus_route_alias(&r.route_id, requested))
|
||||
})
|
||||
.map(|route| route.upstream_model.clone())
|
||||
.or_else(|| legacy_raw_route_upstream_model(provider, &requested))
|
||||
.or_else(|| legacy_raw_route_upstream_model(provider, requested))
|
||||
.or_else(|| {
|
||||
// 角色关键词回落:Claude Desktop 的部分调用(如子 agent)会请求带发布
|
||||
// 日期后缀的完整官方名(claude-haiku-4-5-20251001),与 manifest 暴露的
|
||||
// 简短 route_id(claude-haiku-4-5)不精确相等。按 opus/haiku/sonnet 归类
|
||||
// 到同档已配置路由,对齐 Claude Code model_mapper 的宽松匹配。
|
||||
// 仅对 Claude Desktop 认可的安全模型名回落(排除 [1m] 标记等非法形式)。
|
||||
if !is_claude_safe_model_id(&requested) {
|
||||
// 匹配前已剥离本地 [1m] 标记;这里仍只对 Claude Desktop 认可的
|
||||
// 安全模型名回落,避免非 Claude route 被误映射。
|
||||
if !is_claude_safe_model_id(requested) {
|
||||
return None;
|
||||
}
|
||||
let role = claude_role_keyword(&requested)?;
|
||||
let role = claude_role_keyword(requested)?;
|
||||
routes
|
||||
.iter()
|
||||
.find(|route| claude_role_keyword(&route.route_id) == Some(role))
|
||||
@@ -707,8 +709,8 @@ pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result<V
|
||||
.ok_or_else(|| {
|
||||
AppError::localized(
|
||||
"claude_desktop.provider.route_unknown",
|
||||
format!("Claude Desktop 模型路由未配置: {requested}"),
|
||||
format!("Claude Desktop model route is not configured: {requested}"),
|
||||
format!("Claude Desktop 模型路由未配置: {requested_raw}"),
|
||||
format!("Claude Desktop model route is not configured: {requested_raw}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -719,6 +721,18 @@ pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result<V
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
fn strip_one_m_suffix_for_route_lookup(model: &str) -> &str {
|
||||
let trimmed = model.trim();
|
||||
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();
|
||||
}
|
||||
trimmed
|
||||
}
|
||||
|
||||
fn legacy_raw_route_upstream_model(provider: &Provider, requested: &str) -> Option<String> {
|
||||
provider
|
||||
.meta
|
||||
@@ -1820,15 +1834,48 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_desktop_proxy_rejects_1m_suffix_route() {
|
||||
let provider = proxy_provider("proxy");
|
||||
fn claude_desktop_proxy_strips_1m_suffix_before_route_lookup() {
|
||||
let mut provider = proxy_provider("proxy");
|
||||
provider
|
||||
.meta
|
||||
.as_mut()
|
||||
.expect("meta")
|
||||
.claude_desktop_model_routes = std::collections::HashMap::from([
|
||||
(
|
||||
"claude-sonnet-4-6".to_string(),
|
||||
ClaudeDesktopModelRoute {
|
||||
model: "upstream-sonnet".to_string(),
|
||||
label_override: None,
|
||||
supports_1m: Some(true),
|
||||
},
|
||||
),
|
||||
(
|
||||
"claude-opus-4-8".to_string(),
|
||||
ClaudeDesktopModelRoute {
|
||||
model: "upstream-opus".to_string(),
|
||||
label_override: None,
|
||||
supports_1m: Some(true),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
let err = map_proxy_request_model(
|
||||
let mapped = map_proxy_request_model(
|
||||
json!({"model": "claude-opus-4-8[1m]", "messages": []}),
|
||||
&provider,
|
||||
)
|
||||
.expect("compact 1M suffix should map to Opus route");
|
||||
assert_eq!(mapped["model"], json!("upstream-opus"));
|
||||
|
||||
let mapped = map_proxy_request_model(
|
||||
json!({"model": "claude-sonnet-4-6 [1M]", "messages": []}),
|
||||
&provider,
|
||||
)
|
||||
.expect_err("1M suffix route should not be accepted");
|
||||
assert!(err.to_string().contains("claude-sonnet-4-6 [1M]"));
|
||||
.expect("spaced uppercase 1M suffix should map to Sonnet route");
|
||||
assert_eq!(mapped["model"], json!("upstream-sonnet"));
|
||||
|
||||
let err = map_proxy_request_model(json!({"model": "gpt-5[1m]", "messages": []}), &provider)
|
||||
.expect_err("non-Claude route should still fail after stripping 1M suffix");
|
||||
assert!(err.to_string().contains("gpt-5[1m]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user