jasonxu114514/opencode2api

▲ 186 stars today★ 886⑂ 162

A Go-based OpenCode Zen / Zen Go API proxy with OpenAI & Anthropic API compatibility, key/proxy pooling, automatic routing, and WebUI.

About jasonxu114514/opencode2api

jasonxu114514/opencode2api is an open-source project on GitHub, mainly written in Go. A Go-based OpenCode Zen / Zen Go API proxy with OpenAI & Anthropic API compatibility, key/proxy pooling, automatic routing, and WebUI. It currently holds 886 stars and 162 forks with 0 open issues, and was last pushed on an unknown date (repository created unknown).

Project Overview

AI Homed tracks it on the Today's Trending board, currently at rank #28 with 186 new stars today.

GitHub Repository Details

Repository jasonxu114514/opencode2api · default branch - · size 0 KB · watchers 0 · source: GitHub REST API and repository README

README

opencode2api

English | 简体中文 | Tiếng Việt

A Go gateway for OpenCode Zen and Zen Go. It exposes Chat Completions, Responses, and Anthropic Messages endpoints, translates between their native protocols, and manages upstream keys and proxies.

The executable includes the WebUI. Running the service requires no Node.js runtime or database.

Capabilities

See the management console guide (Chinese) for the new pages and configuration.

Quick start

Download a binary from GitHub Releases, or build with Go 1.24 or newer:

git clone https://github.com/jasonxu114514/opencode2api.git
cd opencode2api
cp config.example.json config.json
go build -o opencode2api ./cmd/opencode2api

Before starting, edit config.json:

1. Replace server_keys with your own local API key. 2. Supply Zen or Go keys. Alternatively, set anonymous: true and empty both upstream key arrays. 3. Replace webui.password. The example enables the WebUI with username admin.

./opencode2api -config config.json

On Windows, use Copy-Item config.example.json config.json, build with go build -o opencode2api.exe ./cmd/opencode2api, then run .\opencode2api.exe -config config.json.

The example listens on 127.0.0.1:8080 for the API and 0.0.0.0:8081 for the WebUI. Open http://localhost:8081 locally. Restrict management access and use an HTTPS reverse proxy when accessing it over a network.

Command-line options:

| Option | Default | Purpose | | ------------- | ------------- | ---------------------------------- | | -config | config.json | Configuration file path. | | -listen | Unset | Override the API listen address. | | -web-listen | Unset | Override the WebUI listen address. |

The configuration directory must be writable for password migration, configuration saves, and model caches.

Docker

The published image is ghcr.io/jasonxu114514/opencode2api.

cp config.example.json config.json

Configure keys and replace the WebUI password before starting.

docker compose up -d docker compose logs -f

Compose imports the host configuration into the opencode2api-state volume only on first startup. Subsequent changes should be made through the WebUI. To import an edited host configuration again:

docker compose cp config.json opencode2api:/var/lib/opencode2api/config.json
docker compose restart

The container runs the service as an unprivileged user. Compose uses a read-only root filesystem, a writable state volume, and a temporary /tmp filesystem.

| Compose variable | Default | Effect | | --------------------------- | -------- | ------------------------------------------------- | | OPENCODE2API_VERSION | latest | Image tag. Pin a release tag for a fixed version. | | OPENCODE2API_PORT | 8080 | Host port mapped to container port 8080. | | OPENCODE2API_WEBUI_PORT | 8081 | Host port mapped to container port 8081. | | OPENCODE2API_LISTEN | Unset | Explicit container API listen override. | | OPENCODE2API_WEBUI_LISTEN | Unset | Explicit container WebUI listen override. |

The last two variables become LISTEN_ADDRESS and WEBUI_LISTEN_ADDRESS inside the container. Empty values defer to the configuration file. While initializing a configuration, the entrypoint changes the example API address 127.0.0.1:8080 to 0.0.0.0:8080 so published ports can reach it.

Changing host ports does not change container listeners. If you change the internal API port, also update the port mapping and the image health check, which uses port 8080.

For a local image:

docker build -t opencode2api:local .
docker volume create opencode2api-state
docker run -d --name opencode2api \
  -p 8080:8080 -p 8081:8081 \
  -e CONFIG_SEED_PATH=/run/config/opencode2api.json \
  -v "$(pwd)/config.json:/run/config/opencode2api.json:ro" \
  -v opencode2api-state:/var/lib/opencode2api \
  opencode2api:local

A proxyfile used in Docker must also be available inside the container at the configured path.

API usage

server_keys authenticate clients to this gateway. They are separate from zen_keys and go_keys and are never used as upstream credentials.

Send Authorization: Bearer YOUR_LOCAL_API_KEY or x-api-key: YOUR_LOCAL_API_KEY. Health checks require no authentication.

