# x-adapter 技术设计

## 范围

x-adapter 只包含 Trae 与豆包工作两个 provider。包名为 `@mlzwl/x-adapter`，源码目录为 `apps/x-adapter`，CLI、Pi provider 和 service 名为 `x-adapter`。

Trae 对外仍是一个 provider；`global` 与 `cn` 是其内部独立 realm。本实现不包含 Web SOLO Remote 的 `chat_sessions/events`、账号池、账号轮换、签到、积分、账单、usage、本地搜索或猜测性协议 fallback；只提供显式、按 realm 的官方客户端凭据只读导入。

## 架构

```text
client
  └─ loopback HTTP gateway
       ├─ protocol normalization
       │    ├─ Chat Completions
       │    ├─ Responses
       │    └─ Anthropic Messages
       ├─ model registry
       │    ├─ Trae adapter
       │    │    ├─ Global realm
       │    │    └─ CN realm
       │    └─ Doubao Work adapter
       ├─ Responses history
       └─ lifecycle / service / logging
```

`ModelRegistry` 并行发现 provider，Trae adapter 再并行发现两个 realm。失败被限制在对应边界，不阻塞另一个 realm 或豆包工作。公开模型 ID 为 `trae-global-*`、`trae-cn-*` 或 `doubao-work-*`，registry 只接受精确公开 ID。

### Trae Standard 与 Max 后端

Trae 目录若同时声明 `display_config.max_mode=true`、`context_window_tokens.max` 和 `model_detail_list` 中的 `__max` 后端，adapter 把一条上游配置展开为两个可精确路由的模型：

1. Standard 保留原 provider-local ID（`<realm>-<config_name>`），使用 `__dev` detail。
2. Max 增加 `-max` 后缀（`<realm>-<config_name>-max`），使用 `__max` detail。

每个变体保存自己的 `model_name`、`prompt_max_tokens`（输入上限）、`max_tokens`（输出上限）和 `context_window_tokens`（总上下文）。代表值：DeepSeek-V4.1-Flash Standard 为 168K 输入 + 32K 输出 = 200K 总上下文；Max 为 936K 输入 + 64K 输出 = 1M 总上下文。

Max 是后端变体，不等于 reasoning effort。两个变体分别沿用上游声明的推理强度列表；detail 级 `model_extra_config.reasoning_effort_options` 只在 entry 级 `supported_reasoning_levels` 缺失时补齐。

缺少任一 Max 合同信号时只暴露 Standard，避免猜测不可用路由。若某条配置只有 `__max` detail，该条自身就投影为 Max 变体。

Pi 用总上下文做 context meter 和 compaction，用 `max_output_tokens` 作为请求输出上限；缺失时回退包内默认值。

## 配置、凭据与产品身份

统一数据根目录是 `~/.x-adapter`，可由 `X_ADAPTER_DATA_DIR` 覆盖。runtime、logs、cache、统一配置以及 provider 子目录都从该根目录派生。

Trae 凭据按 realm 存储在 `trae/global/auth.json` 与 `trae/cn/auth.json`，包含 OAuth token、账号非敏感标识、区域、产品元数据、machine/device ID、P-256 密钥和凭据来源。写入使用 `0700` 目录、`0600` 文件/临时文件、file fsync、原子 rename 和 best-effort directory fsync。refresh token 具有轮换语义，refresh 按 realm single-flight。

产品身份优先级：

1. realm 专属 `X_ADAPTER_TRAE_*` 显式配置；
2. 已安装官方客户端的公开 `product.json`；
3. 包内保守 fallback。

常规配置解析只访问官方 `product.json` 的 `appVersion`、`tronBuildVersion`、`traeVersionCode`、`quality` 和 `iCubeApp.authConfig.TRAE`。只有显式 `trae import` 才会只读打开目标 realm 的 `storage.json`。

## Trae 官方凭据导入

导入器支持官方客户端当前使用的两种 AES-128-CBC storage 包装头。密钥材料由每条记录的 32 字节随机量和对应固定 salt 经两轮 SHA-512 派生；解密后必须校验前置 SHA-512 完整性摘要。导入只投影以下字段：access/refresh token、到期时间、用户非敏感标识、allowlisted account host、`telemetry.machineId`、唯一 `iCubeAuthInfo://icube-dc:<deviceId>` 及其中的 P-256 公私钥。

