# trae-enterprise-proxy **Repository Path**: fleey/trae-enterprise-proxy ## Basic Information - **Project Name**: trae-enterprise-proxy - **Description**: trae企业版cli proxy。严禁前人砍树后人暴晒。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2026-08-21 - **Last Updated**: 2026-09-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # trae2api 把本机的 **TraeCode CLI (`traecli`)** 反代成 **OpenAI 兼容接口 + Anthropic 兼容接口**, 让 Codex、Claude Code、Cherry Studio、NextChat、LangChain、官方 `openai` / `anthropic` SDK 等标准客户端直接用上 Trae 的模型。 - ✅ **三套协议**:OpenAI Chat Completions、OpenAI Responses、Anthropic Messages,同一个端口 - ✅ **真 function calling**:直连模式下客户端的 `tools` 原样透传,回来的是标准 OpenAI `tool_calls` / Anthropic `tool_use` - ✅ **不需要工作目录**:直连模式下工具由客户端自己执行,不用配 workspace、不用挂 volume - ✅ **流式 thinking**:推理内容逐 token 实时吐到 `reasoning_content` / `thinking_delta` - ✅ **图片识别**:OpenAI `image_url` 的 `data:` URI、Anthropic `image.source.base64` 都透传 (发给纯文本模型会被挡在本地并告诉你换哪个,见 `GET /v1/models` 的 `multimodal`) - ✅ **完整 agent 能力**:agent 模式下可以真的改文件、跑命令 - ✅ **19 个模型**:Doubao / GLM / MiniMax / Qwen / Kimi / DeepSeek - ✅ **Docker**:镜像内置官方 Linux 版 traecli --- ## 先选模式:`direct` 还是 `agent` 这是用这个项目**最重要的一个决定**,由 `TRAE2API_UPSTREAM` 控制。两种模式的能力边界完全不同。 | | `direct`(默认) | `agent` | |---|---|---| | 走什么 | 直连 Trae 后端 `/api/ide/v2/llm_raw_chat` | traecli app-server,跑完整 agent loop | | 客户端 `tools` | ✅ **原样透传,返回真的 `function_call` / `tool_use`** | ❌ 只能忽略 | | 谁执行工具 | **客户端自己**(在它自己的当前目录) | traecli(在服务端配置的 cwd 里) | | 要不要配工作目录 | ❌ **完全不需要** | ✅ 必须配,否则 agent 在空目录里瞎编 | | Docker 要不要挂项目 | ❌ 不用 | ✅ 要挂,还要配 `CWD_MAP` | | `instructions` / `system` 提示 | ✅ 用客户端自己的 | 被 traecli 约 14700 字符的系统提示覆盖 | | 输入开销 | ≈ 几百 token | 每轮 22k~27k token | | 正文流式 | ✅ 逐 token | ❌ 上游整块下发 | | 自主改文件 / 跑命令 | ❌ 由客户端驱动 | ✅ | | 服务端文件系统访问 | **零** | 有(受 roots 白名单约束) | | 稳定性 | ⚠️ 未公开内部端点,随时可能变 | ✅ 官方 CLI 接口 | **怎么选:** - **客户端自带 agent loop**(Codex、Claude Code、CC Switch、LangChain agent、任何要 function calling 的东西)→ 用 `direct`。这是它们本来期望的协议,也是唯一能拿到 `function_call` / `tool_use` 的路。 - **想让服务端自己动手改代码**(发一句话,让它把文件改了)→ 用 `agent`。 - **只是纯聊天** → 两个都行,`direct` 更省 token、正文还是流式的。 默认就是 `direct`,不用配。确认上游能通(只读探测,不写任何东西,不打印 token): ```bash python3 tools/verify_direct.py --tools ``` 要服务端自己动手改文件,才需要显式切回: ```bash TRAE2API_UPSTREAM=agent ``` `agent` 模式还得配工作目录,见下面「`agent` 模式专属」那节。 --- ## ⚠️ 安全须知 安全边界**取决于上面选的模式**,差别很大: **`direct` 模式(默认)**:服务端不碰文件系统,不起 traecli 的 agent loop。风险面就是一个普通的 模型 API + 你的 Trae 账号额度。下面那一整套 cwd 白名单机制在这个模式下不参与,因为没有东西需要它约束。 **`agent` 模式(需显式开启)**:**任何能访问这个端口的人,都能在这台机器上执行 shell 命令、读写文件。** 它不是一个普通的模型 API。内置的几道防线(都是默认生效的): | 防线 | 行为 | |---|---| | 强制鉴权 | 没配任何 key 就**拒绝启动**(除非显式 `TRAE2API_ALLOW_NO_AUTH=1`) | | 只听回环 | 默认绑 `127.0.0.1`;绑非回环地址又没鉴权时拒绝启动 | | 工作目录白名单 | 没给 key 配 roots 时,它**根本不能指定 cwd**,一律跑在 scratch 目录 | | 路径归一 | `trae_cwd` 先 `realpath` 再校验,`../../etc` 这类会被 403 | | 按 key 隔离 | 多 key 模式下,每个 key 只能碰自己的 roots,跨项目越权 403 | | 沙箱 | 默认 `workspace-write` + `approvalPolicy=never`:越权动作直接拒,不会挂起等人 | | 越权需双开关 | `danger-full-access` 要求服务端 `TRAE2API_ALLOW_FULL_ACCESS=1` **且**请求显式指定 | 鉴权在两种模式下都强制生效。但不管哪种模式,都不要把这个端口暴露到公网或不受信任的局域网。 --- ## 快速开始 前提:本机已装好 `traecli` 并且 `traecli login` 登录过(`traecli login status` 可查)。 ```bash python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt cp .env.example .env ``` 编辑 `.env`:**把 `TRAE2API_API_KEY` 改掉**。上游模式保持默认的 `direct` 即可。然后: ```bash ./run.sh ``` 验证: ```bash ./.venv/bin/python tools/smoke_test.py --api-key "$TRAE2API_API_KEY" ``` `direct` 模式到这里就配完了 —— 没有工作目录、roots、volume 要操心。 ### 接客户端 两套协议同一个端口、同一把 key(你设的 `TRAE2API_API_KEY`),区别只有 Base URL 带不带 `/v1`: | 客户端 | Base URL | Key 怎么带 | |---|---|---| | OpenAI 系(Codex、Cherry Studio、NextChat、`openai` SDK…) | `http://localhost:8787/v1` | `Authorization: Bearer` | | Anthropic 系(Claude Code、`anthropic` SDK…) | `http://localhost:8787`(**不带 `/v1`**,SDK 自己拼) | `x-api-key`,`Bearer` 也认 | Claude Code 直接用环境变量起: ```bash ANTHROPIC_BASE_URL=http://localhost:8787 ANTHROPIC_AUTH_TOKEN=你的key claude ``` Anthropic 侧的字段对应关系和流式事件见 [`POST /v1/messages`](#post-v1messages)。 下面是 OpenAI 侧的例子。function calling: ```python from openai import OpenAI client = OpenAI(base_url="http://localhost:8787/v1", api_key="你的key") resp = client.chat.completions.create( model="Doubao-Seed-Evolving", messages=[{"role": "user", "content": "北京天气怎么样?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "description": "查询某个城市的天气", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }, }], ) print(resp.choices[0].message.tool_calls) # [ChatCompletionMessageToolCall(id='call_...', function=Function( # arguments='{"city": "Beijing"}', name='get_weather'), type='function')] ``` 流式 + thinking: ```python resp = client.chat.completions.create( model="DeepSeek-V4-Flash 正式版", messages=[{"role": "user", "content": "一步步推理:17*23 等于多少?"}], stream=True, ) for ev in resp: if not ev.choices: continue delta = ev.choices[0].delta if getattr(delta, "reasoning_content", None): print(delta.reasoning_content, end="", flush=True) # 思考过程 if delta.content: print(delta.content, end="", flush=True) # 正文 ``` > 官方 SDK 的 typed model 不认识 `reasoning_content`,非流式下要从 > `resp.choices[0].message.model_extra["reasoning_content"]` 取;流式的 `delta` 可以直接 > `getattr(delta, "reasoning_content", None)`。 --- ## API ### `GET /health` 不需要鉴权,给编排系统探活用。 ```json {"status":"ok","upstream":"direct","traecli":"0.201.4-tob","app_server":"disabled","active_turns":0,"version":"0.1.0"} ``` `direct` 模式下 `app_server` 恒为 `disabled` —— 那个子进程根本不会起。 `agent` 模式下 `app_server` 有 `running` / `restarting` / `failed` / `stopped`。app-server 子进程 挂了会自动按指数退避重启,重启窗口内请求返回 503。 ### `GET /v1/models` / `GET /v1/models/{id}` 返回 19 个模型,`id` 用的是 `traecli models` 里那种展示名(`GLM-5.3`、`DeepSeek-V4-Flash 正式版`)。 每个模型还带顶层整数 `context_window`(直接取自 traecli 模型元数据),Codex Desktop 可以据此自动设置上下文窗口,无需手填。 ```json {"id":"GLM-5.3","object":"model","owned_by":"trae","context_window":200000} ``` OpenAI 和 Anthropic 都在 `/v1/models` 做模型发现,但返回结构不兼容。**带 `anthropic-version` 头的请求**(Anthropic SDK 一定会带)会拿到 Anthropic 那套: ```json {"data":[{"type":"model","id":"GLM-5.3","display_name":"GLM-5.3","created_at":"..."}], "has_more":false,"first_id":"GLM-5.3","last_id":"..."} ``` **别名**:内部 id 也认。`glm-5.3`、`GLM-5.3`、`DeepSeek-V4-Flash-Official` 都能解析到同一个模型, 大小写不敏感。带空格和中文的模型名在某些客户端里不好填,用别名即可。 ### `POST /v1/chat/completions` 生效的标准参数:`model`、`messages`、`stream`、`stream_options.include_usage`,以及 `direct` 模式下的 `tools`、`max_tokens` / `max_completion_tokens`(由代理自己截断,见 [输出兜底](#输出兜底截断与重复塌缩))。`tool_choice` 收下但**不生效**——私有端点忽略它, 实测 `tool_choice:"none"` 照样返回工具调用。 `direct` 模式下的工具调用(`finish_reason` 为 `tool_calls`): ```bash curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ localhost:8787/v1/chat/completions \ -d '{"model":"Doubao-Seed-Evolving", "messages":[{"role":"user","content":"北京天气?调用工具。"}], "tools":[{"type":"function","function":{"name":"get_weather", "parameters":{"type":"object","properties":{"city":{"type":"string"}}}}}]}' ``` 把工具结果喂回去继续下一轮,就是标准 OpenAI 流程:追加一条带 `tool_calls` 的 assistant 消息, 再追加 `{"role":"tool","tool_call_id":"…","content":"…"}`。本代理是无状态的,每轮把完整对话发过来即可。 **扩展参数**(仅 `agent` 模式生效;body 字段优先于 header,header 优先于环境默认): | body | header | 作用 | |---|---|---| | `trae_cwd` | `X-Trae-Cwd` | agent 的工作目录,必须落在该 key 的 roots 内 | | `trae_sandbox` | `X-Trae-Sandbox` | `read-only` / `workspace-write` / `danger-full-access` | | `trae_approval_policy` | — | `untrusted` / `on-failure` / `on-request` / `never` | | `trae_effort` | `X-Trae-Effort` | `none`…`ultra` 推理强度 | | `trae_reasoning_summary` | — | `auto` / `concise` / `detailed` / `none` | | `trae_timeout` | — | 本次超时秒数,仍受 `TRAE2API_TIMEOUT` 封顶 | `direct` 模式会**静默忽略**这一整组参数(包括 `model@cwd` 后缀里的路径),因为那边压根没有服务端 工作目录这回事。 ### `POST /v1/responses` OpenAI **Responses API**。Codex 系客户端用的是这套,而不是 chat/completions —— 两者协议不同, 所以都实现了。 - `input` 支持字符串、`{"role","content"}` 简写、以及完整的 `{"type":"message","content":[{"type":"input_text"...}]}` 项 - `instructions` 映射成 system 消息(`direct` 模式下原样送达模型) - `input_image` 同样支持 `data:` URI - 推理走 `response.reasoning_summary_text.delta` 事件流(**逐 token**) - `direct` 模式下 `tools` 支持扁平写法 `{"type":"function","name":…,"parameters":…}`, 返回标准 `function_call` 输出项;把 `function_call` + `function_call_output` 放回 `input` 就能接着下一轮 - `agent` 模式下 `reasoning.effort` 映射到 traecli 的 effort 流式事件序列(实测): ``` response.created → response.in_progress → output_item.added(reasoning) → reasoning_summary_part.added → reasoning_summary_text.delta × N ← 逐 token 的思考过程 → reasoning_summary_text.done → reasoning_summary_part.done → output_item.done → output_item.added(message) → content_part.added → output_text.delta × N → output_text.done → content_part.done → output_item.done → response.completed ``` 工具调用时把 message 那一段换成 `output_item.added(function_call)` → `function_call_arguments.delta × N` → `function_call_arguments.done` → `output_item.done`。 不支持:`previous_response_id`(400,本代理无状态,请把完整对话放进 `input`)。 ### `POST /v1/messages` **Anthropic Messages API**。Claude Code、官方 `anthropic` SDK、Cherry Studio 的 Claude 供应商走的是这套。鉴权用 `x-api-key`(`Authorization: Bearer` 也认,是同一把 key)。 ```python from anthropic import Anthropic client = Anthropic(base_url="http://localhost:8787", api_key="你的key") msg = client.messages.create( model="Doubao-Seed-Evolving", max_tokens=1024, system="你是一个简洁的助手。", messages=[{"role": "user", "content": "北京天气?调用工具。"}], tools=[{ "name": "get_weather", "description": "查询某个城市的天气", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }], ) print(msg.stop_reason, msg.content) # tool_use [ToolUseBlock(id='call_...', name='get_weather', input={'city': 'Beijing'})] ``` 对应关系: | Anthropic | 这里怎么处理 | |---|---| | `system`(字符串或 text block 数组) | 变成 system 消息,`direct` 模式下原样送达模型 | | `messages[].content` 的 `text` / `image` | 透传;`image.source.base64` 转成 `data:` URI | | 用户消息里的 `tool_result` block | 拆成独立的 tool 消息(Anthropic 塞在 user 轮里,上游要分开) | | `tool_result.is_error` | 内容前面加 `[tool error]`,否则模型看不出工具失败了 | | assistant 消息里的 `tool_use` block | 还原成上游要的 `tool_calls` | | 历史里的 `thinking` block | 丢弃(不带指令价值,且签名不是我们签的) | | `tools[].input_schema` | 映射成 `function.parameters` | | `max_tokens` | **必填**,`direct` 模式下由代理自己数着截断 | | `stop_sequences` | **由代理实现**:匹配上就切流,`stop_reason` 报 `stop_sequence` | | `thinking: {"type":"enabled"}` | 开了才回 `thinking` block;没开就把推理丢掉 | | `temperature` / `top_p` / `top_k` / `tool_choice` | 收下但不生效,上游没这些旋钮 | 流式事件序列: ``` message_start → ping → content_block_start(thinking) → content_block_delta(thinking_delta) × N → content_block_stop → content_block_start(text) → content_block_delta(text_delta) × N → content_block_stop → message_delta(stop_reason, usage) → message_stop ``` 工具调用那一段是 `content_block_start(tool_use)` → `content_block_delta(input_json_delta) × N` → `content_block_stop`,`stop_reason` 为 `tool_use`。 `stop_reason` 映射:正常结束 `end_turn`;有工具调用 `tool_use`;被 [输出兜底](#输出兜底截断与重复塌缩) 截断 `max_tokens`;命中 `stop_sequences` 则是 `stop_sequence`,同时 `stop_sequence` 字段给出命中的那一条。 `agent` 模式下这个端点也能用,但和另外两个协议一样:`tools` 被忽略、永远拿不到 `tool_use`、`max_tokens` 不生效。 报错走 Anthropic 自己的信封,不是 OpenAI 那套: ```json {"type":"error","error":{"type":"invalid_request_error","message":"max_tokens is required."}} ``` ### `POST /v1/messages/count_tokens` 返回 `{"input_tokens": N}`。两边上游都没有 tokenizer,也没有计数端点,所以这是个**估算下界** (和截断时用的是同一套估算:CJK 按 1 token/字,其余按 4 字符/token,不含上游自己加的脚手架)。 够用来做预算,不要拿它对账。 --- ## `agent` 模式专属:工作目录、多项目、磁盘 **用 `direct` 模式的话,这一整节都可以跳过。** ### tools 冲突:为什么 agent 模式给不了 function call Codex / CC Switch 这类客户端**每个请求都带自己的工具定义**,期望模型返回 `function_call` 让客户端自己执行。但 **traecli 是自带 agent loop 的** —— 它会自己跑命令、自己改文件,然后返回文本。 两者不能同时成立:traecli 已经执行过的动作再回传成 `function_call`,客户端会**重复执行一遍**。 所以 `agent` 模式的默认行为是**接受并忽略** `tools`(`TRAE2API_IGNORE_CLIENT_TOOLS=1`)。后果: - ✅ 活会被干完,文件真的会被改 - ❌ 客户端的工具循环、审批 UI、沙箱**完全不参与** —— 干活的是 traecli,用的是 traecli 的沙箱 - ❌ 客户端永远收不到 `function_call`,只会收到一段文本 想要严格拒绝而不是静默忽略:`TRAE2API_IGNORE_CLIENT_TOOLS=0`(返回 400)。 **想要真的 function call,就是切 `TRAE2API_UPSTREAM=direct`** —— 这个架构冲突在那边不存在。 ### 工作目录 这类客户端不会发 `trae_cwd`,所以 `agent` 模式必须设 `TRAE2API_DEFAULT_CWD` 指向你的项目目录, 否则 agent 会在空的 scratch 目录里干活,然后对着一个它看不见的仓库瞎编。 ```bash TRAE2API_DEFAULT_CWD=/workspace TRAE2API_ALLOWED_ROOTS=/workspace ``` 也可以让它从 prompt 里自动认(`TRAE2API_CWD_FROM_REQUEST=1`,默认开):Codex 会在 `` 里发 ``,代理把它抠出来、按 `TRAE2API_CWD_MAP` 映射成容器内路径, 再校验是否落在该 key 的 roots 内。 **`model@cwd` 语法**:给那些只能填模型名的客户端用,受同一套校验,不能用它绕过。 ``` "model": "GLM-5.3@/Users/you/projects/demo" ``` 示例: ```bash curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ localhost:8787/v1/chat/completions \ -d '{"model":"GLM-5.3","trae_cwd":"/Users/you/projects/demo", "messages":[{"role":"user","content":"创建 hello.py 打印 hello"}]}' ``` ### 多项目 / 多租户 一个 key 通吃所有项目是不行的。用 `TRAE2API_KEYS_FILE` 给每个项目发独立 key,各自绑定自己的目录: ```json { "sk-projA-…": {"label": "projA", "roots": ["/srv/projA"]}, "sk-projB-…": {"label": "projB", "roots": ["/srv/projB"]}, "sk-chat-…": {"label": "chatonly"} } ``` ```bash TRAE2API_KEYS_FILE=/etc/trae2api/keys.json ./run.sh ``` - `roots` 省略 → 该 key **永远不能**设 `trae_cwd`,只能纯聊天。 - `roots: "*"` → 继承 `TRAE2API_ALLOWED_ROOTS`。 - 越权返回 403,且**不会**告诉调用方允许的目录是什么(避免泄漏其他项目的路径布局)。 - 也支持内联 `TRAE2API_KEYS='{...}'`,但 key 会进环境变量,生产建议用文件。 日志里每个 turn 都带 `key=