| Method | Path | Description | | ------ | ---------------------- | --------------------------------------------------------- | | GET | /v1/models | Models that can be routed with the current configuration. | | POST | /v1/chat/completions | Chat Completions. | | POST | /v1/responses | Responses. | | POST | /v1/messages | Anthropic Messages. | | POST | /v1/systemone | System One structured decisions. | | GET | /healthz | Readiness and resource summary. |

Discover an available model first:

curl http://localhost:8080/v1/models \
  -H "Authorization: Bearer YOUR_LOCAL_API_KEY"

Replace MODEL_ID below with an ID from that response.

Chat Completions

curl http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer YOUR_LOCAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID","messages":[{"role":"user","content":"Hello"}]}'

Responses

curl http://localhost:8080/v1/responses \
  -H "Authorization: Bearer YOUR_LOCAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID","input":"Hello"}'

Anthropic Messages

curl http://localhost:8080/v1/messages \
  -H "x-api-key: YOUR_LOCAL_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID","max_tokens":512,"messages":[{"role":"user","content":"Hello"}]}'

For streaming, add "stream": true to the body and use curl -N. Responses include an x-request-id for correlation. API request bodies are limited to 32 MiB.

System One

curl http://localhost:8080/v1/systemone \
  -H "Authorization: Bearer YOUR_LOCAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID","state":"1 + 1 = 2","questions":{"arithmetic":{"type":"noul","instructions":"Is 1 + 1 equal to 2?"}}}'

System One pairs a free-form state with typed questions and returns typed answers, so the payload shares no shape with the chat/responses bridge and is relayed verbatim, never translated. Only models whose native protocol is System One are served here; a decision payload submitted on a message endpoint (/v1/chat/completions, /v1/responses) is likewise relayed verbatim instead of converted. Streaming upstream replies are relayed as-is without reset recovery.

Compatibility boundaries

Requests using the upstream's native protocol retain provider-specific fields. Requests crossing protocols pass through a common representation; not every option has an equivalent. For example, Chat JSON output constraints and seed are not forwarded to Anthropic Messages. Unsupported content or tool types may be rejected.

The gateway exposes the routes listed above. It does not implement embeddings, file uploads, image generation, or Responses retrieval and cancellation. It does not store conversation history; clients must supply the history or use features supported by the actual upstream.

Routing and models

Model discovery

The gateway refreshes Zen/Go /v1/models and OpenCode's capability catalog on the configured interval. Native protocols and model limits are tracked separately for each tier. OpenCode's Zen/Go documentation is a fallback for protocol discovery.

models.protocols overrides discovery:

{
  "models": {
    "refresh_seconds": 300,
    "protocols": {
      "custom-model": "chat"
    }
  }
}

Allowed protocol values are chat, responses, and anthropic. Models using unsupported native protocols are filtered from discovery unless overridden.

Cost, deprecation, and model limits come from the official OpenCode metadata endpoint, refreshed every 24 hours. OpenCode's capability catalog remains authoritative for context and token limits; metadata limit.context, limit.input, and limit.output fill fields missing from that catalog. These limits are exposed by /v1/models as context_window/context_length, max_input, and max_output for discovery clients such as CLIProxyAPI and Pi. Metadata requests use a 30-second timeout per HTTP client. Refresh failures retain existing data.

