Skip to content

fix(providers): 「模拟 CLI」切换身份整套替换指纹头,并按官方 CLI 源码对齐头集 - #737

Merged
xiaozhou26 merged 1 commit into
Stack-Cairn:mainfrom
xiaozhou26:feat/mimic-cli-identity-align-official
Sep 3, 2026
Merged

fix(providers): 「模拟 CLI」切换身份整套替换指纹头,并按官方 CLI 源码对齐头集#737
xiaozhou26 merged 1 commit into
Stack-Cairn:mainfrom
xiaozhou26:feat/mimic-cli-identity-align-official

Conversation

@xiaozhou26

@xiaozhou26 xiaozhou26 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Closes #736

Summary

「模拟 CLI」切换身份原本走 mergeImportedCustomHeaders 的"导入"语义——同名覆盖、其余追加、从不删除。三家 CLI 除 User-Agent 外没有同名头,于是 Claude Code → Codex 只换掉 UA,x-app / anthropic-version / X-Stainless-* 等 11 条原样残留,最终发出的是哪家都不像的混合指纹(详见 #736)。

本 PR 做两件事:

  1. 切换即整套替换。 customHeaders.ts 新增 CLI_IDENTITY_HEADER_FAMILIES(每家 CLI 的静态身份头 + 每会话动态头名)与 applyCliIdentity():先剥掉其它 CLI 家族名下的全部头,再并入所选 CLI 的整套静态头。所选 CLI 自家的动态头(如 Codex 的 session-id)保留用户手填值;不属于任何 CLI 家族的业务头(AuthorizationX-Api-Key……)原样不动。摘要行新增"移除 N",让用户看见被剥掉了多少。
  2. 头集按官方 CLI 源码对齐。 之前 Codex / Grok 只写一条 UA,Claude Code 的值来自第三方代理的档案。现在逐字复刻官方客户端发往模型 API 的固定头:
    • Claude Code:claude-code-source 2.1.88(内联 @anthropic-ai/sdk 0.74.0);X-Stainless-Retry-Count 改用 SDK 实际发出的 Pascal-Case。
    • Codex:补 originator: codex_cli_rsversioncodex-rs default_client.rs / model-provider-info),version 与 UA 同源。
    • Grok:补 x-grok-client-identifier / x-grok-client-version / x-grok-client-mode 及 cli-chat-proxy 标记头(xai-grok-http / xai-grok-shell)。
    • 每会话随机的动态头(Claude 的 X-Claude-Code-Session-Id / x-client-request-id、Codex 的 session-id / thread-id、Grok 的 x-grok-conv-id 等)不写死;requestOptions.ts 在有 sessionId 时按官方规则填 Claude / Codex 的两组。

发请求那侧仍不含任何内置伪装:这些值只在用户点按钮时写进自定义请求头。

Change scope

  • Modules: agent-ui / agent-gui / src-tauri
  • Key paths:
    • crates/agent-ui/src/lib/providers/customHeaders.tsCLI_IDENTITY_HEADER_FAMILIESapplyCliIdentity()listCliIdentityProviderIds(),三家头集与版本常量
    • crates/agent-ui/src/pages/settings/ProviderModal.tsx / ProviderModalView.tsx — 切换走 applyCliIdentity,摘要"移除 N",下拉按当前供应商排序并标"匹配当前",菜单加宽避免 UA 换行
    • crates/agent-ui/src/i18n/translations/{zhCN,enUS}Settings.ts — 2 个新文案键
    • crates/agent-gui/src/lib/providers/runtime/requestOptions.ts — Claude / Codex 每会话头
    • crates/agent-gui/src-tauri/src/services/provider_models.rs — 模型列表链路 never-forge 用例补入新增身份头名
    • crates/agent-gui/test/providers/{custom-headers,request-options,transport-golden}.test.mjstest/settings/provider-models-fetch.test.mjs

Screenshots / preview

下拉按当前供应商排序并标"匹配当前"(Anthropic 供应商):

Mimic CLI menu

选 Claude Code:写入 12 条(已导入 12; 覆盖 0):

After applying Claude Code

再切到 Codex:只剩 Codex 的 3 条,摘要 已导入 3; 覆盖 1; 移除 11(修复前这里仍是 12 条、只换了 UA):

After switching to Codex

同一流程继续切到 Grok 的运行时输出(headless Chrome 驱动 npm run dev,从 React 状态直接读取):

--- after switching to Codex (3 headers) summary="已导入 3; 覆盖 1; 移除 11"
    User-Agent: codex_cli_rs/0.151.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal
    originator: codex_cli_rs
    version: 0.151.0
