适用版本:VS Code 1.9x+ / Copilot Chat;opencode 1.14.x;系统:Linux(Ubuntu/Debian 系)
更新时间:2026-08-16
零、前提
首先要会使用copilot的Custom Endpoint或oaicopilot插件配置,二者选其一即可。该文章以Custom Endpoint为例进行讲解。关于Custom Endpoint的配置,可以参考我另一篇文章VS Code 通过 Custom Endpoint 接入第三方模型
一、背景与痛点
opencode 提供了免费的 ZEN 节点,端点地址为:
https://opencode.ai/zen/v1
其中包含一些免费模型(如 deepseek-v4-flash-free),官方在 opencode 客户端里可以直接选用。但当我们想把免费模型接入 VS Code 的 Copilot Chat(通过"自定义端点/自带模型"功能)时,会遇到一个非常典型的报错:
Sorry, your request failed. Please try again.
Client Request Id: xxxx
Reason: Rate limit exceeded
{"type":"FreeUsageLimitError","message":"Error from provider (Console): Rate limit exceeded. Please try again later."}
配置里 API Key 写 public、URL 写 https://opencode.ai/zen/v1 都是对的,但请求就是被 429 拦截。为什么 opencode 客户端能用、VS Code 不能用?答案在请求头里。
二、根因:服务端靠请求头识别客户端
通过抓包/实测(curl 直连对比)可以得到以下结论:
| 请求头组合 | 结果 |
|---|---|
裸请求(只带 Authorization: Bearer public) | ❌ 429 限流 |
带 x-opencode-client / x-opencode-project 等头,没有 UA | ❌ 429 限流 |
带 x-opencode-* 头 + User-Agent: opencode/... | ✅ 200 正常 |
也就是说:ZEN 服务端会把请求头中的 User-Agent 和 x-opencode-* 头当作"客户端指纹",只有看起来像 opencode 客户端的请求才进入免费通道,否则全部丢进共享限流池返回 429。
而 VS Code 的"自定义端点"(Custom Endpoint)在处理 requestHeaders 时,会过滤掉 User-Agent 等保留头。也就是说,不管你如何在 chatLanguageModels.json 里写 "User-Agent": "opencode/...",这个头根本不会被发出去(源码里 _reservedHeaders 包含 user-agent,_sanitizeCustomHeaders 会直接跳过)。
补充:曾经用 API Key(
sk-...)测试也是 429,和认证方式无关,纯粹是"客户端指纹"缺失。
三、解决思路:本地小代理补头转发
既然 VS Code 不让我们改 User-Agent,那就让 VS Code 把请求发给本地代理,由代理负责补上所有 opencode 头,再转发到 ZEN 上游。
整体架构:
代理只监听 127.0.0.1,仅本机可访问,不会暴露到局域网。
四、实施步骤
第 1 步:编写本地代理脚本
新建 /home/你的用户名/zen-proxy.py,内容如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
zen-proxy.py - ZEN (opencode.ai) 免费模型本地代理
背景:
VS Code 的 customendpoint 会把 requestHeaders 中的 User-Agent 当作保留头过滤掉,
而 ZEN 服务端 (opencode.ai/zen/v1) 必须看到 opencode 客户端的 User-Agent 和
x-opencode-* 头才放行免费额度, 否则返回 429 FreeUsageLimitError。
方案:
在本地监听一个端口, VS Code 的模型 URL 指向本代理;
代理收到请求后, 补上 opencode 客户端头(每次请求生成新的 session/request id),
再原样转发到 https://opencode.ai/zen/v1。
用法:
python3 zen-proxy.py [--port 8788]
"""
import argparse
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.request import Request, urlopen, HTTPError
UPSTREAM = "https://opencode.ai/zen/v1"
USER_AGENT = "opencode/1.14.28 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13"
# 需要透传但由代理统一生成的头(防止 VS Code 的固定值被服务端拿去限流)
DYN_HEADERS = {
"User-Agent": USER_AGENT,
"x-opencode-client": "cli",
"x-opencode-project": "global",
}
class ZenProxyHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def _handle(self):
length = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(length) if length > 0 else b""
# 上游路径: VS Code 会请求 /v1/chat/completions, 剥掉 /v1 前缀
path = self.path
if path.startswith("/v1/"):
path = path[len("/v1"):]
upstream_url = UPSTREAM + path
req = Request(upstream_url, data=body, method=self.command)
# 透传客户端头(保留 Authorization: Bearer public 等)
for k, v in self.headers.items():
if k.lower() in ("host", "connection", "content-length",
"transfer-encoding", "user-agent",
"x-opencode-client", "x-opencode-project",
"x-opencode-session", "x-opencode-request"):
continue
req.add_header(k, v)
# 生成 opencode 客户端头: 每次请求唯一 id, 避免共享限流池按 id 计数
ts = int(time.time() * 1000)
for k, v in DYN_HEADERS.items():
req.add_header(k, v)
req.add_header("x-opencode-session", f"ses_vscode_{ts}")
req.add_header("x-opencode-request", f"msg_vscode_{ts}")
try:
resp = urlopen(req, timeout=300)
status = resp.status
resp_headers = dict(resp.headers.items())
data = resp.read()
except HTTPError as e:
status = e.code
resp_headers = dict(e.headers.items())
data = e.read()
except Exception as e:
status = 502
resp_headers = {}
data = str(e).encode("utf-8", errors="replace")
self.send_response(status)
for k, v in resp_headers.items():
if k.lower() in ("transfer-encoding", "connection", "content-encoding"):
continue
self.send_header(k, v)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
do_GET = do_POST = do_OPTIONS = do_PUT = do_DELETE = _handle
def log_message(self, fmt, *args):
pass # 静默
def main():
ap = argparse.ArgumentParser(description="ZEN 免费模型本地代理")
ap.add_argument("--port", type=int, default=8788)
args = ap.parse_args()
server = ThreadingHTTPServer(("127.0.0.1", args.port), ZenProxyHandler)
print(f"[zen-proxy] listening on http://127.0.0.1:{args.port} -> {UPSTREAM}")
print("[zen-proxy] Ctrl+C 停止。")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[zen-proxy] 已停止")
if __name__ == "__main__":
main()
说明:
x-opencode-session/x-opencode-request每次都生成唯一值,避免多个请求共用同一个 id 被服务端限流池计数。
第 2 步:启动代理并自测
python3 /home/你的用户名/zen-proxy.py --port 8788
另开一个终端自测:
curl -s --max-time 60 "http://127.0.0.1:8788/v1/chat/completions" \
-H "Authorization: Bearer public" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-v4-flash-free","messages":[{"role":"user","content":"say ok"}],"max_tokens":20}'
返回 200 且带 choices 字段即成功:
{"id":"router-xxx","object":"chat.completion","model":"deepseek-v4-flash-free",
"choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","content":"ok"}}], ...}
第 3 步:配置 VS Code 自定义端点
编辑 VS Code 用户配置文件 chatLanguageModels.json:
打开方式:
Ctrl+Shift+P→ 输入 Manage Language Models → 添加 Custom Endpoint;或直接编辑
~/.config/Code/User/chatLanguageModels.json
在数组里加入:
{
"name": "ZEN",
"vendor": "customendpoint",
"apiKey": "public",
"apiType": "chat-completions",
"models": [
{
"id": "deepseek-v4-flash-free",
"name": "zen/deepseek-v4-flash-free",
"url": "http://127.0.0.1:8788/v1",
"toolCalling": true,
"vision": true,
"maxInputTokens": 200000,
"maxOutputTokens": 4096,
"thinking": true,
"supportsReasoningEffort": ["low", "medium", "high", "max"],
"reasoningEffortFormat": "chat-completions"
}
],
"settings": {
"deepseek-v4-flash-free": {
"reasoningEffort": "max"
}
}
}
字段含义:
| 字段 | 值 | 说明 |
|---|---|---|
apiKey | public | ZEN 免费节点的公共令牌,无需注册 |
url | http://127.0.0.1:8788/v1 | 指向本地代理(注意不要写 https://opencode.ai) |
apiType | chat-completions | 走 Chat Completions 协议 |
thinking | true | 声明模型支持推理 |
supportsReasoningEffort | ["low","medium","high","max"] | 在模型选择器里显示 Thinking Effort 四级 |
reasoningEffortFormat | chat-completions | 以顶层 reasoning_effort 字段发送 |
settings(provider 级) | reasoningEffort: "max" | 默认推理努力,新会话/重开不重置 |
第 4 步:重载 VS Code 并验证
Ctrl+Shift+P→ Developer: Reload Window- 在 Chat 输入框的模型选择器里选
zen/deepseek-v4-flash-free - 模型旁的
>箭头打开 Thinking Effort 子菜单,可切换 Low/Medium/High/Max - 随便发一条消息,能正常回复即成功
验证底层是否真的带了 reasoning_effort:临时在代理里加一行日志(转发前打印请求体),应能看到:
{"model": "deepseek-v4-flash-free", "reasoning_effort": "max", ...}
五、(可选)配置开机自启
用 systemd 用户服务托管代理:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/zen-proxy.service << 'EOF'
[Unit]
Description=ZEN free model proxy (opencode.ai)
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/你的用户名/zen-proxy.py --port 8788
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now zen-proxy
说明:
enable后登录桌面即自动启动,无需手动操作;- 如果希望"开机未登录也运行"(如远程 SSH 使用),需额外执行
sudo loginctl enable-linger 你的用户名,一般场景不需要; - 常用管理命令:
systemctl --user status zen-proxy、systemctl --user restart zen-proxy、systemctl --user stop zen-proxy。
六、常见问题 FAQ
Q1:为什么直接配 https://opencode.ai/zen/v1 会被 429?
服务端按请求头识别客户端,裸请求 / 非 opencode UA 的请求全部丢进共享限流池。必须通过代理补 User-Agent: opencode/... 和 x-opencode-* 头。
Q2:为什么不用 requestHeaders 在 VS Code 里直接加 User-Agent?
VS Code 的 Custom Endpoint 会把 User-Agent 等保留头从 requestHeaders 中过滤掉,配置了也不会发出去,这是源码层面的安全限制。
Q3:apiKey 用 public 安全吗?
公开节点就是这样设计的,无私有密钥;即便用私有 sk- 密钥,不满足头指纹条件同样 429,所以没有意义。
Q4:免费额度用完了怎么办?
FreeUsageLimitError 是全局共享限流,等一段时间(几分钟到几小时)会自动恢复;代理每次生成新的 session/request id 就是为了尽量避开按 id 的计数。
Q5:模型列表里还有其他免费模型吗?
可以请求 http://127.0.0.1:8788/v1/models 查看当前节点支持的所有模型(包含付费与免费),免费模型一般以 -free 结尾。
七、小结
一句话总结整个方案:
VS Code 无法自定义 User-Agent,而 ZEN 免费节点只认"opencode 客户端指纹"——通过一个 30 行的本地 Python 代理补头转发即可绕过 429,把 deepseek-v4-flash-free 免费模型接入 Copilot Chat,还支持 Thinking Effort(Low/Medium/High/Max)四级推理。
本方案实测:请求全部 200,推理档位 max 在底层请求中确认生效,代理 systemd 托管开机自启,无日志文件产生。
完整指南&spm=1001.2101.3001.5002&articleId=163800057&d=1&t=3&u=974bae00f7f24223a016bcaa066e31e0)
269

被折叠的 条评论
为什么被折叠?