Each /v1/models entry also carries cost (USD per million tokens: input, output, cache_read, cache_write, plus OpenClaw's cacheRead/cacheWrite) when the metadata states a price. Reasoning models list their levels in reasoning_efforts and in OpenClaw's compat.supportedReasoningEfforts: minimal through max, capped at the probed ceiling when the model's effort is auto. A forced or probed level is reported as default_reasoning_effort. Cache prices appear after the next metadata refresh.

Anonymous access and fallback

With anonymous: true, either condition makes a model eligible for the anonymous Zen lane:

Eligibility is a routing decision; the upstream can still reject or rate-limit the request. Anonymous requests use public in the upstream authentication header.

The routing sequence is:

1. For an eligible model, try each available anonymous proxy once. 2. Try authenticated tiers in prefer order, using only tiers with a configured key and a route for that model. 3. Apply retry.max_attempts separately to each authenticated tier.

Anonymous attempts are not cut short by retry.max_attempts, but all attempts share the request timeout. Network errors, authentication failures, rate limits, and server errors can rotate keys. Other 4xx responses end the current tier; another available tier may still be tried.

Requests are encoded for each tier's own protocol. With performance.first_event_timeout_seconds enabled, a successful SSE response is held until the first complete data event. A timeout or disconnect before that event uses the existing key/proxy retry budget; comments do not satisfy the timeout. Once released downstream, the stream is never replayed. The default is 0 (disabled); try 15 seconds for upstreams that accept requests but send no events. This bounds the first event, not the first text token, and does not apply to System One. Rotation aliases use their separate rotation.first_output_seconds timeout. A recognized stale Responses reasoning reference can trigger one repair pass; selected-key diagnostics never use that replay.

When only anonymous access is configured, /v1/models exposes only models eligible for that lane.

Free models never need a Zen key: with anonymous: true they are served by the public lane, and a configured key is only their fallback. To keep keys but expose free models only, set models.free_only: true. Paid models then disappear from /v1/models, direct requests for them are rejected, and rotation aliases keep only free members; free models may still fall back to the configured keys.

Free model availability

The gateway probes models whose IDs contain free (case-insensitive), or whose metadata reports zero input/output cost without deprecation, with a short request using its native protocol. It tries the enabled anonymous lane first and then one configured Zen Key. A valid completed response on either lane keeps the model enabled; the model is disabled in discovery and routing only when every attempted lane explicitly reports that the model does not exist or has been retired. Authentication errors, rate limits, timeouts, transport errors, 5xx responses, and malformed replies are inconclusive: they do not create a new disable, and are checked again in one hour. Probe traffic does not alter production key/proxy cooldowns or request statistics. It still reaches the upstream and may consume its free allowance.

System One models use a native noul question asking whether 1 + 1 equals 2; they are never sent chat/tool payloads. Model discovery includes native_protocol and supported_endpoints; rotation aliases are listed only when Go keys and eligible models exist. Legacy v1.3.6 disables are cleared on load and scheduled for immediate recheck because those probes used invalid session IDs.

The intervals are fixed: enabled models are checked every 1 hour, models confirmed unavailable every 24 hours. An inconclusive due recheck of a disabled model preserves its original disable reason and schedules another retry in 24 hours. A successful recheck restores the model automatically. The WebUI Free model availability page can restore it immediately, which puts the model under manual control: it stays enabled and is excluded from automatic probing, so later probe results cannot overwrite it. Use the per-model manual/automatic toggle on the same page to return it to automatic probing; the next scheduler pass then schedules a probe. Records left as manually_enabled by older versions migrate to manual control on load. An in-flight probe cannot undo a manual restore. Models without a usable Zen/anonymous lane, or without a supported inference protocol, are skipped.

Manual control also pauses effort discovery for reasoning.effort=auto; learned levels stop being forced after two hours without verification. Returning to automatic control allows effort discovery to resume.

State survives restarts in config.json..availability.json, separated by the Zen upstream URL. The background scheduler checks for due models once per minute, skips manually controlled models, and probes the rest sequentially, with a 60-second timeout per lane. No channels available means no automatic disable.

Administration endpoints (login required; writes also require CSRF): GET /api/models/availability, POST /api/models/restore with {"model":"MODEL_ID"} (restores and takes manual control), and POST /api/models/manual with {"model":"MODEL_ID","manual":true|false} to switch between manual and automatic control.

Sessions and proxies

Keys are initially spread across proxies. Real traffic can trigger proxy checks, key rebinding, and cooldowns. Unhealthy proxies are rechecked every 15 minutes against Cloudflare trace.

A stable session hash selects the preferred key or anonymous proxy. Send an explicit x-session-id to separate independent conversations. The gateway also accepts x-opencode-session, x-session-affinity, conversation-id, conversation_id, and metadata.session_id.

Without an explicit ID, the first user message is used as a seed. Conversations beginning with the same content can therefore share affinity. Changing pool membership can change the selected node.

Cooldowns grow exponentially up to eight times performance.failure_cooldown_seconds, and a longer Retry-After is honored. If every authenticated key is cooling down, routing may try the key whose cooldown ends first. Anonymous nodes still in cooldown are skipped.

Configuration reference

See config.example.json for a complete starting configuration. JSON supports // and / ... / comments; unknown fields and invalid values are rejected.

Keys, listeners, and routing

| Field | Default / requirement | | --------------------------- | --------------------------------------------------------------------------- | | listen | 127.0.0.1:8080. | | server_keys | At least one local key is required. | | zen_keys, go_keys | At least one upstream key is required unless anonymous access is enabled. | | anonymous | false. | | prefer | go; accepts go or zen. | | upstream.zen | https://opencode.ai/inference/openai. | | upstream.go | https://opencode.ai/zen/go. | | proxies | Falls back to ["direct"] when both proxy sources are empty. | | proxyfile | Optional; relative paths resolve beside the configuration file. | | models.refresh_seconds | 300; minimum 1. | | models.protocols | {}; per-model native protocol overrides. | | models.free_only | false; expose and route only free models, even with Zen or Go keys. | | reasoning.effort | Empty (off); minimal, low, medium, high, xhigh, max, or none. | | reasoning.effort_by_model | {}; per-model overrides of reasoning.effort. |

Forced thinking level

reasoning.effort sets the thinking level used upstream when a request does not state one of its own, and reasoning.effort_by_model overrides it for the model IDs it names. It applies to all three protocols, in either direction of conversion, and matters most for cross-protocol traffic: an Anthropic thinking.budget_tokens can only be approximated by the Chat and Responses surfaces, so a forced level reaches a rung that the budget alone cannot express.

{
  "reasoning": {
    "effort": "high",
    "effort_by_model": { "claude-opus-5": "max" }
  }
}

A level a client states explicitly always wins, so this only replaces a level nobody asked for: an absent one, or one derived from thinking.budget_tokens. Use "none" to remove reasoning from requests that do not ask for it.

Levels are reported faithfully in both directions. An explicit output_config.effort survives conversion instead of being shadowed by a thinking block, budget_tokens keeps its rung (8192 is high, 32000 is xhigh, so the two stay distinguishable), and the Responses target carries the level as reasoning.effort rather than dropping it.

The WebUI Configuration Center exposes the same default and per-model fields. The management API returns them as reasoning from GET /api/config and accepts the object in PUT /api/config; clients that omit the field keep the existing value, while sending an empty object clears it.

Proxy entries accept direct, http://, https://, socks5://, and socks5h://, including URL credentials. The inline list is merged with proxyfile and deduplicated in order.

A proxy file contains one entry per line and supports blank lines and comments:

# Primary proxy
http://user:password@127.0.0.1:7890
socks5://127.0.0.1:1080  # Backup proxy
direct

Comment markers are #, ;, and // at the start of a line or after whitespace.

Rotating proxy pools

Suffix an entry with #pool (or prefix it with pool+) to declare that the endpoint is a rotating proxy pool such as Resin, where every new TCP connection exits from a different IP:

http://127.0.0.1:7890#pool
socks5://127.0.0.1:1080#pool
pool+http://127.0.0.1:7891

A pool entry is handled differently from a static proxy:

| Behavior | Static proxy | #pool entry | | ------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Connection reuse | Keep-alive plus HTTP/2 multiplexing (performance.* pool settings) | A fresh TCP + CONNECT tunnel per request (DisableKeepAlives, no HTTP/2) | | Failure cooldown | 15s–120s exponential backoff, inflated by Retry-After | None: the next request simply draws a new exit IP | | Health eviction | Marked unhealthy, keys rebound, rechecked every 15 minutes | Never evicted; the health check is skipped entirely | | Anonymous lane retries | At most one attempt per configured proxy | Up to retry.max_attempts, cycling the pool entry |

Use it when the endpoint behind the URL already performs the rotation and the pool's own exit IPs are individually rate-limited or flaky; the gateway then treats each attempt as a fresh endpoint instead of pinning the conversation to one exit. A single #pool entry is enough — you no longer need to list dozens of exits. Static entries keep the pooling behavior described below.

Timeouts and connection pools

| Field | Default | Meaning | | --------------------------------------- | ------- | ------------------------------------------------------ | | retry.max_attempts | 3 | Attempts per authenticated tier, including the first. | | retry.timeout_seconds | 300 | Total inference timeout, including stream consumption. | | performance.attempt_timeout_seconds | 0 | Whole-attempt bound; 0 uses the request timeout. | | performance.connect_timeout_seconds | 5 | Connection establishment timeout. | | performance.failure_cooldown_seconds | 15 | Base failure cooldown. | | performance.max_idle_conns | 2048 | Idle connection limit per proxy transport. | | performance.max_idle_conns_per_host | 256 | Idle connection limit per host and transport. | | performance.max_conns_per_host | 0 | Connection limit per host; 0 is unlimited. | | performance.idle_conn_timeout_seconds | 120 | Idle connection lifetime. |

The per-attempt header timeout is capped by the request timeout. Set it below the total timeout

GitHub Stars & Activity

886Stars
162Forks
0Open issues
GoLanguage

GitHub Popularity

GitHub stars886
Forks162
Open issues0
Primary languageGo
License-
Stars gained today186
Created-
Last pushed-

Trending History

Daily boardrank #28 · ▲ 186 stars

Related AI Projects

1

JuliusBrussee / caveman

Go★ 113,045⑂ 6,541▲ 943 stars
→
2

router-for-me / CLIProxyAPI

Go★ 55,442⑂ 8,479▲ 112 stars
→
3

Wei-Shaw / sub2api

Go★ 44,213⑂ 9,468▲ 115 stars
→
4

Tencent / WeKnora

Go★ 34,380⑂ 4,586▲ 207 stars
→
5

vxcontrol / pentagi

Go★ 30,715⑂ 4,675▲ 339 stars
→
6

bytebase / bytebase

Go★ 14,579⑂ 997▲ 9 stars
→
7

Gentleman-Programming / engram

Go★ 7,337⑂ 764▲ 104 stars
→
8

tbphp / gpt-load

Go★ 7,139⑂ 793▲ 38 stars
→

More AI Rankings