导入在写入前验证：文件大小与读取期间元数据稳定、文档结构、realm 信号、auth host allowlist、唯一设备条目、私钥/公钥均为 P-256 且相互匹配。它从不写回官方 storage；目标 `auth.json` 已存在时默认拒绝覆盖。导入后的两端共享 token family 和服务端设备身份，因此不假设 refresh 可并发或可在错误设备间重绑；x-adapter refresh 只写自己的副本，另一端可能因 token 轮换变旧。

## Trae OAuth

每次登录生成随机 state/trace、PKCE verifier/challenge 和 P-256 设备密钥；callback server 只绑定随机 `127.0.0.1` 端口。官方 Global callback 当前回传并严格匹配随机 `loginTraceID`，但不回传授权 URL 中的 state；因此 trace 是实际会话绑定，state 仅在 callback 主动携带时要求精确匹配。只有精确 `/authorize` 路径、trace、可选 state 和授权内容均有效的 callback 才会被一次性消费；无效探测不能终止合法登录，超时和完成后都会关闭 server。

LoginGuidance、Web、登录、account API 与数据 host 分开 allowlist。所有 realm 的 LoginGuidance 都 fail closed；来自响应或 callback 的 host 必须重新校验，不能把用户可控内容直接作为 token 或数据请求目标。callback `host` 仅作为登录服务和区域信号，不会成为 token exchange 或模型/聊天目标。Global 根据可靠 region/host 信号固定 SG/US account API 与数据面；网络错误不会触发跨区域探测。CN 固定 CN account API 与数据面；其模型/聊天 host 为公开参考实现交叉出现但非 Trae 品牌域名的 `trae-api-cn.mchost.guru`，仅因精确 allowlist 才可访问，尚未由真实账号验证。

OAuth 登录复用已导入的设备身份（存在有效 `auth.json` 时），避免每次登录都在账号设备数量上限（`20401`）下注册新设备；无可用设备身份时退回新生成设备。

授权码使用 realm/region 固定 account API 上的 `/trae/api/v3/oauth/ExchangeToken` 与 PKCE 交换，并提交 P-256 设备公钥。refresh 使用同一路径、原设备身份和 ECDSA P-256 `DeviceProof`；签名原文由 method、path、client ID、refresh token、Unix 秒和随机 nonce 构成。首次换证返回 `20401` 时明确报告账号设备数量上限，不把它误判为 PKCE、host 或普通过期错误。401/403 只允许在聊天响应开始前强制 refresh 并重放一次。

## 模型发现与缓存

各 realm 优先请求 `/api/ide/v1/batch_get_detail_param`；无有效 batch 结果时，分别查询：

- `chat_v3`
- `solo_work_lite`
- `solo_agent`
- `solo_agent_lite`

单 function 失败只舍弃该 function；全部失败且无有效缓存时 realm 不可用。模型元数据保存 realm、config name、model name、实际发现的 candidate functions，以及目录 `display_config` 给出的图片输入/工具图片能力，不使用静态目录伪造账号权益。

`display_config.multimodal` 是目录提示而不是绝对契约。网关以它作为默认值，并只为已有实测证据的例外维护最小覆盖；字段缺失时 fail closed 为纯文本。统一发现接口分别输出 OpenAI `metadata.input_modalities`、Codex `input_modalities` / `supports_image_detail_original` 和 Claude `capabilities.image_input.supported`。Pi extension 读取该结论；三种请求协议都会在 adapter 执行前拒绝发给纯文本模型的图片，Responses 在合并 replay history 后再次校验，错误为 HTTP 400 `unsupported_multimodal_content`。豆包工作始终声明为纯文本消息内容；工具调用与工具结果走独立的 Connector 通道，不改变内容模态。

缓存身份由 realm、账号 UID、数据 host、产品版本和 function 集合派生，不含 access token；token 轮换不会制造新缓存。内存和磁盘缓存均按 realm 隔离。

### Web SOLO remote 目录（仅 Global）

Global realm 在 IDE 发现之后叠加 Web SOLO remote 双源：

