From 084857ce2569b442f1db27c91689e84befe6aa78 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 2 Jun 2026 23:51:27 +0800 Subject: [PATCH] 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 --- src-tauri/src/claude_desktop_config.rs | 73 +++++++++++++++++++++----- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/claude_desktop_config.rs b/src-tauri/src/claude_desktop_config.rs index 5595e9416..de3d0d333 100644 --- a/src-tauri/src/claude_desktop_config.rs +++ b/src-tauri/src/claude_desktop_config.rs @@ -664,7 +664,7 @@ pub fn model_list_response(provider: &Provider) -> Result { } pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result { - 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 Result Result Result &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 { 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]