opencode-auto-jev
v0.1.1
Published
OpenCode plugin that adds an Auto (Jev) virtual model which routes each prompt to a configured real model using TypeSafe AI (Jev).
Readme
opencode-auto-jev
An OpenCode plugin that adds an Auto (Jev) virtual model to the normal model picker. When you pick Auto, each of your turns is sent to TypeSafe AI (Jev), which picks one of your configured real models for that prompt. OpenCode then calls that real model as usual — native credentials, auth adapters, streaming, tools and accounting all keep working.
There is no OpenCode fork and no UI patch. The plugin only:
- registers a virtual provider/model (
auto-jev/auto, displayed asAuto (Jev)) through theconfighook, and - rewrites
chat.message's mutableoutput.message.modelto a realprovider/modelbefore the model call.
Nothing is routed unless you actually selected Auto. Manually selected real models are never touched.
Quick start
The plugin is available on npm. OpenCode installs the package and its dependencies automatically from your configuration; no local checkout or build is needed. Use OpenCode 1.18.32 or newer.
The TypeSafe API key is supplied only through the plugin's apiKey option.
There is no environment-variable support: the plugin never reads, writes or
requires TYPESAFE_API_KEY (it also pins the TypeSafe base URL so
TYPESAFE_BASE_URL cannot redirect the request).
Merge the following into your OpenCode config (~/.config/opencode/opencode.json
or a project opencode.json). Replace REPLACE_WITH_YOUR_TYPESAFE_API_KEY with
your key and choose candidate models you have connected:
{
"$schema": "https://opencode.ai/config.json",
"model": "auto-jev/auto",
"plugin": [
[
"opencode-auto-jev@latest",
{
"apiKey": "REPLACE_WITH_YOUR_TYPESAFE_API_KEY",
"fallback": "anthropic/claude-sonnet-4-5",
"candidates": [
{
"providerID": "anthropic",
"modelID": "claude-sonnet-4-5",
"description": "Strong general coding model for refactors, multi-file changes and debugging."
},
{
"providerID": "anthropic",
"modelID": "claude-haiku-4-5",
"description": "Very fast, cheap model for small edits, simple questions and formatting."
}
]
}
]
]
}Prefer not to inline the secret? OpenCode substitutes {file:...} references in
config text before the plugin sees its options, so you can point at a private
file instead:
"apiKey": "{file:~/.secrets/typesafe-api-key}"Key points:
apiKeyis required. Missing, blank, whitespace-only or non-string values fail plugin setup with an option-named error (the value is never echoed). Whether you inline the key or use{file:...}, it must be a non-empty string.- Merging, not replacing. If you already have
plugin,provider,modeloragentsettings, add this entry to your existingpluginarray instead of overwriting the file. The plugin never rewrites your config; it only adds its own virtual provider at startup. Everything else is preserved. - Candidates must already be connected. Each
providerID/modelIDpair must be a provider you have actually authenticated under OpenCode. Auto only ever routes to a model present in OpenCode's live catalog, so an unconnected or disabled provider is silently ineligible (and reported in the error if nothing is left). "model"is optional. Setting"model": "auto-jev/auto"makes Auto the default for new sessions; you can also just pick Auto (Jev) from the model picker (/models). If you leavemodelout, nothing changes until you select Auto.enabled_providersusers: if your config setsenabled_providers, you must add"auto-jev"to that list yourself, because OpenCode then ignores every provider not listed. This plugin does not modify your provider policy.
Quit and restart OpenCode after editing the config, then choose Auto (Jev) in
the model picker. A fuller configuration is in
examples/opencode.json.
Package versions
The README and example use opencode-auto-jev@latest, which resolves npm's
latest release tag instead of hard-coding a version. The version badge above
also reads from npm automatically (subject to caching), so neither needs a
version-number edit for each release.
For reproducible installs, replace latest with an exact published version from
the npm package page. OpenCode manages plugin installation and caching; @latest
does not hot-reload a running session. Restart OpenCode after updating plugins.
Configuration options
| Option | Default | Meaning |
| --- | --- | --- |
| apiKey | required | TypeSafe API key (non-empty string). Config-only — no env fallback. Sent to TypeSafe as the Authorization header. |
| providerID | auto-jev | Virtual provider id. Must not already exist in your config. |
| modelID | auto | Virtual model id. |
| displayName | Auto (Jev) | Name shown in the model picker. |
| candidates | required | 2–255 { providerID, modelID, description } entries. description is sent to Jev. |
| fallback | none | Candidate key (providerID/modelID) used when Jev fails or is unsure. Must be one of candidates. Without it, Auto fails closed. |
| jevModel | jev-latest | TypeSafe model name. |
| timeoutMs | 15000 | Per-attempt Jev timeout (1000–120000). |
| maxRetries | 1 | Bounded Jev retries (0–5). |
| minConfidence | 0 | Below this Jev confidence, use the fallback (or fail closed). 0..1. See Tuning. |
| maxPromptChars | 4000 | Maximum prompt text sent to Jev. |
| notify | true | Show an Auto → provider/model toast after routing. |
| baseURL | https://api.typesafe.ai | TypeSafe API root. Always explicit so TYPESAFE_BASE_URL cannot redirect the key. |
Invalid configuration fails loudly at plugin load with the offending option named.
Tuning
minConfidence defaults to 0, which trusts any valid closed-set choice Jev
returns. If you prefer to fall back more often, raise it (for example 0.6): any
turn where Jev's reported confidence is below the threshold uses your fallback
(or fails closed if none is configured). This is a preference knob, not a claim
about routing quality.
Behaviour
- Routing happens once per user turn, before the model call, by rewriting
output.message.model. It never reruns per tool iteration, so every tool result in a turn stays on the same model. - Auto stays selected for the session. Once you pick Auto, OpenCode keeps Auto as the session's model and every subsequent turn is re-routed. Selecting a real model manually immediately stops routing — the plugin detects that the incoming message model is not the alias and does nothing.
- Fails closed. If Jev is unreachable/times out/returns an unknown choice/an
invalid confidence, Auto uses your configured
fallbackif it is available for that prompt; otherwise the turn is not sent and you get a clear error. - Candidates are verified against the live OpenCode catalog at routing time. Models that are missing, disabled, deprecated, lack tool-calling, or cannot read the prompt's modalities are removed before Jev ever sees them. Jev can only ever choose one of the configured candidates. Attachment modality filtering removes candidates that cannot read the attachment; if no candidate can, the turn fails with an actionable error rather than routing to an incapable model.
- Single eligible candidate short-circuits. If only one configured candidate is available for a given prompt, Auto selects it deterministically and does not call Jev (a one-label choice carries no routing information).
- Incompatible variants are cleared. The alias has no variants, so a stale variant never leaks onto the real model.
- Concurrency safe. Routing state is per-call; concurrent sessions do not interfere.
- The virtual provider points at a reserved loopback port (
127.0.0.1:9). If an alias were ever invoked without routing, the guard makes that call fail locally instead of reaching a real endpoint; it is a failure guard, not a guarantee that no local listener exists.
Session resume and navigation
Auto persistence is only confirmed through the HTTP/session API and the active-session source path. When you navigate away from a session and back, or restart OpenCode, the TUI re-hydrates the picker from the last user message's model — which the plugin rewrote to the real model that actually served that turn. So after resuming or reloading a session you may need to reselect Auto (Jev); the plugin does not persist the alias across UI reloads or navigation. Manual selection always wins.
Cost and privacy
When Auto is used, a turn makes an additional TypeSafe (Jev) request before the model call, and normal OpenCode requests to the chosen real provider (which may include multiple requests when the agent loops over tools or retries). It is not a fixed "exactly two" request count.
What is sent to Jev:
- the text you typed in this turn (bounded to
maxPromptChars), and - the candidate
providerID/modelID+descriptionstrings.
What is never sent to Jev:
- file contents, directory listings or attachments (only their modality is considered locally),
- conversation history, tool outputs, the system prompt,
- stored credentials or API keys (your
apiKeyis used only as theAuthorizationheader; it is never placed in the request body, logs or errors).
Synthetic text OpenCode adds for file reads, and ignored pastes, are excluded too. This exclusion covers stored credentials and file data; it cannot detect a secret you type directly into the prompt, so avoid pasting secrets into an Auto turn. Attachment-only or empty prompts are rejected with an actionable error rather than routing blindly.
Jev request and response logs
Every Jev HTTP attempt is logged at info level under service
opencode-auto-jev, including retries. Jev HTTP entries contain the request
body (prompt text, candidate descriptions and model), the response body
(including error responses), HTTP status and elapsed milliseconds. Match each
request and response using requestID. Transport failures have an error entry.
There are no Jev HTTP entries when routing is bypassed or only one candidate is
eligible.
On macOS, follow these entries while using OpenCode Desktop:
tail -f ~/.local/share/opencode/log/*.log | grep 'Jev HTTP'These logs contain prompt text and API response contents. Headers are excluded and occurrences of the configured API key are redacted. Other secrets you type into a prompt are not automatically detected. Logging failures do not prevent routing. Restart OpenCode Desktop after rebuilding the plugin to load changes.
Where the key is stored
Your apiKey is a plaintext secret stored in your OpenCode config (or in the
{file:...} it points to). OpenCode may surface config values in places such as
its config endpoints, logs or UI, so:
- prefer a global/private config file or a
{file:...}reference to a file only you can read, - never commit the key or a config containing it to a repository.
The key is transmitted to TypeSafe as the request's Authorization header;
"never sent to Jev" above refers to it not being included in the routing prompt
payload — it is, of course, sent to TypeSafe to authenticate.
Text-only for now. The virtual alias advertises text-input only, and the MVP does not run a verified multimodal picker. Classifiable non-text attachments (image/pdf/audio/video) are used locally to filter candidates by capability and are never forwarded to Jev; unclassifiable MIME types are rejected. Jev usage may be billed by TypeSafe; the downstream model request is billed by your provider as usual.
Troubleshooting
| Symptom | Cause / fix |
| --- | --- |
| Auto (Jev) missing from the picker | Config not reloaded — restart OpenCode. Check the provider id isn't already in use, and that you added "auto-jev" to enabled_providers if you set it. |
| Virtual provider "..." already exists | You already define that provider id. Rename it or change providerID. |
| Auto has no available candidate... | None of your candidates are connected/enabled or capable of this prompt. |
| Auto needs some typed text... | Attachment-only prompt. Type text or pick a model manually. |
| Auto cannot route attachments of type ... | Unsupported/unknown attachment MIME type. |
| Auto could not read the OpenCode provider catalog... | OpenCode was not reachable while routing. Retry or select a model manually. |
| missing required option "apiKey"... | Add a non-empty apiKey to the plugin options (inline or via {file:...}). |
| Turn fails with a Jev error | Set fallback, or check the apiKey value and network access. |
Development
From a local checkout:
npm ci
npm run buildTo test local changes, replace opencode-auto-jev@latest in your plugin config
with file:///absolute/path/to/opencode-auto-jev/dist/index.js, keeping the same
options. Rebuild and restart OpenCode after changes.
npm run typecheck # strict TypeScript
npm run build # emit ./dist
npm run test # build + unit tests + real-runtime smoke tests
npm run test:unit # mocked unit tests only
npm run test:smoke # build + real opencode runtime + local mock endpoints
npm pack --dry-run # inspect the published file listtest and test:smoke build first, so a clean checkout has an up-to-date
dist. The smoke suite starts an isolated opencode serve (its own
HOME/XDG/config) using the public launcher on PATH (override with OPENCODE_BIN)
and local mock TypeSafe/chat servers. It uses no real credentials and makes no
paid API calls. Set AUTO_JEV_SMOKE=0 to skip it.
Manual smoke checklist
With a local build, your own provider credentials, and a valid apiKey in the
plugin options (inline or via {file:...}):
- First turn from home — start OpenCode, pick Auto (Jev), and send a
plain text prompt. The reply should come from one of your candidates; the
toast/log shows
Auto → provider/model. - Second turn (unspecified model) — send another prompt without touching the picker. It should be re-routed by Jev (Auto stays sticky within the session).
- Navigate away and back — open another session and return. The footer may now show the real model from the last turn; reselect Auto (Jev) and confirm routing resumes.
- Restart — quit and relaunch OpenCode, resume the session, reselect Auto if needed, and confirm routing works.
- Manual escape — pick a real model and send a prompt; confirm no Jev request is made (watch the log/toast) and the real model answers.
No visual QA was performed by the implementation worker; this checklist is the required human confirmation.
Publishing
Maintainers with npm publish access can release a new version with:
npm run typecheck
npm test
npm version patch # or minor / major, as appropriate
npm publish --access publicThe prepack script builds dist automatically before publishing. A normal
release updates npm's latest tag, which the quick start and example reference.
For prereleases, use a prerelease version and a separate tag such as
npm publish --access public --tag next to keep them out of latest.
Tested version
OpenCode 1.18.32 (@opencode-ai/plugin 1.18.32, opencode-ai 1.18.32,
@opencode-ai/sdk 1.18.32), @typesafe-ai/sdk 0.6.0, Node 22. The plugin
declares engines.opencode >= 1.18.32; earlier versions are not verified.