- 权威源：`GET https://core-normal.trae.ai/api/remote/v1/models`（`X-Trae-Client-Type: web` + `work.trae.ai` Origin/Referer，`Cloud-IDE-JWT` 授权），解析 `data.list[].models[].name`。
- 补充源：本机官方 Trae 客户端的 `state.vscdb`（`node:sqlite` 只读）中 `solo_agent` preset 目录，携带 `context_window_tokens` 元数据；`gpt-6-sol`、`kimi-k3` 等可调用但不在 `/models` 列表的模型由此补充。
- 两个源均为 best effort：失败时退化为纯 IDE 目录，不隐藏 IDE 模型；不可用模型调用时直接透传上游业务错误。
- 同名合并：SOLO 名与 IDE `config_name` 冲突时，在现有条目的 candidate functions 前置 `solo_agent_remote`，路由优先 SOLO remote，mismatch 时回退 IDE 链路；新名字则生成仅含 `solo_agent_remote` 的条目。`metadata.solo_source` 标注来源（`solo_remote`/`solo_preset`）。

## 聊天协议与 fallback 边界

Trae 数据面使用 `/api/agent/v3/llm_utils_chat` 与 `Cloud-IDE-JWT`。请求以目录绑定的 config/model/function 构造；普通 function tools 继续经过 canonical schema 和稳定名称映射。SSE 映射 reasoning、text、tool-call、usage、progress 和 done；没有显式终态的 EOF 视为失败。

聊天 POST 不使用通用自动重试。仅在以下全部成立时允许尝试下一个 function：

- 错误发生在首个可见 SSE 事件前；
- 上游给出明确 function/agent/mode mismatch 信号；
- 下一项属于同一 realm、同一模型实时发现的 candidate functions。

配额、权益、429、普通 HTTP 错误、timeout、取消或已开始的 SSE 不得触发 function、模型或 realm fallback。Trae hosted search 明确返回 `unsupported_hosted_search`；不接入通用本地搜索执行链。

真实诊断证明 `llm_utils_chat` 不可靠地执行请求级 `max_tokens`。网关因此使用两个独立的本地输出边界：请求 `max_tokens` 限制可见的 text/reasoning/tool-call 增量事件数，`X_ADAPTER_TRAE_CHAT_MAX_OUTPUT_EVENTS` 可进一步收紧；`X_ADAPTER_TRAE_CHAT_MAX_OUTPUT_BYTES` 默认将这些事件的文本或序列化 tool-call 总量限制在 1 MiB。达到上限后取消上游流并合成唯一 `done(length)`，流式和非流式调用都通过同一 canonical generator 获得一致结果；协议出口再分别映射为 Chat Completions `length`、Responses `incomplete/max_output_tokens` 和 Anthropic `max_tokens`。该边界只负责限制本机暴露和聚合的数据量，不把 SSE 事件或 UTF-8 字节伪装成 token，也不承诺取消前的上游计费精度。

模型发现三方言都投影总上下文：OpenAI `metadata.context_window_tokens`，Codex `context_window` / `max_context_window` 与 `max_output_tokens`，Claude `max_input_tokens` / `max_tokens`。Provider 的 `prompt_max_tokens` 仍表示输入上限，不与总上下文混用。

当前官方包的静态客户端投影明确包含 `/api/ide/v2/llm_raw_chat`，其请求 schema 也并非普通 OpenAI body；它包含 config/model/session/conversation、request/mode/config/access type、reasoning、provider context、biz context 与 extra info 等字段。经授权的 Global 单次受控 probe 使用当前目录模型和该字段族，仍返回 HTTP 400 空体、无 SSE；未 refresh、未探测 v1、未 fallback。结合官方内部 AhaNet/native transport 证据，x-adapter 不把 raw-chat 接入正式聊天链路。

### SOLO work 聊天链路（仅 Global，solo_work_lite）

`solo_agent_remote` function 的模型走 `llm_utils_chat` 的 SOLO work 池，与 IDE 池共用端点但身份、头部和 body 完全不同：

