1. 为什么要在 Trae 里手搓一个 AI 中继程序
如果你同时用 Trae、Cursor、Cline、Auto-Coder 这类工具,大概率会遇到一个很烦的问题:每个工具都要单独填一遍 API Key、Base URL、模型名,换一个模型就得改一圈配置。更麻烦的是,有些工具对 OpenAI SDK 版本、stream_options参数、base_url拼接方式的要求还不一样,一个地方没对上就直接报错。
我这次的做法是:用 Trae 里的 Deepseek-v3.1 帮我生成一个轻量级 AI 中继程序骨架,然后把所有上游模型的调用统一收敛到 TaoToken 的 API 通道上。中继程序对外只暴露一个 OpenAI 兼容的/v1/chat/completions接口,内部负责把请求转发到 TaoToken,Key 也只在中继这一层配置一次。这样 Trae、Auto-Coder、Moon Pilot 这些客户端只需要把base_url指向本地中继,api_key随便填一个占位符就能跑通。
这篇内容适合三类人:一是手里有多个 AI 编码工具、想统一管理 Key 的开发者;二是想理解 OpenAI 兼容接口转发链路、自己写中继练手的人;三是被Completions.create() got an unexpected keyword argument 'stream_options'这类报错卡住、想搞清楚根因的人。下面我会把 config.toml、settings.json、中继核心路由代码、curl 验证命令全部给出来,你照着复制就能跑。
2. TaoToken 前置准备:统一 Key 与通道地址
中继程序的核心思路是「上游只认一个通道」。TaoToken 提供的就是这样一个统一入口,你不需要在代码里硬编码各家模型的地址,只需要拿到一个 API Key,然后把请求打到统一的 Base URL 上。
先到控制台创建一个 API Key。地址是:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys创建完之后你会得到一个形如sk-xxxx的 Key。这个 Key 就是中继程序里唯一需要配置的凭证。TaoToken 的 API 入口是:
https://taotoken.net/api注意这个地址后面不带 UTM 参数,它是给程序调用的,不是给浏览器点的。中继程序里配置的base_url就填这个,OpenAI SDK 会自动在后面拼/v1/chat/completions。
如果你还没决定用哪个模型,可以先到模型对话页面手动试一下,确认通道是通的:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat在对话页面里选一个模型发一句话,能正常返回就说明 Key 和通道都没问题。这一步很重要,因为后面中继报错时,你需要先排除「是上游通道的问题」还是「是中继代码的问题」。如果对话页面都不通,那中继再怎么调也是白搭。
提示:Key 只创建一次就够,中继程序、Trae、Auto-Coder 全部复用同一个 Key。不要在每个客户端里各填一份,那样就失去统一管理的意义了。
3. 用 Deepseek-v3.1 生成中继骨架:config.toml 与 settings.json
打开 Trae,把模型切到 Deepseek-v3.1。我用的提示词大致是这样的:
用 FastAPI 写一个 OpenAI 兼容的 AI 中继服务,要求: 1. 对外暴露 POST /v1/chat/completions,支持 stream 和非 stream 2. 上游 base_url 和 api_key 从环境变量读取 3. 转发时保留 messages、model、temperature、stream 参数 4. 流式响应要用 StreamingResponse 返回 text/event-stream 5. 附带 /health 健康检查接口Deepseek-v3.1 生成出来的骨架结构基本可用,但有几个地方需要手动改:一是上游地址要换成 TaoToken 的,二是要处理stream_options参数透传,三是超时时间要调大一点。下面是我改完之后的config.toml,放在项目根目录:
[server] host = "0.0.0.0" port = 8000 reload = true [upstream] base_url = "https://taotoken.net/api" api_key = "sk-你的TaoToken密钥" default_model = "deepseek-v3.1" timeout = 60.0 [proxy] allow_origins = ["*"] strip_auth = true对应的settings.json,如果你用的是支持 JSON 配置的客户端(比如某些 VS Code 插件或 Cline),可以这样写:
{ "aiRelay": { "baseUrl": "http://127.0.0.1:8000/v1", "apiKey": "any_key_placeholder", "model": "deepseek-v3.1", "stream": true, "timeout": 60000 } }这里有个关键点:客户端里的apiKey填什么都行,因为中继程序会忽略客户端传来的 Authorization,统一用config.toml里的 TaoToken Key 去请求上游。这就是「统一 Key」的实现方式——Key 只存在于中继这一层,客户端拿不到也不需要真实 Key。
strip_auth = true这个开关就是干这个的:中继收到请求后,把客户端带的 Authorization 头丢掉,换成自己的。这样即使客户端配置泄露,也不会泄露真实的 TaoToken Key。
4. 中继核心路由代码:转发与流式处理
下面是中继程序的核心代码,保存为main.py。这段代码是在 Deepseek-v3.1 生成的基础上改的,重点处理了流式转发和参数透传:
import os import httpx from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse app = FastAPI(title="TaoToken AI Relay") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) UPSTREAM_BASE = os.getenv("UPSTREAM_BASE", "https://taotoken.net/api") UPSTREAM_KEY = os.getenv("UPSTREAM_KEY", "sk-你的TaoToken密钥") DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "deepseek-v3.1") TIMEOUT = float(os.getenv("UPSTREAM_TIMEOUT", "60")) @app.post("/v1/chat/completions") async def relay_chat(request: Request): try: data = await request.json() except Exception: raise HTTPException(status_code=400, detail="Invalid JSON body") messages = data.get("messages", []) if not messages: raise HTTPException(status_code=400, detail="No messages provided") stream = data.get("stream", False) payload = { "model": data.get("model", DEFAULT_MODEL), "messages": messages, "temperature": data.get("temperature", 0.6), "stream": stream, } if "stream_options" in data: payload["stream_options"] = data["stream_options"] if "max_tokens" in data: payload["max_tokens"] = data["max_tokens"] headers = { "Content-Type": "application/json", "Authorization": f"Bearer {UPSTREAM_KEY}", } client = httpx.AsyncClient(timeout=TIMEOUT) url = f"{UPSTREAM_BASE}/v1/chat/completions" if stream: req = client.build_request("POST", url, json=payload, headers=headers) resp = await client.send(req, stream=True) async def event_stream(): try: async for chunk in resp.aiter_bytes(): yield chunk finally: await resp.aclose() await client.aclose() return StreamingResponse( event_stream(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, ) else: resp = await client.post(url, json=payload, headers=headers) await client.aclose() if resp.status_code != 200: raise HTTPException(status_code=resp.status_code, detail=resp.text) return resp.json() @app.get("/health") async def health(): return {"status": "healthy", "upstream": UPSTREAM_BASE} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)启动命令:
uvicorn main:app --host 0.0.0.0 --port 8000 --reload或者直接:
python main.py这段代码里有两个容易踩坑的地方。第一,流式转发必须用client.send(req, stream=True)配合aiter_bytes(),如果直接用client.post再iter_bytes,连接会在响应结束前被关掉,客户端会收到截断的 SSE。第二,stream_options要显式透传,因为有些客户端(比如 Auto-Coder)会带这个参数,如果中继把它吞掉,上游可能返回的 usage 统计就不完整。
5. 验证请求:curl 与 OpenAI SDK 双通道测试
中继跑起来之后,先用 curl 验证非流式请求:
curl -X POST "http://127.0.0.1:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer any_key" \ -d '{ "model": "deepseek-v3.1", "messages": [{"role": "user", "content": "你好,介绍一下你自己"}], "temperature": 0.6, "stream": false }'如果返回里有choices[0].message.content,说明中继到 TaoToken 的链路是通的。注意这里的Authorization填的是any_key,中继会忽略它,用自己配置的 TaoToken Key 去请求上游。
再验证流式请求:
curl -N -X POST "http://127.0.0.1:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer any_key" \ -d '{ "model": "deepseek-v3.1", "messages": [{"role": "user", "content": "用中文写一首关于春天的短诗"}], "temperature": 0.8, "stream": true }'-N参数是关闭 curl 的缓冲,这样你能实时看到 SSE 数据一行行刷出来。如果看到data: {...}一行行出现,最后以data: [DONE]结束,说明流式转发正常。
用 OpenAI SDK 测试更贴近真实客户端场景:
from openai import OpenAI client = OpenAI( api_key="any_key", base_url="http://127.0.0.1:8000/v1" ) resp = client.chat.completions.create( model="deepseek-v3.1", messages=[{"role": "user", "content": "你好"}], temperature=0.6, stream=False ) print(resp.choices[0].message.content) print("tokens:", resp.usage.total_tokens)这里要特别注意 OpenAI SDK 的版本。我一开始用旧版本跑,直接报了:
TypeError: Completions.create() got an unexpected keyword argument 'stream_options'这个报错的根因是旧版 SDK 不认识stream_options参数,而 Auto-Coder 这类工具会主动带上它。解决办法就是升级:
pip install openai -U升到 1.105.0 之后问题消失。所以如果你在客户端侧遇到这个报错,先别怀疑中继代码,先pip show openai看一眼版本。
6. 本篇常见错排查
报错一:Completions.create() got an unexpected keyword argument 'stream_options'
这是最高频的一个。根因是客户端用的 OpenAI SDK 版本太旧,不支持stream_options。解决方式是升级 SDK:pip install openai -U。中继侧不需要改代码,因为中继只是透传参数,问题出在客户端本地库。
报错二:curl 返回 401 或 403
先检查config.toml里的api_key是不是 TaoToken 控制台创建的那个,注意不要有多余空格。再确认base_url是https://taotoken.net/api,不要手动加/v1,因为代码里已经拼了/v1/chat/completions,重复拼会变成/api/v1/v1/...。
报错三:流式响应卡住不返回
大概率是 httpx 的超时设置太短,或者用了client.post而不是client.send(stream=True)。把TIMEOUT调到 60 秒以上,并确认流式分支用的是build_request+send。
报错四:客户端连不上127.0.0.1:8000
如果客户端跑在容器或另一台机器上,127.0.0.1指向的是客户端自己,不是中继所在机器。把base_url换成中继机器的局域网 IP,比如http://192.168.0.98:8000/v1,并确认防火墙放行了 8000 端口。
报错五:模型名不匹配
中继里DEFAULT_MODEL设的是deepseek-v3.1,但客户端可能传了别的模型名。如果上游返回「模型不存在」,检查客户端传的model字段是否在 TaoToken 支持的模型列表里。可以在模型对话页面确认可用模型名。
7. 把中继接进 Trae 与 Auto-Coder
中继跑通之后,接下来就是把它接到实际工具里。Trae 里配置自定义模型时,base_url填http://127.0.0.1:8000/v1,api_key填任意占位符,模型名填deepseek-v3.1。这样 Trae 的所有请求都会经过中继转发到 TaoToken。
Auto-Coder 的配置命令类似:
/models /add_model name=relay model_name=deepseek-v3.1 base_url=http://127.0.0.1:8000/v1 /models /add relay any_key /conf model:relay如果你打算长期用这套中继跑编码 Agent,建议把 TaoToken 的 Coding Plan 也了解一下,它在长会话和 Agent 场景下的额度策略更划算:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan接入文档里有完整的参数说明和错误码对照,遇到中继返回的 4xx/5xx 可以对照排查:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc整套链路跑下来,我的体会是:中继程序本身不复杂,难的是把客户端 SDK 版本、参数透传、流式处理这几个点对齐。一旦对齐,后面换模型、加工具都只需要改中继一处配置,客户端完全不用动。