ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【Agent】【OpenCode】本地代理分析(分块传输):TaoToken 统一 Key 接入与 config.toml 配置骨架

【Agent】【OpenCode】本地代理分析(分块传输):TaoToken 统一 Key 接入与 config.toml 配置骨架 1. OpenCode 本地代理为什么会卡在分块传输OpenCode 这类 Agent 客户端在跑长任务时最典型的动作就是持续向模型要流式输出一边生成 token一边把增量内容吐回终端。它默认走 OpenAI 风格的/v1/chat/completions请求体里带stream: true响应侧就是 SSE 加 HTTP chunked。问题往往不出在模型本身而是出在中间那层本地代理——请求体是分块传进来的响应体也是分块吐出去的任何一头没接住表现就是终端一直转圈、内容半截断掉、或者干脆报socket hang up。我试过把 OpenCode 直接指向远端兼容接口短对话没问题一旦让它连续改十几个文件就开始飘。后来把链路拆开看才发现本地代理在转发时对 chunked 的处理有坑请求侧用Content-Length一次性发完没问题但 OpenCode 某些版本会用 chunked 上传 body响应侧如果代理手动设了Content-Length流式就被压成一次性返回Agent 的增量渲染直接失效。这篇就聚焦这条链路把 TaoToken 统一 Key 接进来给一份能直接抄的config.toml骨架再配一套抓包验证动作帮你定位到底是哪一段把流掐断了。适合谁看已经在用 OpenCode 或类似 OpenAI 兼容客户端、想接统一 API 通道、并且被流式响应问题折腾过的人。下面所有配置都以本地代理监听127.0.0.1:2048为例你可以按自己端口改。2. TaoToken 统一 Key 与接入前置TaoToken 在这里的角色是统一 API 通道你不需要在 OpenCode 里分别填各家模型的地址和 Key而是拿一个统一 Key通过它的 API 入口转发到具体模型。对本地代理来说这带来两个直接好处——第一代理只需要认一个上游 host 和一套鉴权头转发逻辑大幅简化第二模型切换在服务端完成OpenCode 侧配置不用动。接入前你需要准备三样东西。第一是 TaoToken 的 API Key在控制台的 API Keys 页面创建格式通常是sk-开头的一串。第二是确认 API 基地址TaoToken 的 API 入口是https://taotoken.net/api注意这个地址不带任何查询参数直接作为 base URL 使用。第三是确认你要调的模型名比如qwen-plus、claude-sonnet这类填在请求体的model字段里。有一点要提前说清楚本地代理只负责转发不负责鉴权转换。OpenCode 发出来的Authorization: Bearer sk-xxx会被代理原样透传给 TaoToken所以你在 OpenCode 里填的 Key 就应该是 TaoToken 的统一 Key而不是某个模型厂商的 Key。这样链路是OpenCode → 本地代理127.0.0.1:2048→ TaoToken API → 目标模型。代理层不做 Key 替换只做 host 和 path 的改写排障时链路更干净。如果你还没建 Key可以先到控制台把 Key 建好顺手在模型对话页面发一条测试消息确认通道可用再回来配代理。这一步能省掉后面很多“到底是代理错还是 Key 错”的扯皮。3. 可复制的 config.toml 与 settings.json 骨架OpenCode 的配置分两层一层是config.toml管模型 provider 和默认行为一层是settings.json管运行时和代理相关字段。下面这份骨架你可以直接改端口和模型名使用。先看config.toml# ~/.config/opencode/config.toml # 本地代理模式所有请求先打到 127.0.0.1:2048 [provider.local_proxy] name taotoken-proxy base_url http://127.0.0.1:2048/v1 api_key sk-你的TaoToken统一Key wire_api chat # 走 OpenAI 风格 /v1/chat/completions [model] provider local_proxy name qwen-plus # 换成你要用的模型名 stream true # Agent 场景必须开流式 temperature 0.7 max_tokens 8192 [agent] auto_compact true max_turns 50关键点有三个。base_url指向本地代理而不是 TaoToken因为代理会帮你改写 pathwire_api chat告诉 OpenCode 用 chat completions 协议stream true是 Agent 增量输出的前提关掉它分块传输就无从谈起。再看settings.json这里放代理和超时相关字段{ proxy: { enabled: true, url: http://127.0.0.1:2048, no_proxy: localhost,127.0.0.1 }, request: { timeout_ms: 120000, stream_idle_timeout_ms: 60000, max_retries: 2 }, logging: { level: debug, log_chunks: true } }stream_idle_timeout_ms这个字段值得单独说它管的是流式响应里两个 chunk 之间的最大间隔。Agent 跑长任务时模型思考阶段可能十几秒不吐 token如果这个值设太小客户端会误判超时然后断开表现就是“内容生成到一半突然停”。设成 60000 比较稳。log_chunks: true打开后代理会把每个 chunk 的到达时间打进日志后面排障全靠它。代理脚本本身用 Node.js 内置http/https就够核心是请求侧拼 body、响应侧 pipe// proxy.js const http require(http); const https require(https); const UPSTREAM_HOST taotoken.net; const UPSTREAM_PATH /api/v1/chat/completions; const server http.createServer((req, res) { if (req.method POST req.url /v1/chat/completions) { let body ; req.on(data, chunk { body chunk; }); req.on(end, () { const auth req.headers[authorization] || ; const options { hostname: UPSTREAM_HOST, port: 443, path: UPSTREAM_PATH, method: POST, headers: { Authorization: auth, Content-Type: application/json, Content-Length: Buffer.byteLength(body) } }; const proxyReq https.request(options, proxyRes { // 关键透传上游响应头保留 Transfer-Encoding: chunked res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); proxyReq.on(error, e { res.writeHead(502); res.end(Bad Gateway: e.message); }); proxyReq.write(body); proxyReq.end(); }); return; } res.writeHead(404); res.end(Not Found); }); server.listen(2048, 127.0.0.1, () { console.log(proxy on http://127.0.0.1:2048); });这里最容易写错的一行是res.writeHead(proxyRes.statusCode, proxyRes.headers)。如果你手动构造响应头、只写Content-Type而漏掉Transfer-EncodingNode 会默认用Content-Length或直接缓冲整个响应流式就没了。透传上游头是保住 chunked 的最省事做法。4. 验证请求与分块传输抓包配好之后别急着跑 Agent先用 curl 打一发确认代理和上游都通curl -N -X POST http://127.0.0.1:2048/v1/chat/completions \ -H Authorization: Bearer sk-你的TaoToken统一Key \ -H Content-Type: application/json \ -d { model: qwen-plus, messages: [{role: user, content: 数到五}], stream: true }-N关掉 curl 的缓冲这样你能肉眼看到 token 一个个蹦出来。正常输出长这样data: {choices:[{delta:{content:1}}]} data: {choices:[{delta:{content:、}}]} data: {choices:[{delta:{content:2}}]} ... data: [DONE]如果所有内容一次性刷出来说明流式在某一环被缓冲了。接下来用抓包确认分块传输到底有没有发生。推荐用tcpdump抓本地回环或者更直观地用mitmproxy看明文# 抓 2048 端口的回环流量写到文件 sudo tcpdump -i lo0 -A -s 0 tcp port 2048 -w proxy.pcap跑完一次请求后用 Wireshark 打开proxy.pcap过滤http看响应头里有没有Transfer-Encoding: chunked。有说明分块传输成立没有而是Content-Length说明代理把流压平了。再看请求侧如果 OpenCode 用 chunked 上传 body你会在请求头看到Transfer-Encoding: chunked且没有Content-Length此时代理的req.on(data)会分多次触发body chunk把它们拼起来end时才是完整 JSON。一个更轻量的验证方式是在代理里打时间戳req.on(data, chunk { console.log(chunk at, Date.now(), len, chunk.length); body chunk; });如果日志里同一请求出现多行chunk at说明请求体确实是分块来的如果只有一行说明客户端用了Content-Length一次性发。两种都正常代理都要能处理。5. 本篇常见错排查错误一Content-Length和Transfer-Encoding同时出现。这是代理手动设头时最常见的冲突。HTTP 规范里两者不能共存Node 会直接报ERR_HTTP_CONTENT_LENGTH_MISMATCH或让客户端解析失败。解法就是上面说的响应侧透传proxyRes.headers别自己拼。错误二流式响应被缓冲终端一次性出结果。检查三处代理有没有透传Transfer-Encoding中间有没有别的反向代理比如 nginx默认开了proxy_buffering onOpenCode 的stream是不是被某层配置覆盖成了 false。nginx 场景加一行proxy_buffering off;即可。错误三socket hang up或流到一半断。多半是stream_idle_timeout_ms太小或者上游在长思考时超过了代理的 socket 超时。把代理的server.timeout和客户端 idle 超时都调大同时确认max_retries不会在流已经开始后重试——流式请求重试会导致重复内容。错误四请求体 JSON 解析失败。如果代理里对 body 做了JSON.parse再改写注意 chunked 场景下end之前 body 不完整必须等end再 parse。另外body chunk在 chunk 是 Buffer 时默认按 utf8 转中文多字节字符跨 chunk 边界可能被截断稳妥做法是先收集 Buffer 数组再Buffer.concat后toString(utf8)。错误五404 Not Found。代理只监听了/v1/chat/completionsOpenCode 如果请求/v1/models或别的路径就会 404。要么在代理里补上这些路由要么确认 OpenCode 的wire_api配置没让它走别的 endpoint。6. 继续接入与验证链路跑通之后建议按这个顺序往下走先在模型对话页面确认统一 Key 能正常出流式结果排除 Key 和通道问题再回到本地代理用上面的 curl 和抓包确认 chunked 透传最后才让 OpenCode 跑真实 Agent 任务。这样出问题时你能快速判断是通道、代理还是客户端。如果你打算长期用 OpenCode 跑编码和 Agent 任务可以看下 Coding Plan 的额度方案比按次调用更适合高频场景。接入文档里有完整的 base URL 和鉴权说明配置字段对不上时以文档为准。API Keys 页面可以随时新建和吊销 Key建议给本地代理单独建一个方便轮换。最后留一个实用习惯把代理日志按天切分log_chunks打开后日志会涨得很快但排障时它就是你的时间线。等链路稳定了再关掉能省不少磁盘。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进