1. **凭据**：必须使用 SOLO 身份 token（OAuth client `en1oxy7wnw8j9n`，存于 `<dataDir>/<realm>/solo-token.json`）。IDE 身份 token 在该池被拒（4120 `CAN_NOT_USE_SOLO_MODE`）；SOLO refresh token 必须配 SOLO client id 刷新（10101 绑定校验）。`auth/solo-token.ts` 负责加载与自动刷新（过期前 5 分钟），刷新走 `growsg-normal.trae.ai` 的 Cloud-IDE `ExchangeToken`。
2. **伪装**：请求头完整伪装 TRAE SOLO 桌面客户端（`x-device-brand: 83DG`、`x-os-version: Windows 11 Pro`、`x-ide-version: 3.5.51`、`x-app-version: default`、W3C trace 头）。body 只携带 `messages/function/stream/config_name/model`——多余字段（如 `agent_type`/`device_id`/`ide_version`）会能 4023 “model is unknown”。
3. **工具调用**：该池无原生工具透传，采用提示词模拟——工具目录注入为 `<toolcall_protocol>` system 指令，模型输出的 `<toolcall>{"name","params"}</toolcall>` 块由流式解析器转回 canonical tool-call 事件（相邻重复块丢弃，上游会重放；未闭合/坏块降级为文本，不中断流）。工具结果以 `<tool_result>` 文本回喉，多轮对话天然支持。`required`/指定 `tool_choice` 仍拒绝。协议格式与实测遵从性见 verification.md（参考 wangqi233/trae2api 的实现证据）。
4. **SSE 映射**：`output.response` 映射 text、`output.reasoning_content` 映射 reasoning、`token_usage` 映射 usage、`done` 映射终态；有解析出的 tool-call 时终态强制为 `tool_calls`。解析出 tool-call 时 `finish_reason=tool_calls`，否则 `stop`。
5. **回退**：该链路独立于 IDE function 循环，业务码不触发跨池回退；HTTP 401/403 复用共享强制 refresh 重试一次。超时与输出边界与 IDE 链路共用 watchdog 配置族。

前置的 chat_sessions 会话式链路（`solo_agent_remote` 自主 agent）已从生产路由移除：它是服务端编排 agent，无工具注入通道（前端 buildSendMessageRequest 实锤无 tools 字段），且实测无视注入的工具协议（用自己的内置工具或直接回答）；代码与测试保留在 git 历史中。

## Provider 隔离

- Trae Global/CN 分别读取自己的凭据、缓存、账号身份和 route metadata。
- 豆包 adapter 只读取目标域 Cookie 和 common query，使用自己的目录、模型目录、会话队列与 SSE parser。
- 本机客户端 Authorization 仅由 gateway 鉴权 middleware 消费，不得转发到 provider。
- 不允许跨 realm、跨模型或跨 provider fallback。

### 豆包工作工具桥接

豆包工作的客户端工具桥接是实验能力，依赖私有接口，不是公开或受支持的 API。实现分四层：

1. `prepareDoubaoRequest` 保留真实客户端工具与工具历史。只有 `tool_choice: none` 才移除工具目录；强制选择与显式选择在 adapter 执行前校验声明一致性。
2. `createDoubaoToolRuntime` 为每个轮次生成 sandbox、sandbox env 与 client env ID，并通过 `ensureDoubaoBridge` 查找或创建名为 `X Adapter Bridge` 的个人 Connector。`ensureDoubaoBridge` 只在显式 `--create-bridge` 确认时创建；普通请求缺少 Connector 时以 `missing_doubao_bridge` 失败。
3. `startCompletion` 在存在运行时把 `runtime_type` 设为 2，并发送 `client_option`、`agent_task_param` 与 `task_input_json`（含 AGENTS 内容与 `localConnectors[].mcpToolsList`）。
4. `doubaoToolStream` 通过 HTTP/2 长连接订阅 `/alice/office/tool_local/chunk_stream`。`runToolListener` 按 sandbox ID 与 conversation ID 过滤调用，`routeDoubaoToolCall` 只接受 `connector.call` 与少量已映射的内置工具名，并按客户端 JSON Schema 校验参数。

参数校验使用 Ajv 2020 严格模式编译，支持 `$ref`、`const`、`pattern`、数值与数组边界、`format` 等完整约束。未知关键字、无效 schema 或 `$async` schema 在编译阶段以 `invalid_input` 失败，不会触发异步校验。校验函数按 schema 对象缓存。

工具轮次的状态机只保存在当前 daemon 内存中：

