Install
$ agentstack add mcp-kochc-opencode-llm-proxy ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README — it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
opencode-llm-proxy
[](https://www.npmjs.com/package/opencode-llm-proxy) [](https://www.npmjs.com/package/opencode-llm-proxy) [](https://github.com/KochC/opencode-llm-proxy/actions/workflows/ci.yml) [](https://opensource.org/licenses/MIT)
One local endpoint. Every model you have access to. Any API format. Tool calling included.
opencode-llm-proxy is an OpenCode plugin that starts a local HTTP server on http://127.0.0.1:4010. It translates between the API format your tool speaks and whichever LLM provider OpenCode has configured — so you never reconfigure the same models twice.
Your tool (OpenAI / Anthropic / Gemini SDK, coding agent, etc.)
│
▼ http://127.0.0.1:4010
opencode-llm-proxy
│
▼ OpenCode SDK
GitHub Copilot · Anthropic · Gemini · Ollama · OpenRouter · Bedrock · …
Supported API formats — all with streaming and [tool/function calling](#tool-calling):
| Format | Endpoint | |---|---| | OpenAI Chat Completions | POST /v1/chat/completions | | OpenAI Responses API | POST /v1/responses | | Anthropic Messages API | POST /v1/messages | | Google Gemini | POST /v1beta/models/:model:generateContent |
✨ Tool calling works with all four formats — point a coding agent (Claude Code, Cursor, Continue, Cline, your own agent loop, ...) at the proxy and its tools/tool_choice calls are translated through to whatever model OpenCode has configured, with a real tool_calls / tool_use / functionCall response handed back. See [Tool calling](#tool-calling).
Contents
- [Why](#why)
- [Quickstart](#quickstart)
- [Install](#install)
- [Configuration](#configuration)
- [Tool calling](#tool-calling)
- [Using with SDKs and tools](#using-with-sdks-and-tools)
- [n8n](#n8n)
- [Finding model IDs](#finding-model-ids)
- [API reference](#api-reference)
- [How it works](#how-it-works)
- [Limitations](#limitations)
- [License](#license)
Why
Most LLM tools speak exactly one API dialect. OpenCode already manages connections to every provider you use. This proxy bridges the two — your tools keep working as-is, and you change which model they use in one place.
Common situations it solves:
- You have a GitHub Copilot subscription. Open WebUI, Chatbox, or a VS Code extension only accepts an OpenAI-compatible URL. Point them at the proxy — done.
- You run Ollama locally. Your Python scripts use the OpenAI SDK. Set
base_urlto the proxy and use your Ollama model IDs directly. - You want to swap models without code changes. Your app talks to the proxy; you change the model in OpenCode config.
- You want to share your models on a LAN. Expose the proxy on
0.0.0.0and give teammates the URL. - You use the Anthropic SDK but want to route through GitHub Copilot or Bedrock. No code change in the SDK — just point it at the proxy.
- You're building or running a coding agent that needs real tool/function calling (read files, run shell commands, etc.) against whatever model OpenCode has configured. See [Tool calling](#tool-calling).
- You run n8n (self-hosted or in Docker, possibly on a different machine on your LAN) and want its AI nodes to use whatever models OpenCode already has authenticated access to — GitHub Copilot, Anthropic, Bedrock, local Ollama models, etc. — without giving n8n its own separate API keys. Point n8n's native OpenAI/Anthropic credentials at the proxy. See [n8n](#n8n).
Quickstart
npm install opencode-llm-proxy
Add to opencode.json:
{
"plugin": ["opencode-llm-proxy"]
}
Start OpenCode — the proxy starts automatically:
opencode
Send a request:
curl http://127.0.0.1:4010/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "github-copilot/claude-sonnet-4.6",
"messages": [{"role": "user", "content": "Hello!"}]
}'
Install
npm plugin (recommended)
npm install opencode-llm-proxy
Add to your global ~/.config/opencode/opencode.json (works everywhere) or a project-level opencode.json:
{
"plugin": ["opencode-llm-proxy"]
}
Copy the file
Global — loaded for every OpenCode session:
curl -o ~/.config/opencode/plugins/llm-proxy.js \
https://raw.githubusercontent.com/KochC/opencode-llm-proxy/main/index.js
Per-project — loaded only in this directory:
mkdir -p .opencode/plugins
curl -o .opencode/plugins/llm-proxy.js \
https://raw.githubusercontent.com/KochC/opencode-llm-proxy/main/index.js
> Copying just index.js works for everything except [tool calling](#tool-calling), which also needs mcp-tool-bridge.js alongside it. Use the npm plugin install method if you want tool calling.
Configuration
| Variable | Default | Description | |---|---|---| | OPENCODE_LLM_PROXY_HOST | 127.0.0.1 | Bind address. 0.0.0.0 to expose on LAN or Docker. | | OPENCODE_LLM_PROXY_PORT | 4010 | TCP port. | | OPENCODE_LLM_PROXY_TOKEN | (unset) | Bearer token required on every request. Unset = no auth. | | OPENCODE_LLM_PROXY_CORS_ORIGIN | * | Access-Control-Allow-Origin value for browser clients. | | OPENCODE_LLM_PROXY_TOOL_BRIDGE_POOL_SIZE | 8 | Max concurrent in-flight requests using [tool calling](#tool-calling). |
OPENCODE_LLM_PROXY_HOST=0.0.0.0 \
OPENCODE_LLM_PROXY_TOKEN=my-secret \
opencode
Tool calling
The proxy supports real tool/function calling on all four API formats — OpenAI function tools (tools on /v1/chat/completions and /v1/responses), Anthropic tools (tools on /v1/messages), and Gemini function declarations (tools on :generateContent/:streamGenerateContent). This is what lets coding agents and other tool-using clients work through the proxy, not just plain chat.
curl http://127.0.0.1:4010/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "github-copilot/claude-sonnet-4.6",
"messages": [{"role": "user", "content": "What is the weather in NYC?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}]
}'
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_...",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"NYC\"}" }
}]
}
}]
}
Send the tool's result back on your next request (role: "tool" / tool_result / functionResponse, per your API's convention) alongside the full conversation history, same as any other multi-turn request — the proxy is stateless between calls either way.
How tool calling works under the hood
OpenCode's own agent loop always executes tools itself, server-side, so there's no native concept of a "client-executed" tool call to hand off to. To bridge that gap, when a request includes tools:
- The proxy dynamically registers a small local MCP server whose tool list is exactly your declared tool schemas (see
mcp-tool-bridge.js). - Only those tools are enabled for that one prompt call — every built-in OpenCode tool stays disabled, same as always.
- As soon as the model proposes calling one of your tools, the proxy immediately aborts the OpenCode session (before the bridge's no-op handler is ever consulted) and translates the captured call name + arguments into your API's tool-call shape —
tool_calls(OpenAI),tool_use(Anthropic), or afunctionCallpart (Gemini) — instead of a text answer.
Notes and current limitations
- One tool call per turn — parallel/multiple simultaneous tool calls aren't supported.
tool_choice: "none"(OpenAI/Geminimode: "NONE"/Anthropictype: "none") disables tool calling for that request; forcing a specific named tool is supported.- Bridge servers are reused from a small fixed-size pool (
px_tools_0,px_tools_1, ...) rather than registered fresh per request, since OpenCode's server API has no endpoint to deregister an MCP server once added. Configure the pool size withOPENCODE_LLM_PROXY_TOOL_BRIDGE_POOL_SIZE(default8) if you expect more than 8 concurrent in-flight tool-calling requests. - The bridge process is spawned with
node, sonodemust be onPATHwherever OpenCode is running.
Using with SDKs and tools
OpenAI SDK (JS/TS)
import OpenAI from "openai"
const client = new OpenAI({
baseURL: "http://127.0.0.1:4010/v1",
apiKey: "unused",
})
const response = await client.chat.completions.create({
model: "github-copilot/claude-sonnet-4.6",
messages: [{ role: "user", content: "Explain recursion." }],
})
OpenAI SDK (Python)
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:4010/v1", api_key="unused")
response = client.chat.completions.create(
model="ollama/qwen2.5-coder",
messages=[{"role": "user", "content": "Write a Python function to reverse a string."}],
)
print(response.choices[0].message.content)
Anthropic SDK (Python)
import anthropic
client = anthropic.Anthropic(
base_url="http://127.0.0.1:4010",
api_key="unused",
)
message = client.messages.create(
model="anthropic/claude-3-5-sonnet",
max_tokens=1024,
messages=[{"role": "user", "content": "What is the Pythagorean theorem?"}],
)
print(message.content[0].text)
Anthropic SDK (JS/TS)
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic({
baseURL: "http://127.0.0.1:4010",
apiKey: "unused",
})
const message = await client.messages.create({
model: "anthropic/claude-opus-4",
max_tokens: 1024,
messages: [{ role: "user", content: "Explain async/await." }],
})
Google Generative AI SDK (JS/TS)
import { GoogleGenerativeAI } from "@google/generative-ai"
const genAI = new GoogleGenerativeAI("unused", {
baseUrl: "http://127.0.0.1:4010",
})
const model = genAI.getGenerativeModel({ model: "google/gemini-2.0-flash" })
const result = await model.generateContent("What is machine learning?")
console.log(result.response.text())
LangChain (Python)
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="anthropic/claude-3-5-sonnet",
openai_api_base="http://127.0.0.1:4010/v1",
openai_api_key="unused",
)
response = llm.invoke("What are the SOLID principles?")
print(response.content)
Open WebUI
- Settings → Connections → OpenAI API
- Set API Base URL to
http://127.0.0.1:4010/v1 - Leave API Key blank (or set to your
OPENCODE_LLM_PROXY_TOKEN) - Save — all your OpenCode models appear in the model picker
> Running Open WebUI in Docker? Use http://host.docker.internal:4010/v1 and set OPENCODE_LLM_PROXY_HOST=0.0.0.0.
n8n
The proxy lets n8n's native AI nodes use whatever models OpenCode already has authenticated access to — GitHub Copilot, Anthropic, Bedrock, local Ollama models, etc. — without configuring separate API keys in n8n at all. This works with n8n's regular LangChain-based Chat Model nodes, including real tool/function calling (e.g. an "AI Agent" node with a Tool attached) since [Tool calling](#tool-calling) support was added.
- In OpenCode, expose the proxy on your LAN instead of just localhost, and set a bearer token since it'll be network-reachable:
``bash OPENCODE_LLM_PROXY_HOST=0.0.0.0 \ OPENCODE_LLM_PROXY_TOKEN=some-long-random-token \ opencode ``
- In n8n, create a credential:
- OpenAI: Base URL
http://:4010/v1, API Key = your token - Anthropic: Base URL
http://:4010(no/v1— the node adds/v1/messagesitself), API Key = your token
- Add an OpenAI Chat Model (or Anthropic Chat Model) node using that credential. The model dropdown calls
GET /v1/modelson the proxy, so it auto-populates with every model OpenCode has connected (github-copilot/claude-sonnet-5,anthropic/claude-3-5-sonnet,ollama/qwen2.5-coder, ...) — pick one directly, no manual typing needed. - Wire it into a Basic LLM Chain node for simple prompt/response use, or an AI Agent node (with Tools attached, e.g. an HTTP Request Tool) for agentic tool-using workflows.
> n8n running in Docker on a different machine on your LAN (a common setup)? Use that machine's actual LAN IP for ` — not localhost/host.docker.internal, which only resolve to the OpenCode host if Docker is running on that same machine. Make sure your firewall allows incoming connections to the opencode` binary (macOS's Application Firewall in particular will silently drop connections from an app it hasn't been told to allow, even with the port open).
Chatbox
Settings → AI Provider → OpenAI API → set API Host to http://127.0.0.1:4010.
Continue (VS Code / JetBrains)
In ~/.continue/config.json:
{
"models": [
{
"title": "Claude via OpenCode",
"provider": "openai",
"model": "anthropic/claude-3-5-sonnet",
"apiBase": "http://127.0.0.1:4010/v1",
"apiKey": "unused"
}
]
}
Zed
In ~/.config/zed/settings.json:
{
"language_models": {
"openai": {
"api_url": "http://127.0.0.1:4010/v1",
"available_models": [
{
"name": "github-copilot/claude-sonnet-4.6",
"display_name": "Claude (OpenCode)",
"max_tokens": 8096
}
]
}
}
}
Finding model IDs
curl http://127.0.0.1:4010/v1/models | jq '.data[].id'
# "github-copilot/claude-sonnet-4.6"
# "anthropic/claude-3-5-sonnet"
# "ollama/qwen2.5-coder"
# ...
Use provider/model for clarity. Bare model IDs (e.g. gpt-4o) work if unambiguous across your providers.
To force a specific provider without changing the model string, add:
x-opencode-provider: anthropic
API reference
GET /health
{ "healthy": true, "service": "opencode-openai-proxy" }
GET /v1/models
Returns all models from all configured providers in OpenAI list format.
POST /v1/chat/completions
OpenAI Chat Completions. Required fields: model, messages. Optional: stream, temperature, max_tokens, tools, tool_choice.
POST /v1/responses
OpenAI Responses API. Required fields: model, input. Optional: instructions, stream, max_output_tokens, tools, tool_choice.
POST /v1/messages
Anthropic Messages API. Required fields: model, messages. Optional: system (string or array of {type: "text", text: string} content blocks), max_tokens, stream, tools, tool_choice.
Errors are returned in Anthropic format: { "type": "error", "error": { "type": "...", "message": "..." } }.
POST /v1beta/models/:model:generateContent
Google Gemini non-streaming. Model name in URL path. Required field: contents. Optional: systemInstruction, generationConfig, tools, toolConfig.
POST /v1beta/models/:model:streamGenerateContent
Same as above, returns newline-delimited JSON stream.
How it works
Each request:
- Is authenticated if
OPENCODE_LLM_PROXY_TOKENis set - Has its model resolved —
provider/model, bare model ID, or Gemini URL path - Creates a temporary OpenCode session (visible in the session list)
- Sends the prompt via
client.session.prompt/client.session.promptAsync - Returns the response in the same format as the request
Streaming uses OpenCode's client.event.subscribe() SSE stream. Text deltas are forwarded in real time.
Limitations
- Text only — image, audio, and file inputs are ignored
- No cross-request session state — send full conversation history
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: KochC
- Source: KochC/opencode-llm-proxy
- License: MIT
- Homepage: https://www.npmjs.com/package/opencode-llm-proxy
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.