用 opencode-llm-proxy 把模型接到本地 OpenAI 接口

摘要

  • 插件仓库:KochC/opencode-llm-proxy,npm 包名 opencode-llm-proxy。本文按插件 1.11.2、OpenCode 2.1.2 来写。
  • 它不是单独启动的服务。OpenCode 2.x 的后台服务加载插件后,在本机 127.0.0.1:4010 提供 OpenAI、Anthropic、Gemini 兼容接口,模型用的是 OpenCode 里已经登录或配置好的那些。
  • OpenCode 2.x 本身的安装、后台服务和插件命令见 opencode 2.x 的安装和使用。

外部工具通常只认一种接口,例如 OpenAI 的 POST /v1/chat/completions。OpenCode 里已经接好的模型,并不能直接给这些工具用。opencode-llm-proxy 把这件事接起来:请求打到本机端口,插件在 OpenCode 里开一个临时会话去调模型,再把回复翻成调用方原来的格式。

支持的入口:

格式 路径
OpenAI Chat Completions POST /v1/chat/completions
OpenAI Responses POST /v1/responses
Anthropic Messages POST /v1/messages
Gemini POST /v1beta/models/<模型>:generateContent

四条都支持流式输出,也支持把 tools 交回调用方执行。插件跑在 OpenCode 进程里,用的是 Bun.serve,本机不需要再起一个 Node 服务。

安装

下面两种方式互相独立,按需要任选其一。两种一起用时,后台服务会加载两份插件。

插件包

opencode plugin add 把包名写进 ~/.config/opencode/opencode.json 的 plugins,并从 npm 仓库下载到 OpenCode 的缓存目录:

1
opencode plugin add opencode-llm-proxy

缓存路径是:

1
~/.cache/opencode/npm/opencode-llm-proxy@latest/<安装时间戳>/node_modules/opencode-llm-proxy

也可以自己改配置、写上同样的包名,效果相同。

1
2
3
4
5
6
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
"opencode-llm-proxy"
]
}

opencode plugin remove 只从配置里去掉包名。这份缓存会留着,除非手动删除,或执行 opencode uninstall。插件仍在配置里时,opencode plugin update 会再装一版;它只保留最新的两代,更老的一代还要超过 7 天才删。

本地文件

把仓库里已经打包好的 dist/llm-proxy.js 放到 OpenCode 会自动加载的插件目录。文件留在原处,不进入上面的 npm 缓存。

全局生效:

1
2
3
mkdir -p ~/.config/opencode/plugins
curl -fsSL -o ~/.config/opencode/plugins/llm-proxy.js \
https://raw.githubusercontent.com/KochC/opencode-llm-proxy/main/dist/llm-proxy.js

只在当前项目生效时,放到该项目的 .opencode/plugins/llm-proxy.js。和 opencode.json 平级的 plugins/ 不会自动加载,需要写进 plugins 配置,或挪到 .opencode/plugins/。

重启并确认

两种方式装完都要重启。2.x 的后台服务会一直留着,已经在跑的进程不会因为刚装好就打开端口:

1
2
opencode service restart
opencode service status

确认端口和模型列表:

1
2
curl -s http://127.0.0.1:4010/health
curl -s http://127.0.0.1:4010/v1/models | jq '.data[].id'

健康检查返回:

1
{ "healthy": true, "service": "opencode-openai-proxy" }

/v1/models 把 OpenCode 当前能看到的模型列成 provider/model。这一步只读配置,不会真正请求模型,所以列得出来不代表这个模型现在能调用成功。

服务日志里对应的一行是 OpenAI proxy server started,路径一般是 ~/.local/share/opencode/log/opencode.log。端口被占用时,OpenCode 本身仍会起来,插件只记一条 OpenAI proxy server failed to start,然后这次不监听。

调用

模型 id 用上面列出来的 provider/model。本地回环、又没有设置令牌时,Authorization 可以不传,SDK 里的 apiKey 填任意非空字符串即可,常见写法是 unused。

1
2
3
4
5
6
curl http://127.0.0.1:4010/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "opencode/deepseek-v4.1-flash-free",
"messages": [{"role": "user", "content": "Hello!"}]
}'

流式请求加上 "stream": true,响应是 text/event-stream,以 data: [DONE] 结束。

Anthropic SDK 的 base URL 是代理根地址,不要带 /v1,SDK 自己会拼 /v1/messages:

1
2
3
4
5
6
import anthropic

client = anthropic.Anthropic(
base_url="http://127.0.0.1:4010",
api_key="unused",
)

OpenAI SDK 则要带到 /v1:

1
2
3
from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:4010/v1", api_key="unused")

Open WebUI 的 OpenAI API Base URL 填 http://127.0.0.1:4010/v1。Chatbox 的 API Host 填 http://127.0.0.1:4010。n8n、Continue、Zed 的具体字段见仓库里的 recipes。

同名模型落在多个提供商上时,加请求头 x-opencode-provider 指定提供商。变体用 x-opencode-variant。

每次请求都会在 OpenCode 里建一个临时会话,用完就删。插件不记住上一轮,多轮对话要把完整 messages 再发一遍。想留下这些会话时,给后台服务加上 OPENCODE_LLM_PROXY_KEEP_SESSIONS=true。

