OpenAI-compatible HTTP API in front of opencode serve, using the free model
opencode/muse-spark-1.3-contributor-free. No OPENCODE_API_KEY needed —
reuses whatever auth the opencode CLI already has.
Targets the OpenCode 2 server API (/api/*, basic auth, session-scoped
model/agent, session.text.delta events). OpenCode 1.x servers are detected
and rejected at startup; upgrade with opencode upgrade --major.
Rust implementation (formerly a Node server.js wrapper). Same env vars and HTTP
surface. Not published to npm — use Cargo or a release binary instead of npx.
-
opencodeCLI 2.x installed and authenticated:opencode --version # must report v2.x opencode auth login -
Rust 1.75+ (
rustup/cargo) when installing from source. Prebuilt release binaries do not require Rust.
Server checks for opencode at startup and exits if CLI is missing.
Closest current equivalent to a global npm install:
cargo install --git https://github.com/Fast-Editor/OpenCode-Wrap.git
opencode-wrapOnce opencode-wrap is published to crates.io:
cargo install opencode-wrap
opencode-wrapOnce GitHub Releases are configured with cargo-dist, users can install without
Rust using generated installer:
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/Fast-Editor/OpenCode-Wrap/releases/latest/download/opencode-wrap-installer.sh | shThis installer command requires release workflow and installer asset to exist. Until then, use Cargo-from-GitHub command above.
git clone https://github.com/Fast-Editor/OpenCode-Wrap.git
cd OpenCode-Wrap
cargo install --path . --force
opencode-wrapFrom this repository:
cargo build --release
./target/release/opencode-wrap
# OpenAI-compatible API at http://127.0.0.1:8000/v1Or during development:
cargo run --releaseIf cargo is not found after installing Rust:
export PATH="$HOME/.cargo/bin:$(dirname "$(rustup which rustc)"):$PATH"
cargo run --releaseSet custom wrapper port and working directory:
WRAP_PORT=8080 WRAP_CWD="$PWD" cargo run --releaseSpawns its own opencode serve on :4100 if none is reachable. OpenCode 2
protects its API with HTTP basic auth: the wrapper generates a random password
for the server it spawns, or uses OPENCODE_SERVER_PASSWORD when set. To point
it at an opencode serve you started yourself, export the same
OPENCODE_SERVER_PASSWORD (and OPENCODE_SERVER_USERNAME if you changed it)
before starting the wrapper.
| Env | Default | Meaning |
|---|---|---|
WRAP_PORT |
8000 |
This server's port |
OPENCODE_BASE |
http://127.0.0.1:4100 |
Existing opencode serve URL (trailing / ok) |
OPENCODE_PORT |
4100 |
Port for auto-spawned serve |
OPENCODE_SERVER_PASSWORD |
random per start | Basic-auth password for opencode serve. Required when OPENCODE_BASE points at a server you started yourself |
OPENCODE_SERVER_USERNAME |
opencode |
Basic-auth username for opencode serve |
WRAP_MODEL |
muse-spark-1.3-contributor-free |
Backend model |
WRAP_PROVIDER |
opencode |
Backend provider |
WRAP_AGENT |
build |
OpenCode agent each per-request session runs under |
WRAP_CWD |
/tmp (Unix) / %TEMP% (Windows) |
Existing directory for spawned serve. Native bash tools run here — use an empty sandbox, not your home directory |
WRAP_OCO_TIMEOUT_MS |
180000 |
Per-backend-request timeout |
WRAP_MAX_BODY_BYTES |
8388608 |
Max chat body. Oversize requests return OpenAI-shaped 413 |
WRAP_ZEN_MODELS_URL |
https://opencode.ai/zen/v1/models |
Zen model allowlist |
WRAP_API_KEY |
unset | When set, /v1/* routes require Authorization: Bearer <key> (health stays open) |
WRAP_CORS_ORIGINS |
unset (any) | Comma-separated allowed CORS origins |
WRAP_MAX_IN_FLIGHT |
8 |
Concurrent chat requests before replying 429 |
Works with a native opencode.exe on PATH (official installer, scoop,
etc.). npm installs expose OpenCode as an opencode.cmd shim; the wrapper
finds that too, but then only the shim is killed on shutdown, so the spawned
opencode serve can outlive the wrapper — prefer the native install, or run
opencode serve yourself and point OPENCODE_BASE at it.
cargo testUnit tests cover translation, validation, retries, prepare_turn, the V2
message projection and event filtering. CI also runs cargo fmt --check and
cargo clippy --all-targets -- -D warnings.
GET /health,/v1/health— liveness probe; reports the wrapperversionso you can tell which build is running (opencode-wrap --versiontoo)GET /v1/models— default backend first, then full Zen list (opencode/<id>)POST /v1/chat/completions—model,messages,tools,tool_choice(none/auto/required/{"type":"function","function":{"name"}}),stream, visionimage_urlparts,developerrole,response_format,stop(at most 4 sequences),max_tokens/max_completion_tokens,n=1only.temperature/top_pvalidated but the backend has no per-request sampling knobs.stream: trueforwards backendsession.text.deltaevents as OpenAI SSE chunks and ends withdata: [DONE]. Append#variantto anopencode/<id>model (e.g.opencode/<id>#high) to select an OpenCode 2 model variant.
curl http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"opencode/union-alpha",
"messages":[{"role":"user","content":"say hi"}]}'List models:
curl http://127.0.0.1:8000/v1/modelsHealth check:
curl http://127.0.0.1:8000/healthcurl http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "opencode/muse-spark-1.3-contributor-free",
"messages": [
{
"role": "user",
"content": "Explain Rust ownership in one sentence."
}
]
}'Streaming:
curl -N http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "opencode/muse-spark-1.3-contributor-free",
"stream": true,
"messages": [
{
"role": "user",
"content": "Write a short Rust hello-world program."
}
]
}'curl http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"opencode/muse-spark-1.3-contributor-free",
"messages":[{"role":"user","content":"Explain closures in one paragraph."}]}'
curl http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"opencode/muse-spark-1.3-contributor-free",
"messages":[{"role":"user","content":"Weather in Paris?"}],
"tools":[{"type":"function","function":{"name":"get_weather",
"description":"Get weather for a city",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},
"required":["city"]}}}]}'- Stateless: fresh opencode session per request (
POST /api/sessionwith the model and agent, prompt, wait for idle, read the projected messages,DELETE); history replayed each turn. Very long histories keep the newest turns (about 200k characters) and omit the oldest. system/developermessages (plus the tool and JSON-mode instructions) are attached to the session as an instruction entry, since OpenCode 2 has no per-prompt system field. If that experimental endpoint is unavailable the text is folded into the prompt instead.WRAP_OCO_TIMEOUT_MSbounds the whole run of one request (prompt until the session goes idle), not just a single HTTP call.- Spawned
opencode servelogs go to$WRAP_CWD/opencode-serve.log(mode0600on Unix), not the wrapper process. - File / image parts must be
data:URIs orhttp(s)URLs. OpenCode 2 only accepts inline data, so remote URLs are downloaded by the wrapper (20 s timeout) and inlined. Anything over 4 MB is rejected with400. The MIME type comes from the data URI or theContent-Typeheader, falling back to the file extension. - Opencode-native tools (bash/read/edit) execute server-side automatically.
- Bare
muse-spark-*-contributoraliased to...-contributor-free. - Explicit
opencode/<id>typos → HTTP 400 with suggestions when Zen list loads. - Empty contentless turns retried (fresh session), not served as
stop. - Caller tools use instruction +
tool_callfence parsing; returned as OpenAItool_calls. Undertool_choice:autothe model may answer from knowledge — userequired(or a named tool) to force. - Transient backend failures retried 3×; rate limits → OpenAI-shaped
429.
Personal / light use only. Shared free-tier backend — no bulk jobs or parallel
hammering. Respect upstream Zen / Meta terms. Default 8 MB body cap
(OpenAI-shaped 413 on oversize). Listens on 127.0.0.1 only; it reuses your
opencode CLI credentials, so do not expose it on a network interface.
WRAP_CWD is where spawned tools run — point it at an empty sandbox. Sessions
are per-request and deleted afterward.