One-step setup for using Doubleword in opencode via ocx. Installs:
doubleword: realtime provider on Doubleword's Anthropic-compatible Messages endpoint (/v1/messages, wired with@ai-sdk/anthropicso prompt caching works).doubleword-flex: the same models on the flex (async) tier (service_tier: flex,/v1/chat/completions, OpenAI-compatible). Slower and cheaper. Works as a normal model because flex now streams SSE. No prompt caching on this tier.small_modelpinned to realtime, so title/summary stays fast and only your answers pay flex latency.doubleword_async: an MCP tool that runs a prompt on the flex tier and returns the result (fire-and-forget async from any model/agent).dw-flex: a background subagent pinned to the flex tier for non-urgent work.
No API key is stored in this repo. Each user supplies their own via DOUBLEWORD_API_KEY
(resolved at runtime with opencode's {env:...} substitution).
- opencode installed
curl -fsSL https://opencode.ai/install | bash - ocx:
npm i -g ocxorbun i -g ocx python3(the MCP tool is stdlib-only with no pip installs)- A Doubleword API key:
export DOUBLEWORD_API_KEY=sk-...
Run the registry as a container, install it into opencode, done. The container is only needed during install. The components get copied into your config.
git clone <this-repo> && cd doubleword-opencode-ocx
export DOUBLEWORD_API_KEY=sk-...
# 1. serve the registry locally
docker compose up -d --build
# 2. install into opencode
ocx init --global # This is only necessary for the first time
ocx registry add http://localhost:8077 --name dw --global
ocx add dw/doubleword --global # providers + flex + MCP tool + agent
# 3. stop the container (files are now in ~/.config/opencode)
docker compose down
# 4. use it
# Either you can just run opencode and navigate the menus to choose a model (select /models to see options)
opencode
# or you can initialise it with a model when you start it, like this example:
opencode --model doubleword/moonshotai/Kimi-K2.6 # realtime (fast)
opencode --model doubleword-flex/moonshotai/Kimi-K2.6 # flex (async, cheaper)You installed with --global, so every ocx command below needs -g/--global. Its
lockfile lives in ~/.config/opencode, not in this repo. (Without it you get
No ocx.jsonc found in .opencode/ or project root.)
# 1. rebuild the static registry from your edited files/ + registry.jsonc
ocx build . --out dist
# 2. rebuild + restart the container so it serves the new dist
docker compose up -d --build
# 3. update the installed FILE-backed components (the MCP tool .py + the agent .md).
# --all reconciles every changed component without you needing to paste hashes.
ocx update --all --global
# 4. re-apply the bundle's config (provider / models / small_model).
# The `doubleword` bundle has no files, so step 3 never touches it. Only `add`
# rewrites those blocks in opencode.jsonc. Run this whenever you change a provider,
# add/remove a model, or change small_model.
ocx add dw/doubleword --global
# 5. stop the registry again
docker compose down
# 6. refresh opencode's model cache if you added/changed models
opencode --refreshocx updatevsocx add.updatere-copies changed files (thedoubleword-async-tool.pyanddoubleword-flex-agent.md).addre-writes the config blocks inopencode.jsonc(provider, models,small_model, MCP server entry). File changes →update; config changes →add. When in doubt, run both (update first, then add).- The
No ocx.jsonc founderror. You ran anocx update/ocx addwithout--global. ocx then looks for a project-local lockfile in the current directory and fails. Add-g. - The hash command ocx prints. When a file component changed,
ocx addrefuses it and printsUse 'ocx update http://localhost:8077::dw/...@sha256:<old-hash>'. That suggestion omits--global. Running it verbatim is exactly what triggers the error above. Either append--globalto it, or just skip it and runocx update --all --global. timeoutin the MCP block is dropped. opencode's MCP schema has notimeoutfield, soocx buildstrips it, so it never reachesopencode.jsonc. Don't rely on it.- Sanity check that an update landed (installed copy should match what the container serves):
shasum -a 256 ~/.config/opencode/tools/doubleword-async/dw_async_mcp.py curl -s http://localhost:8077/components/doubleword-async-tool/mcp/dw_async_mcp.py | shasum -a 256
In chat you can also fire an async job from any model:
Use the doubleword_async tool to summarise these notes: ...
That's the whole setup. Afterwards it's just opencode. Models included:
moonshotai/Kimi-K2.6, zai-org/GLM-5.2-FP8 (add more in registry.jsonc); use only models
deployed on your Doubleword account. Other install methods (no Docker, hosted registry, sandbox)
are below.
- Realtime: instant streaming, as usual.
- Flex: a pause (≈seconds when the queue is empty, up to ~60s when busy), then the whole answer at once. It's SSE-framed but buffered, not token-by-token. Best for non-urgent work.
The realtime doubleword provider uses @ai-sdk/anthropic because that wiring turns prompt
caching on. opencode only adds cache_control breakpoints for providers it treats as
Anthropic-shaped. With @ai-sdk/openai-compatible it sends no markers and nothing is cached.
Measured with opencode 1.18.29 through a logging proxy:
| Wiring | Cold call | Warm call |
|---|---|---|
@ai-sdk/openai-compatible |
0 cached | 0 cached |
@ai-sdk/anthropic |
wrote 7407 | read 7407 |
- Auth is unchanged. opencode resolves the key from
options.apiKey, anenventry or/connectbefore it builds the client. - TTL. opencode leaves
ttlunset so the API default applies. On 1.18.29 or later a model'soptionscan setcacheControlwith attlof"5m"or"1h". Provider-leveloptionsignore it. - Floor. The prefix must clear the model's minimum, which is 1024 tokens on most models.
- Flex.
doubleword-flexstays on@ai-sdk/openai-compatibleand does not cache. - MCP tool.
dw_async_mcp.pysends one-shot prompts with no shared prefix, so it is uncached. ANTHROPIC_API_KEY. If no Doubleword key resolves,@ai-sdk/anthropicfalls back to it and fails with a confusingAPIError. This registry pinsapiKey, so that cannot happen here.
ocx installs from an http/https URL only (it rejects local paths and file://), so the
registry must be served over HTTP, but only for the moment of install. The components are copied
into your opencode config, so you stop the server right after (as in Quick start). The Quick start
uses Docker Compose; the alternatives below do the same thing differently.
ocx build . --out dist # if dist/ isn't already built
( cd dist && python3 -m http.server 8077 ) & # or: go run . (serves dist/ on :8077)
export DOUBLEWORD_API_KEY=sk-...
ocx init --global
ocx registry add http://127.0.0.1:8077 --name dw --global
ocx add dw/doubleword --global
pkill -f "http.server 8077" # stop the server
opencode --model doubleword-flex/moonshotai/Kimi-K2.6docker build -t doubleword-ocx .
docker run -d -p 8077:8077 --name doubleword-ocx doubleword-ocx # then ocx registry add ... as above
docker rm -f doubleword-ocx # stop when done- Change the port:
PORT=9000 docker compose up -d(or-e PORT=9000 -p 9000:9000on plain docker). ocx update/ocx verifyre-contact the registry URL, so restart the server for those.- First
docker buildcan hit a transientDeadlineExceededpullinggolang:1.23-alpinefrom Docker Hub. Rundocker pull golang:1.23-alpineonce, then rebuild.
Prefix the ocx + opencode commands with HOME=/tmp/dw-try so everything installs into a
throwaway /tmp/dw-try/.config/opencode instead of your real one:
( cd dist && python3 -m http.server 8077 ) &
export DOUBLEWORD_API_KEY=sk-...
HOME=/tmp/dw-try ocx init --global
HOME=/tmp/dw-try ocx registry add http://127.0.0.1:8077 --name dw --global
HOME=/tmp/dw-try ocx add dw/doubleword --global
HOME=/tmp/dw-try opencode --model doubleword-flex/moonshotai/Kimi-K2.6ocx installs from a static URL where index.json is reachable. To make this registry installable
by your team:
- Push this repo to a public GitHub repo (the registry must be reachable by
ocx). ocx build . --out dist.- Host
dist/as static files. Pick one:- Cloudflare Workers:
npm install && npm run deploy(see below). URL becomeshttps://doubleword-opencode-ocx.<subdomain>.workers.dev. - GitHub Pages: enable Pages for the repo, serving
dist/(or commitdist/to agh-pagesbranch). URL becomeshttps://<user>.github.io/<repo>. - Raw URL: commit
dist/and usehttps://raw.githubusercontent.com/<user>/<repo>/main/dist. - Or any host (Vercel / Netlify) serving
dist/.
- Cloudflare Workers:
- Share:
ocx registry add <that-url> --name dw --globalthenocx add dw/doubleword --global.
src/index.ts is a Cloudflare Worker equivalent of main.go: it serves the built dist/ via the
Workers static-assets binding (env.ASSETS, wired in wrangler.jsonc) and logs each request, just
like the Go http.FileServer. Cloudflare doesn't run Go natively, so this is the worker-compatible
path.
npm install
# local dev: serves dist/ on http://localhost:8077 (same as `go run .`)
npm run dev -- --port 8077
# deploy to your Cloudflare account (needs `wrangler login` once)
npm run deployThen install into opencode against the deployed URL:
ocx registry add https://doubleword-opencode-ocx.<subdomain>.workers.dev --name dw --global
ocx add dw/doubleword --globalRebuild dist/ with ocx build . --out dist after editing registry.jsonc / files/, then
re-run npm run deploy (or restart npm run dev) to serve the new bundle.
- MCP tool path: the
doubleword_asyncMCP server is launched from{env:HOME}/.config/opencode/tools/doubleword-async/dw_async_mcp.py. This assumes a global install at the default config location (~/.config/opencode). If you use a customXDG_CONFIG_HOMEor a project-scoped install, edit themcp.doubleword-async.commandpath in youropencode.jsonto point at the installed script. - Config-only flex is the primary path; the MCP tool is complementary (use it to fire a discrete async job from a realtime chat, rather than switching the whole turn to flex).
- Once Doubleword is an official models.dev provider, the realtime provider
block becomes unnecessary (just a key), and the flex provider + MCP tool remain useful. That
entry needs
npm: "@ai-sdk/anthropic"to keep prompt caching. - The
doubleword_asyncMCP tool (files/mcp/dw_async_mcp.py) calls/v1/chat/completionsdirectly withservice_tier: flexand sends nocache_control, so it is uncached by design. It fires one-shot prompts with no shared prefix, so there is nothing for a cache to hit; leave it as is unless it grows a reused system prompt.
registry.jsonc # registry manifest (3 components)
files/mcp/dw_async_mcp.py # async MCP tool (stdlib only)
files/agent/dw-flex.md # flex background subagent
dist/ # built output (generated by `ocx build`)
main.go # Go static server that embeds + serves dist/ (Docker path)
go.mod # (no dependencies, stdlib only)
Dockerfile # self-contained image: docker run -p 8077:8077
docker-compose.yml # docker compose up -d --build
src/index.ts # Cloudflare Worker port of main.go (serves dist/ via ASSETS)
wrangler.jsonc # Worker config: ./dist static-assets binding
package.json / tsconfig.json # Worker deps + TS config (npm run dev / deploy)