- 远端调用转为 canonical `tool_call_delta` 后，当前 HTTP 响应以 `finish_reason=tool_calls` 结束。
- 客户端下一次请求必须携带匹配 `call_id` 的 tool result。`currentTurnToolResults` 只取末尾连续的 tool 消息，已完成的历史结果被忽略。
- 结果上传到 `/alice/office/tool_local/upload_tool_call_result` 后继续消费原 completion 流。
- 只支持串行调用。若豆包在上一调用完成前再发一个调用，网关直接向豆包上传“不支持并行”的错误结果，不把它暴露给客户端。
- 工具长时间运行时先返回“仍在运行”，再通过桥接等待工具恢复。`X_ADAPTER_DOUBAO_TOOL_WAIT_MS` 控制等待时限，`X_ADAPTER_DOUBAO_TOOL_LIMIT_MS` 是从首个 pending 调用开始计算的绝对上限。
- 绝对上限到期时，网关中止上游流与工具监听，删除活动轮次，并让迟到结果以 `doubao_tool_turn_expired` 或 `previous_response_unavailable` 失败。

`local_runtime` 元数据声明 `available: true`、`executor: client`、`authorization: private_connector_bridge`、`reason: experimental_private_api`。`dispatch`、`resultUpload` 与 `recovery` 为 true；`cancellation` 与 `signedEnvelope` 为 false。该声明表示代码路径存在，不表示豆包官方支持。

## Canonical 与 HTTP 安全

Chat Completions、Responses 与 Anthropic Messages 转换为 canonical request，由选定 adapter 执行，再投影回调用方协议。

用量投影遵循各协议的原生字段名和计数约定。Trae IDE 聊天链路的上游 `token_usage` 事件按当前实现假设提供 `cache_read_input_tokens` 与 `cache_creation_input_tokens`，adapter 把这两个值写入 canonical `Usage`。该字段形状尚无本仓真实上游探针证据，目前只有离线构造 payload 覆盖协议投影与钳制（见 verification.md 的未验证清单）。按该假设，上游 `prompt_tokens` 包含缓存命中与写入部分，且 `prompt_tokens + completion_tokens` 与 `total_tokens` 一致；canonical `Usage` 中因此不改变 `input_tokens` 与 `total_tokens` 的取值，只把缓存计数作为其子集附加。

两种计数约定必须分别处理：

1. OpenAI 系（Responses 与 Chat Completions）的 `input_tokens` / `prompt_tokens` 包含缓存部分，缓存计数是其子集。因此保持上游原值，输出 `usage.input_tokens_details.cached_tokens` / `usage.prompt_tokens_details.cached_tokens` 与 `cache_write_tokens`。
2. Anthropic Messages 的 `input_tokens` 不包含缓存计数，三者相加才是完整 prompt。因此必须先减去 `cache_read_input_tokens` 与 `cache_creation_input_tokens`，再输出，并保证结果不小于 0。直接透传上游 `prompt_tokens` 会让 Anthropic 客户端的上下文计量接近翻倍，并让缓存命中率算成约 50% 而非真实值。

上游未提供缓存计数时必须省略对应字段，不得补 0，否则客户端无法区分“未命中”与“未上报”。目前只有 Trae IDE 聊天链路投影缓存计数；Trae SOLO work 链路与豆包工作的内部用量事件尚未验证，因此这两条链路不输出缓存字段。

Anthropic 流式 `message_start` 在流开始时尚不知道上游用量，只能输出 `{ input_tokens: 0, output_tokens: 0 }`。上游 `token_usage` 事件在流末尾才到达，因此该限制无法在本网关内消除。Anthropic 客户端若在 `message_delta` 之前读取上下文用量，会读到 0；`message_delta` 输出完整 usage，包含缓存计数。

该 `message_delta.usage` 带 `input_tokens`，偏离官方形状（真实 Anthropic 只返回 `output_tokens`）。原因是 Magpie 等客户端用 `{ ...message_start.usage, ...message_delta.usage }` 合并，而 `message_start` 的 prompt 只能为 0，因此完整用量必须由 `message_delta` 补齐。按累加语义处理 `message_delta.usage` 的客户端会重复计数；主流 SDK 是替换语义，风险已知并接受。

Responses history 记录 provider；跨 provider `previous_response_id` 被拒绝。history 受 session 数、单 session record 数、TTL、总序列化字节数、replay 数和上下文字节预算约束。流式上游必须产生 canonical `done`，自然 EOF 不补成功终态。

