@danipl/opencode-jev
v1.2.0
Published
Route OpenCode tool selection through TypeSafe System-1 (Jev): a cheap fast model picks the next tool so the reasoning model executes instead of deliberating.
Maintainers
Readme
Your reasoning model should think about your problem — not about which of twelve tools to call.
opencode-jev puts TypeSafe's cheap, fast System-1 model in front of every inference request: it picks
the next tool, trims the request to that one tool, and lets the expensive model do what you pay it for.
How it works · Developer guide · CI/CD · Config example
Why
Every iteration of an agentic loop re-sends the whole conversation plus every tool schema to a
premium model, which burns tokens and latency deliberating between read, grep, edit, bash,
MCP tools… — a routing decision, repeated every turn, billed at reasoning prices.
The fix: this plugin observes each outbound inference request (OpenCode V2 http.request session
hook), asks Jev — a cheap, fast, calibrated System-1 model — which of the request's tools comes
next, and when Jev is confident enough, trims the tools array down to that single tool. The
reasoning model then executes instead of deliberating. Dropped tool schemas also shrink the
request itself.
flowchart LR
O["OpenCode<br/>agent loop"] -->|request · 12 tool schemas| J{{"jev plugin<br/>ask System-1"}}
J -->|"confident ✓<br/>tools: [1]"| L["Reasoning model<br/>executes immediately"]
J -->|"not confident ✗<br/>request untouched"| L
style J fill:#12101f,stroke:#818cf8,color:#e2e8f0
style L fill:#0b0b14,stroke:#22d3ee,color:#e2e8f0
style O fill:#0b0b14,stroke:#e879f9,color:#e2e8f0Trimming — not pinning via tool_choice — is deliberate: providers reject forced tool_choice
in thinking mode (HTTP 400), while a single offered tool is always valid.
The full picture — diagrams inside the agentic loop, the token/latency math, and the trust dial — lives in docs/HOW_IT_WORKS.md.
At a glance
| | |
| --- | --- |
| ⚡ System-1 routing | Jev answers "which tool next, and how sure am I?" — fast, cheap, calibrated. |
| ✂️ Trim, don't pin | Only the tools array shrinks; tool_choice is never touched. Always provider-valid. |
| 🛡️ Fail-safe by design | Low confidence, network error, timeout, parse error, bad key → original request passes through untouched. It can never break a session. |
| 🔭 Live decision log | Every request ends in one tagged line — apply: or bypass: with the reason. tail -f it. |
| 🫥 Invisible when unconfigured | No API key → no hook registered, no log file, zero latency. |
| 🔌 Any compatible gateway | Anthropic Messages (/v1/messages), OpenAI Chat Completions (/chat/completions), OpenAI Responses (/responses) — path-matched. |
Quick start
1. Register the plugin — OpenCode installs it automatically on next start:
// opencode.jsonc — project-level or ~/.config/opencode/
{
"plugins": ["@danipl/opencode-jev"]
}Requires OpenCode V2 (Plugin.define API; V1 hosts reject it).
The bare name tracks the latest dist-tag — every publish moves it, so a plain
"plugins": ["@danipl/opencode-jev"] keeps you current with zero maintenance. Pin an exact
version for reproducibility or to freeze a known-good release:
{ "plugins": ["@danipl/[email protected]"] } // x-release-please-version — exact version, never auto-updatesHeads-up while pre-1.0: feat: releases (minor bumps) can change behavior. If that matters to
you, pin; otherwise latest is the recommended choice.
2. Set an API key — either method:
export TYPESAFE_API_KEY="apikey_..."or copy jev.yaml.example to jev.yaml next to your OpenCode config
(or anywhere, pointed to by JEV_CONFIG_PATH).
3. Watch it route:
tail -f /tmp/opencode-jev.logThat's it. Run a session — you'll see apply: lines where Jev trimmed the tools and bypass:
lines (with reasons) where the request went through untouched.
Configuration
Config sources — first defined value wins per field:
$JEV_CONFIG_PATHfile (JSON or YAML)./jev.config.yaml/.yml/.json./.opencode/jev.yaml/jev.json~/.config/jev/config.yaml/config.json- plugin options (directory-package registrations only)
- env
TYPESAFE_API_KEY/JEV_API_URL/JEV_MIN_CONFIDENCE
Config-file fields: apiKey, apiUrl, minConfidence, model, timeoutMs
(milliseconds; non-positive or bogus values count as unset) — see
jev.yaml.example. JEV_MODEL / JEV_TIMEOUT_MS are env
fallbacks for model / timeoutMs.
| Env var | Default | Meaning |
| --- | --- | --- |
| JEV_MODEL | jev-latest | Jev model id (fallback for model) |
| JEV_TIMEOUT_MS | 2000 | Jev round-trip timeout (fallback for timeoutMs) |
| JEV_DEBUG_FILE | /tmp/opencode-jev.log | decision log path |
| JEV_DEBUG | — | 1 echoes the log to stdout |
| JEV_DEBUG_MAX_BYTES | 262144 | log rotation cap (one .1 backup) |
Safety
Jev must never break a session. The request passes through untouched on: low confidence,
respond_to_user, payloads with no usable tools or an already-pinned tool_choice, network
failure, timeout, parse errors, or an invalid API key (latched off after the first 401/403 — zero
added latency afterwards). Only primary agent-loop requests are considered
(event.kind === "primary"); title/compaction traffic is skipped. Responses-API built-in tools
(type !== "function") are never offered to Jev, never trimmed to, and never removed by a trim —
they always survive (issue #12 verdict: Jev may only demote function tools).
Reading the decision log
Every decision is appended to the debug log — tail -f /tmp/opencode-jev.log to watch routing
live. Each request ends in one tagged line:
apply:— Jev acted; tools trimmed to its pick (+ any built-ins kept).bypass:— request untouched, with the reason.
Line-by-line interpretation: docs/DEVELOPMENT.md — "Reading the decision log".
Privacy
With routing active, every eligible request POSTs a compact snapshot to apiUrl (default
https://api.typesafe.ai/v1/systemone — TypeSafe's endpoint; override with the apiUrl config
field or JEV_API_URL). The snapshot contains:
state— the first user message plus the last 4 conversation turns, each turn capped at 2 KB and the whole payload at 8 KB. In a coding agent those turns routinely contain file contents, tool output, and error messages — treat this like sending context to another model provider.- Tool names — up to the first 254 names, offered as routing choices. Names only, never schemas or descriptions.
No apiKey configured = no hook registered = nothing ever leaves your machine. If conversation
egress is not acceptable at all, leave the plugin unconfigured or point apiUrl at a self-hosted
Jev-compatible endpoint.
Development
Full developer guide — architecture, unit testing, local manual testing with OpenCode, PR workflow, SDLC: docs/DEVELOPMENT.md.
npm install
npm run build # tsc -> dist/
npm pack # inspect the tarballLocal trial without publishing — point OpenCode at the checkout:
{ "plugins": ["/absolute/path/to/opencode-jev"] }Releasing
Fully automated via release-please — the commit type picks the bump: fix: → patch, feat: →
minor, feat!: or a BREAKING CHANGE: footer → major. Merge the resulting PR and a release PR
appears; merge that and the version bump, tag, GitHub Release and npm publish happen on their
own. See docs/CICD.md.
License
MIT — © danipl