--- after switching to Grok (6 headers) summary="已导入 6; 覆盖 1; 移除 2"
    User-Agent: grok-shell/1.0.6 (linux; x86_64)
    x-grok-client-identifier: grok-shell
    x-grok-client-version: 1.0.6
    x-grok-client-mode: interactive
    X-XAI-Token-Auth: xai-grok-cli
    x-authenticateresponse: authenticate-response

Verification

cd crates/agent-gui && node --test test/providers/custom-headers.test.mjs test/providers/request-options.test.mjs \
  test/providers/transport-golden.test.mjs test/settings/provider-models-fetch.test.mjs   # 95 pass / 0 fail
cd crates/agent-gui && npx tsc --noEmit -p tsconfig.json
cd crates/agent-ui && npm run typecheck
cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml --lib provider_model_headers

新增 / 更新的用例:

  • custom-headers.test.mjs(+9):Claude → Codex 整套替换且业务头保留;离开 Grok 时剥掉手填的 x-grok-* 动态头;重选同一家保留自家手填动态头;x-client-request-id 在 Claude / Codex / Grok 之间的归属;三家 buildCliIdentityHeaders 的头集与 UA/version 同源;listCliIdentityProviderIds 排序;全部身份头合法、不在保留集、合并无重复。
  • request-options.test.mjs:Claude / Codex 链路带 sessionId 时的每会话头;Codex 自动生成 session 时 session-id / thread-id / x-client-request-idsession_id 同值;三家预设键名含新增头。
  • transport-golden.test.mjs:golden 快照随新增头更新。
  • provider_models.rsprovider_model_headers_never_forge_a_client_identity 的禁止列表补入新增身份头名,锁定模型列表链路不自动注入。

Pre-submit checklist

  • A requirement issue is linked (or this is a trivial fix that needs no issue, as explained in the summary).
  • Synced with the target branch; no merge conflicts.
  • The change is focused, with no unrelated modifications.
  • No secrets, tokens, or personal data included.
  • Docs are updated for changes affecting user behavior, deployment, or configuration. (无用户文档描述此按钮的头清单;文案变更已同步 zh-CN / en-US 两份翻译。)

切换 CLI 身份原本走 mergeImportedCustomHeaders 的"导入"语义:同名覆盖、其余追加、
从不删除。三家 CLI 除 User-Agent 外没有同名头,于是 Claude Code → Codex 只换掉
UA,x-app / anthropic-version / X-Stainless-* 等 11 条原样残留,拼出哪家都不像的混合
指纹。

- customHeaders.ts:新增 CLI_IDENTITY_HEADER_FAMILIES(每家静态身份头 + 每会话动态
  头名)与 applyCliIdentity():先剥掉其它 CLI 家族名下的全部头,再并入所选 CLI 的整套
  静态头;所选 CLI 自家的动态头保留用户手填值,非 CLI 业务头不动;结果带 removedCount。
- 按官方源码对齐一键写入的头集:Claude Code 复刻 claude-code-source 2.1.88(内联 SDK
  0.74.0,X-Stainless-Retry-Count 用 SDK 的 Pascal-Case);Codex 补 originator/version
  (与 UA 同版本);Grok 补 x-grok-client-identifier/version/mode 与 cli-chat-proxy 标记
  头。UA 版本号与随附 version 头统一取自 *_CLI_VERSION 常量。
- requestOptions.ts:Claude 链路有 sessionId 时补 X-Claude-Code-Session-Id +
  x-client-request-id;Codex 链路补 session-id / thread-id / x-client-request-id。
- ProviderModal(View):切换身份走 applyCliIdentity,摘要行新增"移除 N";下拉按当前
  供应商排序并标"匹配当前",菜单加宽避免 UA 换行。
- provider_models.rs:模型列表链路 never-forge 用例补入新增身份头名。
- 测试:新增 Claude→Codex 整套替换、离开 Grok 剥 x-grok-*、重选同家保留自家动态
  头、x-client-request-id 跨家归属 4 个用例;更新 golden 与 request-options 用例。

Closes Stack-Cairn#736
@xiaozhou26
xiaozhou26 force-pushed the feat/mimic-cli-identity-align-official branch from 26ff30f to 4e1bd4a Compare September 2, 2026 13:37
@xiaozhou26
xiaozhou26 merged commit 97c1c14 into Stack-Cairn:main Sep 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 「模拟 CLI」切换身份只覆盖 User-Agent,上一家 CLI 的指纹头全部残留

1 participant