opencode-sync
Keep opencode's model list in sync with your local vLLM or llama.cpp server.
When you swap models on your inference server, opencode-sync queries /v1/models and updates opencode.jsonc in place. Run it once manually, or install the shell wrapper so it happens automatically every time you start opencode.
It edits only the model list of the providers it syncs. Your comments, formatting, and every other setting in the file are left byte-for-byte alone.
Requirements
- Python 3.8+
- No runtime dependencies (pure stdlib)
Install
pip install opencode-sync
Or from source:
git clone https://github.com/ozskywalker/opencode-sync
cd opencode-sync
pip install -e .
Usage
Sync against the URL already in your config (never changes the stored URL — safe to run any time):
opencode-sync
Point at a specific server and update the stored URL:
opencode-sync --host llm-server.local --port 8080
Preview changes without writing anything:
opencode-sync --dry-run
Auto-sync on every opencode launch
Install a shell wrapper once:
opencode-sync install
This writes ~/.local/bin/opencode — a small script that runs opencode-sync silently, then execs the real opencode binary. opencode always starts with a fresh model list, and still launches normally if the inference server is unreachable.
Make sure ~/.local/bin is earlier in your $PATH than the real opencode binary. The installer warns you if it isn't.
opencode-sync install [--wrapper PATH] [--opencode-bin PATH] [--force] [--dry-run]
How it works
On each run, opencode-sync:
- Works out which providers to sync (see Which providers get synced)
- Fetches each one's live model list from
GET /v1/models - Rebuilds that provider's
models— surviving IDs keep their entry exactly as you wrote it; new IDs get a generated name from the model ID - If
modelorsmall_modelpoints at this provider and its model is gone, repoints it to the first available one - Rewrites only those parts of the file
The model/small_model fields are written in opencode's expected provider/model-id format (e.g. vllm/org/model-name), so opencode resolves them correctly.
What it won't touch
Everything outside the model lists it syncs. Concretely:
- Comments and formatting. The file is edited by splicing text, not by re-serializing it, so your
//notes, indentation, and blank lines survive exactly. - Other providers. Syncing one provider never modifies another — including
model/small_modelwhen they point somewhere else. - Everything else.
$schema,agent,mcp,keybinds, and any other key stay byte-identical. - The file itself, when nothing changed. A no-op sync doesn't rewrite it at all.
Before writing, it re-parses the result and checks it means exactly what was intended. If that check fails it writes nothing and tells you — it never falls back to reformatting your file. Writes are atomic (temp file + rename), and a .bak is kept whenever content actually changes.
Which providers get synced
| Invocation | Providers synced |
|---|---|
opencode-sync |
Every provider with a baseURL, each against its own URL |
opencode-sync --provider ID |
Just ID |
opencode-sync --host X |
The one matching http://X:8080/v1 |
With no arguments it syncs everything, which is what the wrapper does. If one server is down, the others still sync; the exit code is non-zero only if every one failed. A server that's up but serving no models is skipped — your model list is never wiped because an inference server was mid-restart.
When a server renames a model
If a server starts reporting a different ID for what is really the same model, a naive sync would drop your old entry and everything you'd tuned on it. So:
opencode-sync --provider vllm --rename aeon=qwen3.5-122B-A10B
This moves the entry — display name, context limits, sampling options, comments and all — to the new ID.
A sync that removes exactly one model and adds exactly one is assumed to be a rename, but only when the old entry has settings worth keeping. It'll tell you when it does this:
~ Renamed: aeon -> qwen3.5-122B-A10B (kept limit, options, reasoning, temperature, tool_call)
That's a guess, and a genuine model swap would inherit the wrong settings — use --no-infer-renames to turn it off and drop the old entry instead.
Which model opencode defaults to
opencode resolves its default model in this order: the top-level model key in the config, then its own recent[] state file (~/.local/state/opencode/model.json), then the first model of the first provider. The sync keeps the provider's model list fresh, but by default it never creates a model pointer — after a server-side rename, a config that never had one still won't, and opencode falls through to its (possibly stale) recent list.
--default-model makes that tunable:
# After a model-set change, point config.model at the first served model
opencode-sync --default-model auto
# Never touch model/small_model at all, not even to repair a stale pointer
opencode-sync --default-model none --default-small-model none
# Pin an explicit model (provider-qualified)
opencode-sync --default-model vllm/qwen3.5-122B-A10B
| Mode | Behavior |
|---|---|
first (default) |
Legacy: never invent a pointer; only repair one that already points at the synced provider |
auto |
When this provider's model set changes, point model/small_model at the first served model |
none |
Never touch the keys |
provider/model-id |
Explicit pin; written whenever it isn't already there |
--default-small-model takes the same values and controls small_model independently. A pointer belonging to a different provider is never touched in any mode. auto only fires when the model set actually changed, so routine no-op syncs don't churn the config.
If opencode has recently-used leftovers for models that no longer exist, they can shadow the synced default (opencode skips dead entries rather than cleaning them up):
opencode-sync --prune-recent # drop dead models from ~/.local/state/opencode/model.json
Entries for providers this tool doesn't sync are left alone.
URL behaviour
| Invocation | Server queried | baseURL in config |
|---|---|---|
opencode-sync |
Existing baseURL (fallback: localhost:8080) |
Unchanged |
LLAMA_ARG_HOST=X opencode-sync |
http://X:8080/v1 |
Updated |
LLAMA_ARG_HOST=X LLAMA_ARG_PORT=9000 opencode-sync |
http://X:9000/v1 |
Updated |
opencode-sync --host X |
http://X:8080/v1 |
Updated |
opencode-sync --host X --no-url-update |
http://X:8080/v1 |
Unchanged |
CLI --host/--port values override LLAMA_ARG_HOST/LLAMA_ARG_PORT.
Your customisations are preserved
Anything you've written on a model entry survives a sync, as long as the server still reports that model ID:
"models": {
"org/Qwen3-27B": {
"name": "My Qwen label", // kept
"limit": { "context": 229376 }, // kept
// Even this comment is kept.
"options": { "temperature": 0.6 }
}
}
All options
opencode-sync [--host HOST] [--port PORT] [--provider ID] [--config PATH]
[--rename OLD=NEW] [--no-infer-renames] [--dry-run]
[--no-url-update] [--no-model-update] [--timeout SECONDS]
[--default-model MODE] [--default-small-model MODE] [--prune-recent]
| Flag | Default | Description |
|---|---|---|
--host HOST |
env, config, else localhost | vLLM server hostname |
--port PORT |
env, config, else 8080 | vLLM server port |
--provider ID |
all providers | Provider key to update |
--config PATH |
auto-detect | Path to opencode.jsonc |
--rename OLD=NEW |
— | Move a model entry to a new ID, keeping its settings (repeatable) |
--no-infer-renames |
off | Don't treat a 1-in/1-out sync as a rename; drop the old entry |
--dry-run |
off | Print a diff of the planned changes without writing |
--no-url-update |
off | Don't update baseURL even when --host/--port is given |
--no-model-update |
off | Don't update model/small_model if the active model is removed |
--default-model MODE |
first |
Pointer policy: first, none, auto, or explicit provider/model-id |
--default-small-model MODE |
first |
Same, for small_model |
--prune-recent |
off | Drop dead models from opencode's recent[] state file |
--timeout SECONDS |
10 | HTTP request timeout |
Config file locations
opencode uses xdg-basedir on every platform, so the search order is the same everywhere:
$XDG_CONFIG_HOME/opencode/opencode.jsonc(ifXDG_CONFIG_HOMEis set)~/.config/opencode/opencode.jsonc
Override with --config PATH.
Development
pip install -e ".[dev]"
python -m pytest # 205 tests
python -m pytest --cov=opencode_sync # with coverage
Metadata
Release files for opencode-sync 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| opencode_sync-0.5.0.tar.gz | 51.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| opencode_sync-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.7 kB
Release files / opencode_sync-0.5.0.tar.gz
| Download URL | opencode_sync-0.5.0.tar.gz |
|---|---|
| Size | 51.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
028fef0dc8bb5c0bfed07af101efc505b64dc0bee139edc2ef2543eba9bdc0af
|
|
BLAKE2b-256 checksum How to use checksums |
20d5cf470366fda073a44875349ad95a804a8362e6031e3f4f3eb5c325b67bbc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.
Transparency logRelease files / opencode_sync-0.5.0-py3-none-any.whl
| Download URL | opencode_sync-0.5.0-py3-none-any.whl |
|---|---|
| Size | 27.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2dcdf37195c1bf0678c1e1836c9e47e6bd7fc36e5445f51457ea59ea7fec997c
|
|
BLAKE2b-256 checksum How to use checksums |
20416b62bd6ea95f4d68b5f3f17c983cf99982b30414f6698887815f3a5ced55
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.
Transparency logSecurity for opencode-sync 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for opencode_sync-0.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.