## Summary This PR implements transparent `headroom wrap opencode` support without asking users to edit OpenCode provider URLs, choose an extra CLI flag, or maintain a static provider list. The wrapper now lives at the runtime transport boundary: OpenCode keeps its user/provider config, while Headroom intercepts outbound provider traffic in-process and routes it through the local Headroom proxy. ## What changed ### Transparent OpenCode wrapping - `headroom wrap opencode` injects the `headroom-opencode` plugin through `OPENCODE_CONFIG_CONTENT`. - Existing OpenCode provider URLs are preserved. We do not rewrite user config URLs to point at Headroom. - Existing `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` env vars are preserved. - Local OpenCode traffic, localhost traffic, and Headroom proxy traffic bypass the shim to avoid loops. ### Runtime transport interception - Added an OpenCode plugin transport shim that wraps: - `globalThis.fetch` - `http.request` / `http.get` - `https.request` / `https.get` - External provider calls are routed to the local Headroom proxy. - The original upstream origin is passed through `x-headroom-base-url`, so the proxy can forward to the real provider without changing OpenCode config. - External `http2.connect` is blocked loudly instead of allowing direct provider traffic to leak outside Headroom. ### Live provider additions Provider coverage is no longer based on a static config scan. Because routing happens at outbound request time, providers added mid-session are routed through Headroom automatically as long as they use the covered Node transport paths. ### Subagent and child-process coverage - The parent OpenCode plugin sets a packaged Node preload shim through `NODE_OPTIONS=--import=.../hook-shim/handler.js`. - The transport shim patches `child_process.spawn`, `exec`, `execFile`, and `fork` so child Node processes receive the Headroom preload even when OpenCode passes a custom `env`. - The child-process shim fails closed if it loads without `HEADROOM_OPENCODE_TRANSPORT_PROXY_URL`. - This closes the subagent leak path where a child Node process could otherwise start without Headroom transport interception. ## Why this goes beyond PR #1089 PR #1089 improves OpenCode provider registration, but it still focuses on provider config shape. This PR moves the enforcement boundary to runtime transport interception. This PR goes further because: - No provider URL rewriting is required. - New providers added mid-session are covered automatically. - Subagents and child Node processes inherit the Headroom transport shim. - Direct external HTTP/2 paths fail loudly instead of leaking. - The wrap remains transparent to the user's OpenCode provider config. - The wrapper is fail-closed for unsupported child-process preload state. ## Additional robustness fixes While validating the change in Docker, the full Python suite exposed unrelated Linux/container robustness issues. These are fixed in this PR so the suite is green: - Binary cache handling now treats cache paths under a non-writable existing parent as unavailable, including when tests run as root in Docker. - `release_version.py` honors `MANUAL_VER` before git calls so direct script execution works outside a `.git` checkout. - Test logger isolation now resets relevant Headroom child loggers so proxy logging setup cannot poison later `caplog` tests. - The scanner missing-path test now uses a guaranteed missing `tmp_path` child instead of relying on `/nonexistent/path`. ## Validation All implementation validation was run inside Docker. - Full Python suite from a fresh Docker copy: `6605 passed, 523 skipped`. - Ruff on changed Python/OpenCode paths: passed. - OpenCode plugin typecheck: passed. - OpenCode plugin tests: `9 passed`. - OpenCode plugin build: passed. - Hook shim preload smoke test: passed. ## Notes This PR intentionally does not add a CLI option. `headroom wrap opencode` means full wrap. Either Headroom wraps OpenCode transparently, or the path fails loudly instead of silently leaking provider traffic. --------- Co-authored-by: Rudimar Ronsoni <6081613+rudironsoni@users.noreply.github.com> |
||
|---|---|---|
| .. | ||
| hook-shim | ||
| src | ||
| .gitignore | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsup.config.ts | ||
| vitest.config.ts | ||
headroom-opencode
Headroom proxy integration for OpenCode. Routes LLM traffic through the Headroom proxy for token compression, provides CCR retrieval, and handles provider configuration.
Install
npm install headroom-opencode
Quick start
Create a Headroom provider for opencode.json
import { createHeadroomProvider } from "headroom-opencode";
const provider = createHeadroomProvider({
proxyPort: 8787,
});
// Write to opencode.json:
// {
// "provider": { "headroom": provider },
// "model": "headroom/claude-sonnet-4-6"
// }
Build OPENCODE_CONFIG_CONTENT
import { buildOpencodeConfigContentJson } from "headroom-opencode";
const json = buildOpencodeConfigContentJson({
proxyPort: 8787,
defaultModel: "claude-sonnet-4-6",
});
// Set as env var: process.env.OPENCODE_CONFIG_CONTENT = json;
Compress messages through the proxy
import { compressWithHeadroom } from "headroom-opencode";
const result = await compressWithHeadroom(messages, {
model: "gpt-4o",
proxyUrl: "http://localhost:8787",
});
console.log(`Saved ${result.tokensSaved} tokens`);
CCR retrieve tool
import { createHeadroomRetrieveTool } from "headroom-opencode";
const retrieveTool = createHeadroomRetrieveTool({
proxyBaseUrl: "http://localhost:8787",
});
// Register in OpenCode's MCP config under mcp.headroom_retrieve
API
createHeadroomProvider(options?)
Creates a provider object compatible with OpenCode's @ai-sdk/openai-compatible format.
| Option | Default | Description |
|---|---|---|
proxyBaseUrl |
http://127.0.0.1:8787 |
Full proxy base URL |
proxyPort |
8787 |
Proxy port (ignored if proxyBaseUrl is set) |
models |
See below | Custom model mappings |
defaultModel |
claude-sonnet-4-6 |
Default model ID |
buildOpencodeConfigContent(options?)
Returns a full OPENCODE_CONFIG_CONTENT JSON object with provider and model.
buildOpencodeConfigContentJson(options?)
Same as above but returns a JSON string ready for the OPENCODE_CONFIG_CONTENT env var.
compressWithHeadroom(messages, options?)
Compresses an array of messages through the Headroom proxy. Returns compression stats and compressed messages.
createHeadroomRetrieveTool(config)
Creates a CCR retrieve tool for OpenCode's MCP system.
setDefaultProxyUrl(url) / getDefaultProxyUrl()
Set or get the default proxy URL for all operations. Defaults to HEADROOM_BASE_URL env var or http://localhost:8787.
Default models
| Model ID | Context | Output |
|---|---|---|
claude-sonnet-4-6 |
200K | 16K |
claude-opus-4-6 |
200K | 16K |
claude-haiku-4-5-20251001 |
200K | 8K |
gpt-4o |
128K | 16K |
gpt-4.1 |
1M | 32K |
OpenCode plugin
The package default export is an OpenCode plugin. It adds a headroom_retrieve
tool and Headroom metadata for shell commands:
{
"plugin": [["headroom-opencode", { "proxyUrl": "http://127.0.0.1:8787" }]]
}
The plugin does not set OPENAI_BASE_URL or ANTHROPIC_BASE_URL. Model traffic
is routed by the headroom provider config generated by
buildOpencodeConfigContent or headroom wrap opencode.
License
Apache-2.0