- bind host 必须是 loopback。
- middleware 校验 Host、Origin、`sec-fetch-site`、Content-Type 与 body size。
- 可选 `X_ADAPTER_CLIENT_TOKEN` 保护 data-plane。
- 日志仅记录有界结构化诊断；Cookie、JWT、token、设备私钥、完整 callback query、schema literal、prompt、工具参数和工具输出不得写入。

## 生命周期与验证状态

CLI 使用 owner-only runtime lock 和 control socket 管理 daemon。停止与更新先关闭活动 data-plane 连接，再释放 lock。孤儿恢复仅针对命令行已验证为本包 `__daemon__` 的进程。共享 daemon 的调用方可先用 `x-adapter capabilities` 探测能力，再通过 `x-adapter start --no-orphan-recovery` 或 `X_ADAPTER_NO_ORPHAN_RECOVERY=1` 禁止孤儿恢复。该模式只报告 lock 冲突，不终止仍在运行的 daemon。

Magpie 使用独立的 OpenCode 插件包 `@mlzwl/magpie-x-adapter`。插件只连接 loopback 网关，透传稳定的 `x-session-id`，并复用同一数据目录、登录与额度。插件需要补启动 daemon 时，必须先确认 CLI 支持禁止孤儿恢复，再启动一次；插件从不停止 daemon。

Pi extension 仅接受 loopback URL，可自动安装公共 npm 包并启动 daemon；不自动执行 provider 登录。

extension 另注册 `/x-adapter-doubao [model-id]`，解决 Pi 工具 transcript 不能直接沿用旧会话语义的问题。命令先从 `buildSessionProjection()` 判断活动上下文是否包含 `toolCall` / `toolResult`：没有时可直接切换；存在时则以带随机 handoff ID 的 custom turn 请求当前模型生成自包含摘要。`before_agent_start` 只对匹配该 ID 的 custom turn 清空 selected tools，`tool_call` 再作为第二道门禁阻止并终止任何违规调用。只有 assistant 以正常 `stop` 返回非空文本，`agent_before_settle` 才追加 `firstKeptEntryId: null` 的 retain-none compaction；Pi 持久化时会把该 null 改写为 compaction 自身 ID，使活动投影从摘要开始而不携带旧工具 transcript，但 append-only session 文件里的原始条目不会删除。`agent_settled` 必须按随机 handoff ID 找到已持久化边界后才调用 `setModel`，避免旧 compaction 或同目标模型记录被误认。目标认证在摘要前预检，并在最终切换时复检；摘要错误/中止/工具调用、边界非 completed、落盘缺失或模型切换异常均 fail closed。切换流程期间 input hook 拦截新输入，普通 `/model` 不被劫持。该迁移是有损摘要，只改变 Pi 的活动上下文；豆包的工具能力仍由实验性 Connector 桥接提供，不是通用 coding agent 保证。

截至 2026-10-15，Trae 双 realm 行为来自公开源码、多来源协议交叉分析、官方安装包的静态 REA 证据、本仓库离线 mock 和经授权的受控真实请求。Global 个人账号验证确认了授权页、精确 `/authorize` loopback callback、`loginTraceID` correlation 和 callback 字段形状；独立新设备的首次 token exchange 返回 HTTP 403 / 业务码 `20401`。随后在本机 Global/CN 官方 `storage.json` 上确认了真实加密凭据、唯一设备条目和匹配 P-256 密钥，完成隔离目录导入，并从固定 Global SG/CN 数据面成功取得账号可见模型目录；重复验证时目录数量会随账号状态变化，因此不固化为静态列表。两个 realm 的最小非流式纯文本请求均成功；输出边界另以 Global 流式、CN 非流式聚合和 Global 客户端取消完成真实验收。Global DeviceProof refresh 在官方客户端完全退出后执行一次，access/refresh token 均发生轮换，隔离凭据原子更新，随后模型发现和一条有界短聊天成功。全程未输出真实凭据或模型 ID，也未保存正文。单次受控 Global `/api/ide/v2/llm_raw_chat` 仍为 HTTP 400 空体、无 SSE，因此没有接入。CN refresh、工具调用、reasoning、Global US/企业 host、长期轮换和全部目录模型仍无完整真实 E2E 证据，该能力继续标记为实验性。
