本来想带个最新的编译好的 发现不行
各位自己尝试尝试吧
背景
用的是claude desktop + ccswitch路由模式
发现claude desktop有大量的异常调用,特征是每个请求中 max token参数都是1
分析了下数据,比较夸张
同一session 的统计样本
摘要: 在样本 session 中,max_tokens=1 请求占 91.24%,是绝大多数请求;按输入 token 计算则占 7.51%。
数据库中 session:
c78005f4-b8a5-494b-abba-39d0c96d3841
共记录 194 条 /v1/messages 请求:
max_tokens |
请求数 |
请求占比 |
|---|
1 |
177 |
91.24% |
64000 |
16 |
8.25% |
2048 |
1 |
0.52% |
原因
桌面端有个这个东西

实际上我看了下请求的数量,和这个差不多对得上,没仔细数,这个是原因,但是不是全部因为这个不太清楚
我猜问题的原因是这样的
claude desktop 刷新这个信息
然后 ccswitch路由模式并没有透传 count_tokens 相关接口 ,比如 /v1/messages/count_tokens
然后 桌面端fallback到max token =1 的请求,直接读接口返回的token数量
解决方案
像CPA自己已经有这个接口了,实际上透传请求就行了
各 Provider 的 CountTokens
Provider |
是否打上游 |
做法 |
备注 |
|---|
Claude |
是 |
POST {baseURL}/v1/messages/count_tokens?beta=true |
真 Anthropic 计数;有 cache_control/beta/OAuth 处理 |
Gemini / Vertex / AI Studio |
是 |
models/{model}:countTokens |
Google 官方 count |
Antigravity |
是 |
上游 count(带 token/baseURL 回退) |
真请求 |
xAI |
否 |
本地 cl100k 数 body |
不消耗 grok 额度;数值近似 |
Codex |
否 |
本地按模型选 tokenizer 估算 |
不打 ChatGPT 上游 |
OpenAI-compat / Kimi 等 |
视实现 |
多为本地或兼容路径 |
以对应 executor 为准 |
Plugin executor |
视插件 |
CountPluginExecutor |
路由命中插件时走这条 |
/v1/messages/count_tokens = Claude 入口 → 统一选号 → 调各 provider 的 CountTokens;Claude/Gemini 等打上游,xAI/Codex 本地估算。
把问题和猜的原因给claude code 或者 codex 让他基于ccswitch的源码 改下
下面是我基于新版本的patch
Subject: [PATCH] fix
---
Index: pnpm-workspace.yaml
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
--- a/pnpm-workspace.yaml (revision 3d176b98cc0bfd151a42882e88ab59b62083b92f)
+++ b/pnpm-workspace.yaml (revision 34ce8fde0d68f092c3e099ca7e1e1d866060086b)
@@ -6,3 +6,7 @@
ignoredBuiltDependencies:
- msw
+
+allowBuilds:
+ esbuild: true
+ msw: true
Index: src-tauri/src/proxy/forwarder.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs
--- a/src-tauri/src/proxy/forwarder.rs (revision 3d176b98cc0bfd151a42882e88ab59b62083b92f)
+++ b/src-tauri/src/proxy/forwarder.rs (revision 34ce8fde0d68f092c3e099ca7e1e1d866060086b)
@@ -1165,6 +1165,10 @@
mapped_body
};
+ // Token Count 必须始终按 Anthropic `/v1/messages/count_tokens` 透传:
+ // 不能走 openai/gemini 格式转换,也不能被 media prevention 改写 body。
+ let is_count_tokens = is_count_tokens_endpoint(endpoint);
+
// 与 CCH 对齐:请求前不做 thinking 主动改写(仅保留兼容入口)
let mut mapped_body = normalize_thinking_type(mapped_body);
@@ -1328,12 +1332,23 @@
provider,
api_format,
);
- self.apply_media_prevention(&mut mapped_body, provider);
+ // count_tokens 的 body 结构本身就是计数输入,不能做 media 剥离,
+ // 否则 Desktop 看到的上下文占用会系统性偏低。
+ if !is_count_tokens {
+ self.apply_media_prevention(&mut mapped_body, provider);
+ }
}
}
- let needs_transform = match resolved_claude_api_format.as_deref() {
- Some(api_format) => super::providers::claude_api_format_needs_transform(api_format),
- None => adapter.needs_transform(provider),
+ // count_tokens 永远透传:忽略 apiFormat 的 openai/gemini 转换开关。
+ let needs_transform = if is_count_tokens {
+ false
+ } else {
+ match resolved_claude_api_format.as_deref() {
+ Some(api_format) => {
+ super::providers::claude_api_format_needs_transform(api_format)
+ }
+ None => adapter.needs_transform(provider),
+ }
};
// Codex → Anthropic: Claude Code emulation is off by default and only
// enabled when the user explicitly turns it on in the UI, so requests can
@@ -1376,7 +1391,16 @@
let codex_anthropic_base_is_full_endpoint =
codex_responses_to_anthropic && base_url_is_full_endpoint(&base_url, "/v1/messages");
- let url = if matches!(resolved_claude_api_format.as_deref(), Some("gemini_native")) {
+ let url = if is_count_tokens
+ && (is_full_url || codex_anthropic_base_is_full_endpoint)
+ && base_url_is_full_endpoint(&base_url, "/v1/messages")
+ {
+ // full URL 配成了 `.../v1/messages` 时,count_tokens 要改写到
+ // `.../v1/messages/count_tokens`,避免把计数请求打到 messages 端点。
+ rewrite_full_messages_url_to_count_tokens(&base_url, passthrough_query.as_deref())
+ } else if matches!(resolved_claude_api_format.as_deref(), Some("gemini_native"))
+ && !is_count_tokens
+ {
super::gemini_url::resolve_gemini_native_url(
&base_url,
&effective_endpoint,
@@ -2726,6 +2750,31 @@
matches!(path, "/v1/messages" | "/claude/v1/messages")
}
+/// Anthropic Token Count API 路径(含本地 gateway 前缀形态)。
+fn is_count_tokens_endpoint(endpoint: &str) -> bool {
+ let (path, _) = split_endpoint_and_query(endpoint);
+ matches!(
+ path,
+ "/v1/messages/count_tokens"
+ | "/claude/v1/messages/count_tokens"
+ | "/claude-desktop/v1/messages/count_tokens"
+ ) || path.ends_with("/messages/count_tokens")
+}
+
+/// 把 full-url 形态的 `.../v1/messages` 改写为 `.../v1/messages/count_tokens`。
+///
+/// 保留原 URL 上的 query / fragment,并合并额外透传 query。
+fn rewrite_full_messages_url_to_count_tokens(base_url: &str, extra_query: Option<&str>) -> String {
+ let trimmed = base_url.trim();
+ let (path_part, suffix) = match trimmed.find(['?', '#']) {
+ Some(idx) => (&trimmed[..idx], &trimmed[idx..]),
+ None => (trimmed, ""),
+ };
+ let path = path_part.trim_end_matches('/');
+ let rewritten = format!("{path}/count_tokens{suffix}");
+ append_query_to_full_url(&rewritten, extra_query)
+}
+
fn rewrite_codex_responses_endpoint_to_chat(endpoint: &str) -> (String, Option<String>) {
let (_path, query) = split_endpoint_and_query(endpoint);
let passthrough_query = query.map(ToString::to_string);
@@ -3986,6 +4035,42 @@
assert_eq!(passthrough_query.as_deref(), Some("foo=bar"));
}
+ #[test]
+ fn is_count_tokens_endpoint_matches_known_paths() {
+ assert!(is_count_tokens_endpoint("/v1/messages/count_tokens"));
+ assert!(is_count_tokens_endpoint(
+ "/claude-desktop/v1/messages/count_tokens?x=1"
+ ));
+ assert!(is_count_tokens_endpoint("/claude/v1/messages/count_tokens"));
+ assert!(!is_count_tokens_endpoint("/v1/messages"));
+ assert!(!is_count_tokens_endpoint("/v1/messages/count"));
+ }
+
+ #[test]
+ fn rewrite_full_messages_url_to_count_tokens_preserves_query() {
+ assert_eq!(
+ rewrite_full_messages_url_to_count_tokens(
+ "https://relay.example/api/v1/messages",
+ None
+ ),
+ "https://relay.example/api/v1/messages/count_tokens"
+ );
+ assert_eq!(
+ rewrite_full_messages_url_to_count_tokens(
+ "https://relay.example/api/v1/messages?beta=true",
+ Some("x=1")
+ ),
+ "https://relay.example/api/v1/messages/count_tokens?beta=true&x=1"
+ );
+ assert_eq!(
+ rewrite_full_messages_url_to_count_tokens(
+ "https://relay.example/api/v1/messages/",
+ None
+ ),
+ "https://relay.example/api/v1/messages/count_tokens"
+ );
+ }
+
#[test]
fn rewrite_claude_transform_endpoint_strips_beta_for_responses() {
let (endpoint, passthrough_query) = rewrite_claude_transform_endpoint(
Index: src-tauri/src/proxy/handlers.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src-tauri/src/proxy/handlers.rs b/src-tauri/src/proxy/handlers.rs
--- a/src-tauri/src/proxy/handlers.rs (revision 3d176b98cc0bfd151a42882e88ab59b62083b92f)
+++ b/src-tauri/src/proxy/handlers.rs (revision 34ce8fde0d68f092c3e099ca7e1e1d866060086b)
@@ -153,6 +153,105 @@
Ok(Json(response))
}
+/// 处理 `/v1/messages/count_tokens`(Claude API)
+///
+/// 始终透传到上游:只做供应商选择 / 模型映射 / 鉴权,不做 openai/gemini 格式转换。
+/// 上游若不支持该接口,会把上游错误原样返回给客户端。
+pub async fn handle_count_tokens(
+ State(state): State<ProxyState>,
+ request: axum::extract::Request,
+) -> Result<axum::response::Response, ProxyError> {
+ handle_count_tokens_for_app(state, request, AppType::Claude, "Claude", "claude", None).await
+}
+
+/// 处理 Claude Desktop gateway 的 `/v1/messages/count_tokens`
+pub async fn handle_claude_desktop_count_tokens(
+ State(state): State<ProxyState>,
+ request: axum::extract::Request,
+) -> Result<axum::response::Response, ProxyError> {
+ validate_claude_desktop_gateway_auth(&state, request.headers())?;
+ handle_count_tokens_for_app(
+ state,
+ request,
+ AppType::ClaudeDesktop,
+ "Claude Desktop",
+ "claude-desktop",
+ Some("/claude-desktop"),
+ )
+ .await
+}
+
+async fn handle_count_tokens_for_app(
+ state: ProxyState,
+ request: axum::extract::Request,
+ app_type: AppType,
+ tag: &'static str,
+ app_type_str: &'static str,
+ strip_prefix: Option<&'static str>,
+) -> Result<axum::response::Response, ProxyError> {
+ let (parts, body) = request.into_parts();
+ let method = parts.method.clone();
+ let uri = parts.uri;
+ let headers = parts.headers;
+ let extensions = parts.extensions;
+ let body_bytes = body
+ .collect()
+ .await
+ .map_err(|e| ProxyError::Internal(format!("Failed to read request body: {e}")))?
+ .to_bytes();
+ let body: Value = serde_json::from_slice(&body_bytes)
+ .map_err(|e| ProxyError::Internal(format!("Failed to parse request body: {e}")))?;
+
+ let mut ctx =
+ RequestContext::new(&state, &body, &headers, app_type.clone(), tag, app_type_str).await?;
+
+ let raw_endpoint = uri
+ .path_and_query()
+ .map(|path_and_query| path_and_query.as_str())
+ .unwrap_or(uri.path());
+ let endpoint = strip_prefix
+ .and_then(|prefix| raw_endpoint.strip_prefix(prefix))
+ .unwrap_or(raw_endpoint);
+
+ // count_tokens 是非流式探测接口,不参与 messages 的格式转换分支。
+ let forwarder = ctx.create_forwarder(&state);
+ let mut result = match forwarder
+ .forward_with_retry(
+ &app_type,
+ method,
+ endpoint,
+ body,
+ headers,
+ extensions,
+ ctx.get_providers(),
+ )
+ .await
+ {
+ Ok(result) => result,
+ Err(mut err) => {
+ if let Some(provider) = err.provider.take() {
+ ctx.provider = provider;
+ }
+ log_forward_error(&state, &ctx, false, &err.error);
+ return Err(err.error);
+ }
+ };
+
+ let connection_guard = result.connection_guard.take();
+ ctx.outbound_model = result.outbound_model.take();
+ ctx.provider = result.provider;
+
+ // 始终透传响应(不做 Claude openai/gemini transform)
+ process_response(
+ result.response,
+ &ctx,
+ &state,
+ &CLAUDE_PARSER_CONFIG,
+ connection_guard,
+ )
+ .await
+}
+
async fn handle_messages_for_app(
state: ProxyState,
request: axum::extract::Request,
Index: src-tauri/src/proxy/server.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src-tauri/src/proxy/server.rs b/src-tauri/src/proxy/server.rs
--- a/src-tauri/src/proxy/server.rs (revision 3d176b98cc0bfd151a42882e88ab59b62083b92f)
+++ b/src-tauri/src/proxy/server.rs (revision 34ce8fde0d68f092c3e099ca7e1e1d866060086b)
@@ -296,6 +296,15 @@
// Claude API (支持带前缀和不带前缀两种格式)
.route("/v1/messages", post(handlers::handle_messages))
.route("/claude/v1/messages", post(handlers::handle_messages))
+ // Token Count API:始终透传到上游(仅做模型映射 / 鉴权,不做格式转换)
+ .route(
+ "/v1/messages/count_tokens",
+ post(handlers::handle_count_tokens),
+ )
+ .route(
+ "/claude/v1/messages/count_tokens",
+ post(handlers::handle_count_tokens),
+ )
// Claude Desktop 3P 本地 gateway(独立 provider namespace)
.route(
"/claude-desktop/v1/models",
@@ -305,6 +314,10 @@
"/claude-desktop/v1/messages",
post(handlers::handle_claude_desktop_messages),
)
+ .route(
+ "/claude-desktop/v1/messages/count_tokens",
+ post(handlers::handle_claude_desktop_count_tokens),
+ )
// OpenAI Chat Completions API (Codex CLI,支持带前缀和不带前缀)
.route("/chat/completions", post(handlers::handle_chat_completions))
.route(
丢给ai,让他给你直接全套搞定,然后编译之后替换本地的
Issues已经提了 看会不会处理 有内部人员可以帮忙催一催,我提了俩Issues都没啥动静
验证结果
在增加 /v1/messages/count_tokens 直接透传后,Claude Desktop 的相关请求可以正常完成。
脱敏日志如下:
INFO [2026-07-14 23:57:08.669 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":261,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
INFO [2026-07-14 23:57:08.678 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":259,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
INFO [2026-07-14 23:57:08.717 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":293,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
INFO [2026-07-14 23:57:08.731 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":315,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
INFO [2026-07-14 23:57:08.750 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":335,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
INFO [2026-07-14 23:57:08.781 +0800] (<pid>): proxy request started {"service":"sse-inspector","component":"passthrough","requestId":"-","method":"POST","path":"/v1/messages/count_tokens","target":"https://<upstream-host>/v1/messages/count_tokens"}
INFO [2026-07-14 23:57:08.783 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":366,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
INFO [2026-07-14 23:57:08.835 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":54,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
INFO [2026-07-14 23:57:08.895 +0800] (<pid>): proxy request ended {"service":"sse-inspector","component":"passthrough","requestId":"-","status":200,"durationMs":37,"target":"https://<upstream-host>/v1/messages/count_tokens","reason":"upstream_complete"}
直接透传后,未再观察到对应的 max_tokens=1 普通消息降级请求
相关问题
[image]
如题,我心血来潮用用当时申请的mimo,用的是claude code desktop
然后发现正式请求完以后有大量的非流式请求
请求体类似:
[image]
看到了这个帖子,但是依然不知道为什么,
haiku模型我觉得没问题,但是为什么会限制max tokens为1?这不是什么都请求不到吗
昨天刷到好几个claude desktop的教程,按照佬友的教程一步步配置可以用了,先感谢下各位佬。放一个最详细的链接
教程:【教程】如何用功能更强大更美观的claude桌面端替代cli还能随意切换模型? - 搞七捻三 - LINUX DO
同时也发现个问题,在code中发一句"你好"发了44次请求!我接的是京东的coding plan,这样太容易干到限额了!附ccs的log
[image…
A/ 又在干嘛?cc 产生巨量输出 tokens 为 1 的 API 调用 - 搞七捻三 - LINUX DO
CLI的解决方案在这里,但是我比较熟悉GUI的使用。之前是用codex的。
不知道有没有佬友知道可以怎么办。
邪修搞定 Claude Desktop App 单独配置 HTTP/Socks5 网络代理 - 开发调优 / 开发调优, Lv1 - LINUX DO
搜到这个,让gem…