• codexvn

    本来想带个最新的编译好的 发现不行

    各位自己尝试尝试吧

    背景

    用的是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&quot;,
    + None
    + ),
    + "https://relay.example/api/v1/messages/count_tokens&quot;
    + );
    + assert_eq!(
    + rewrite_full_messages_url_to_count_tokens(
    + "https://relay.example/api/v1/messages?beta=true&quot;,
    + Some("x=1")
    + ),
    + "https://relay.example/api/v1/messages/count_tokens?beta=true&x=1&quot;
    + );
    + assert_eq!(
    + rewrite_full_messages_url_to_count_tokens(
    + "https://relay.example/api/v1/messages/&quot;,
    + None
    + ),
    + "https://relay.example/api/v1/messages/count_tokens&quot;
    + );
    + }
    +
    #[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…

    主楼
  • Transformer

    我去,我让ai改了一天,没找出毛病。看了你这个算是解决了。居然没人提这个问题,估计都用的cli,没这个问题。

    #1
  • newbee-linuxdo

    看到确实还没解决啊,这是ccswitch的问题对吧

    #2
  • 西西

    佬,你仓库里的那个是编译好的吗,我能直接拉取下来用吗

    #3

发表回复

登录后回复