带 tools 时,插件把模型决定调用的函数翻译成 OpenAI 的 tool_calls、Anthropic 的 tool_use 或 Gemini 的 functionCall,然后结束这一轮。函数本身在调用方执行,结果按各自协议放进下一轮请求。OpenCode 自带的读文件、跑命令等工具不会在这条请求里执行。

配置

配置全是环境变量,由后台服务进程读取。在当前终端里 export 之后再执行 opencode,改不到已经在跑的那个服务。写进服务配置:

1
2
3
opencode service set env OPENCODE_LLM_PROXY_PORT 4020
opencode service get env
opencode service start

service set 和 service unset 会停掉正在跑的服务,新值在下一次启动时生效。改完用 opencode service status 看一下,停着就再 start。

常用项:

变量 默认 作用
OPENCODE_LLM_PROXY_HOST 127.0.0.1 监听地址。局域网用 0.0.0.0
OPENCODE_LLM_PROXY_PORT 4010 端口
OPENCODE_LLM_PROXY_TOKEN 空 Bearer 令牌。监听非回环地址时必须设置
OPENCODE_LLM_PROXY_TOKENS [] 额外接受的令牌,JSON 数组
OPENCODE_LLM_PROXY_REQUEST_TIMEOUT_MS 120000 单次请求超时
OPENCODE_LLM_PROXY_MAX_REQUEST_BYTES 1048576 请求体上限,最大 100 MiB
OPENCODE_LLM_PROXY_KEEP_SESSIONS false true 时保留临时会话
OPENCODE_LLM_PROXY_MODEL_ALIASES {} 别名到模型 id,值可以是字符串或按顺序回退的数组
OPENCODE_LLM_PROXY_METRICS_ENABLED false true 时开放 GET /metrics,同样要带令牌

别名示例:

1
opencode service set env OPENCODE_LLM_PROXY_MODEL_ALIASES '{"fast":"opencode/deepseek-v4.1-flash-free"}'

请求里的 model 就可以写 fast。

暴露到局域网时同时改地址和令牌,否则插件拒绝启动:

1
2
3
opencode service set env OPENCODE_LLM_PROXY_HOST 0.0.0.0
opencode service set env OPENCODE_LLM_PROXY_TOKEN '换成一长串随机字符串'
opencode service start

调用时带 Authorization: Bearer <令牌>。Docker 里的 Open WebUI 访问宿主机时,Base URL 用 http://host.docker.internal:4010/v1,并确认系统防火墙放行这个端口。

temperature、top_p、topK 会传给模型。max_tokens 一类字段会收下,避免客户端因缺字段报错,但当前 OpenCode SDK 不会按这个上限截断。stop、seed、frequency_penalty、presence_penalty、logprobs、n 会直接返回 400,不会悄悄忽略。

远程图片 URL 默认拒绝。确有需要再打开 OPENCODE_LLM_PROXY_REMOTE_MEDIA_ENABLED,并读仓库的 Security。完整变量表以仓库 README 为准。

排错

curl 连不上 127.0.0.1:4010 时,先看服务在不在、插件有没有写进配置:

1
2
opencode service status
opencode plugin list

服务是 stopped 就 opencode service start。plugin list 里没有 opencode-llm-proxy,就重新 plugin add,然后 opencode service restart。

模型列表正常,聊天却返回:

1
{"error":{"message":"Upstream request failed.","type":"invalid_request_error"}}

这是插件把上游错误收成了一句通用文案。具体原因在 OpenCode 日志里,例如:

1
2
grep -E "Failed to drain Session|Proxy completion failed|Free usage limit" \
~/.local/share/opencode/log/opencode.log | tail

看到 Free usage limit reached 时,OpenCode Zen 的免费额度已经用完。/v1/models 仍会列出 opencode/*-free,因为列表不检查额度。换成自己在 providers 里配置的模型,例如 my-deepseek/deepseek-v4-flash,或者等额度重置。额度由 Zen 账号计算,在 OpenCode 终端里直接用同一个免费模型也会失败。近期有人反馈重置时间到了仍然被拦,这种情况写信给 help@anoma.ly,不要在公开场合贴 key。

api_key_expired 是另一件事,按 opencode 2.x 的安装和使用 里「免费模型报 API key 过期」换 Console 的 oc_sk_ key,然后 opencode service restart。

卸载

从全局配置去掉插件:

1
opencode plugin remove opencode-llm-proxy

如果还把 llm-proxy.js 拷进了插件目录,删掉这些文件,否则服务重启后仍会加载:

1
2
~/.config/opencode/plugins/llm-proxy.js
<项目>/.opencode/plugins/llm-proxy.js

改过监听地址、端口或令牌时,把对应变量清掉:

1
2
3
opencode service unset env OPENCODE_LLM_PROXY_HOST
opencode service unset env OPENCODE_LLM_PROXY_PORT
opencode service unset env OPENCODE_LLM_PROXY_TOKEN

正在运行的服务里,插件已经加载,端口还会继续听。卸完后重启,再确认端口已经关掉:

1
2
opencode service restart
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4010/health

连接被拒绝就说明代理已经停了。~/.cache/opencode/npm/ 里可能还留着安装缓存,不影响端口;下次再 plugin add 时会重新用到它。要换新版本用 opencode plugin update,然后同样重启服务。

后台服务本身可以继续留着。只有不再使用 OpenCode 时才需要 opencode service stop,那是服务的生命周期,不是这个插件的卸载步骤。