1. OpenCode 本地代理为什么要做响应透传
OpenCode 这类 Agent 工具在本地跑起来之后,请求链路通常是这样的:OpenCode 客户端把对话请求发给本地代理,本地代理再转发给真正的模型服务,拿到响应后原样回传给客户端。这条链路里最容易出问题的不是请求发出去,而是响应能不能完整、实时地传回来。
我试过在本地代理里手动拼响应体,结果流式输出直接变成一次性返回,前端聊天框卡半天才刷出全部内容。后来才明白,问题出在响应透传没做对。所谓响应透传,就是把上游返回的状态码、响应头、响应体(包括 SSE 流式分块)原封不动地交给下游客户端,中间不做缓冲、不做改写。
OpenCode 的本地代理场景里,响应透传要处理两种形态。普通响应是一次性返回的 JSON,比如模型列表、非流式对话结果;流式响应是 SSE,服务端会持续推送data: {...}分块,客户端要能边收边渲染。如果代理层用res.send()或者手动拼接字符串,流式就废了。
另一个现实问题是多模型 Key 管理。Agent 开发者手里往往有好几家的 Key,OpenCode 配置里散落着不同 baseURL 和 apiKey,换模型就要改配置、重启。TaoToken 提供统一 Key 和统一 API 通道,把多模型入口收敛到一个地址,本地代理只需要认一个上游,响应透传链路也跟着简化。
这篇就围绕 OpenCode 本地代理的响应透传,给出 TaoToken 统一 Key 接入的settings.json配置骨架,再演示一次请求透传验证,确认响应完整回传。适合正在搭 Agent 本地代理、需要统一管理多模型 Key 的开发者。
2. TaoToken 统一 Key 与 API 通道前置准备
TaoToken 在这里扮演的角色是统一入口:你拿到一个 Key,通过一个 API 地址访问多家模型,OpenCode 本地代理不用再为每个模型维护一套鉴权逻辑。对响应透传来说,上游只有一个,透传链路更干净。
先做两件事。第一,去官网了解接入方式,地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,页面里有接入文档入口。第二,进控制台创建 API Key,控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
API 基础地址用 https://taotoken.net/api ,注意这个地址不带 UTM 参数,配置里直接写它。Key 拿到后先别急着塞进 OpenCode,建议用 curl 单独验证一次,确认 Key 和通道是通的,再往代理里接。这样排障时能快速区分是 Key 问题还是代理透传问题。
模型对话调试可以用 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你后面要做长期编码或 Agent 任务,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
注意:Key 只放在本地环境变量或本地配置文件里,不要提交到 Git 仓库,也不要在日志里打印完整 Key。
3. settings.json 配置骨架与本地代理透传实现
OpenCode 的配置一般落在settings.json,本地代理的监听地址、上游地址、鉴权头都在这里定义。下面给一份配置骨架,你可以按自己的目录结构调整。
{ "agent": { "proxy": { "listen": "http://127.0.0.1:8787", "upstream": "https://taotoken.net/api", "authHeader": "Authorization", "authPrefix": "Bearer ", "apiKeyEnv": "TAOTOKEN_API_KEY", "timeoutMs": 120000, "stream": true }, "models": { "default": "claude-sonnet", "fallback": "gpt-4o-mini" } } }字段含义对照一下:
| 字段 | 作用 | 建议值 |
|---|---|---|
| listen | 本地代理监听地址 | 127.0.0.1:8787 |
| upstream | 上游 API 基础地址 | https://taotoken.net/api |
| authHeader | 鉴权头名称 | Authorization |
| authPrefix | 鉴权头前缀 | Bearer 加空格 |
| apiKeyEnv | 读取 Key 的环境变量名 | TAOTOKEN_API_KEY |
| timeoutMs | 上游超时时间 | 120000 |
| stream | 是否开启流式透传 | true |
配置里不写死 Key,而是通过环境变量注入。启动代理前先导出:
export TAOTOKEN_API_KEY="你的Key"接下来是本地代理的核心透传逻辑,用 Node.js 写一个最小实现。关键点有三个:请求头透传、响应头透传、响应体 pipe。
const http = require("http"); const https = require("https"); const { URL } = require("url"); const UPSTREAM = "https://taotoken.net/api"; const API_KEY = process.env.TAOTOKEN_API_KEY; const server = http.createServer((req, res) => { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { const target = new URL(UPSTREAM + req.url); const headers = { "Content-Type": req.headers["content-type"] || "application/json", "Authorization": "Bearer " + API_KEY, "Content-Length": Buffer.byteLength(body), }; const proxyReq = https.request( { hostname: target.hostname, port: 443, path: target.pathname + target.search, method: req.method, headers, }, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); } ); proxyReq.on("error", (err) => { console.error("proxy error:", err.message); res.writeHead(502, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "upstream unreachable" })); }); proxyReq.write(body); proxyReq.end(); }); }); server.listen(8787, "127.0.0.1", () => { console.log("local proxy on http://127.0.0.1:8787"); });这段代码里,res.writeHead(proxyRes.statusCode, proxyRes.headers)把上游状态码和响应头原样写回,proxyRes.pipe(res)把响应流直接管道给客户端。流式场景下,SSE 的每个 chunk 会实时转发,pipe 在流结束时自动关闭下游响应,不需要手动res.end()。
Content-Length用Buffer.byteLength(body)计算,不要用body.length,中文等多字节字符会导致长度算错,上游可能直接拒绝或截断请求体。
4. 验证请求透传与响应完整回传
代理跑起来后,先验证普通响应,再验证流式响应。
普通响应验证,用 curl 打本地代理:
curl -s http://127.0.0.1:8787/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet", "messages": [{"role": "user", "content": "用一句话说明什么是响应透传"}], "stream": false }'预期结果是返回一段完整 JSON,包含choices字段和模型回复内容。如果返回 401,说明 Key 或鉴权头有问题;返回 502,说明上游地址或网络层有问题。
流式响应验证,把stream改成true:
curl -N http://127.0.0.1:8787/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet", "messages": [{"role": "user", "content": "数到五"}], "stream": true }'-N关闭 curl 缓冲,你应该能看到data: {...}一行行实时刷出来,而不是等几秒后一次性出现。这就是响应透传生效的直接证据。如果全部内容一次性出现,检查代理里是不是用了缓冲写法,或者stream配置没开。
验证响应头是否透传,可以加-i看返回头:
curl -i -s http://127.0.0.1:8787/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"claude-sonnet","messages":[{"role":"user","content":"hi"}],"stream":true}'重点看content-type是不是text/event-stream,transfer-encoding是不是chunked。这两个头能透传,说明响应头链路是通的。
5. 本篇常见错误排查
502 Bad Gateway 反复出现。先确认upstream地址写的是https://taotoken.net/api,不要带多余路径。再确认本机能正常访问该地址,可以用 curl 直接打上游验证。如果上游通、代理不通,检查proxyReq.on("error")有没有打印具体错误信息。
流式输出变成一次性返回。最常见原因是代理层做了缓冲,比如先把proxyRes收集完再res.end()。正确做法是proxyRes.pipe(res),让数据边收边转发。另一个原因是客户端侧缓冲,curl 要加-N,前端 fetch 要确认没开额外缓冲。
401 Unauthorized。检查环境变量TAOTOKEN_API_KEY是否导出成功,echo $TAOTOKEN_API_KEY看有没有值。再检查鉴权头拼接,Bearer后面有一个空格,少了空格会鉴权失败。Key 本身如果失效,去 API Keys 页面重新生成。
请求体被截断或中文乱码。检查Content-Length是不是用Buffer.byteLength(body)算的。用body.length在纯 ASCII 下没问题,一旦有中文就会偏小,上游按错误的长度读取,请求体就被截断。
请求一直挂起直到超时。检查有没有调用proxyReq.end()。只write不end,请求永远不会发出。可以合并成proxyReq.end(body),效果一样。
响应头丢失自定义字段。res.writeHead传入的是proxyRes.headers,如果上游返回了自定义头,理论上会一起透传。如果发现丢了,检查中间有没有手动改写 headers 对象。
6. 统一 Key 接入后的下一步
本地代理透传链路跑通之后,OpenCode 侧只需要把 baseURL 指向http://127.0.0.1:8787,模型名按 TaoToken 支持的名称填,Key 交给代理层统一注入。这样换模型不用改 OpenCode 配置,只改代理里的默认模型字段。
如果你在排障或接入阶段卡住,优先看 API Keys 和接入文档:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 与 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。想先验证模型通不通,用模型对话页面:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。长期跑编码或 Agent 任务,看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
透传这块我踩过的坑基本都在响应侧:状态码透传了但响应头没透传,客户端拿不到text/event-stream就不按流式解析;pipe 用对了但上游超时没设,长任务直接断。把timeoutMs调大、把proxyRes.pipe(res)写对,这两步能解决大部分响应不完整的问题。