feat(wrap): add OpenCode support with headroom wrap opencode#1173
feat(wrap): add OpenCode support with headroom wrap opencode#1173praffulomg wants to merge 1 commit into
Conversation
OpenCode (anomalyco/opencode) is an OpenAI/Anthropic-compatible client that does not honor OPENAI_BASE_URL/ANTHROPIC_BASE_URL env vars. The new wrap target injects an inline OPENCODE_CONFIG_CONTENT config pointing both the openai and anthropic providers baseURL at the local Headroom proxy. Both baseURLs are /v1-based so @ai-sdk/openai resolves to /v1/chat/completions and @ai-sdk/anthropic (which appends only /messages) resolves to /v1/messages -- the proxy actual routes. Per-project savings are attributed via the X-Headroom-Project header; autoupdate is disabled to pin OpenCode under the wrapper.
Additive only: new headroom/providers/opencode/ package + @wrap.command("opencode") mirroring the aider/vibe pattern. No proxy/registry/auth-mode changes (OpenCode is BYOK = PAYG, already mapped). Existing wraps unaffected.
PR governanceThis PR follows the template and is marked ready for human review. |
|
Leaving a cross-link because this overlaps with #1105. I ended up taking #1105 in a different direction: do not patch provider URLs at all. The OpenCode plugin installs a transport shim and routes outbound It also handles the subagent case. The plugin preloads the same shim into child Node processes through The fail-closed behavior matters here. External HTTP/2 is blocked instead of being allowed to bypass Headroom. Local OpenCode calls and Headroom proxy calls are skipped so the shim does not loop into itself. #1105 is green in Docker: full Python suite |
|
Thanks @rudironsoni for the heads-up, and for the thorough work on #1105. You're right that #1105 is the stronger direction. Intercepting at the runtime transport boundary cleanly handles what this PR's config-injection approach cannot — preserving user provider URLs, covering providers added mid-session, and propagating through subagents and child Node processes — and the Docker validation (6,605 passed) speaks for itself. For context, #1173 took the lighter-weight route of injecting Thanks again for flagging the overlap. |
|
@praffulomg thank you for the work on this. It was genuinely useful while shaping #1105, especially for comparing where the OpenCode wrap should live and what edge cases we needed to cover. I ended up taking a stricter route in #1105, with the wrapper sitting at the transport layer so provider changes during a session and subagents are covered too. Your PR helped make that direction clearer. If you have time, I would really appreciate another set of eyes testing #1105. In particular, anything around OpenCode provider config, adding providers after startup, and subagent behavior would be valuable. |
Description
Adds
headroom wrap opencode, extending Headroom's existing wrap family (claude, codex, copilot, aider, cursor, ...) to the OpenCode CLI so its LLM traffic is routed through Headroom's local compression proxy.OpenCode does not honor the generic
OPENAI_BASE_URL/ANTHROPIC_BASE_URLenvironment variables (it only uses env vars for API keys and${VAR}substitution). Endpoint redirection must therefore be injected via OpenCode's own config mechanism. The wrapper sets theOPENCODE_CONFIG_CONTENTenv var to an inline JSON config that points both theopenaiandanthropicproviders'baseURLat the local proxy.Both baseURLs are
/v1-based (http://127.0.0.1:{port}/v1). This is deliberate and load-bearing:@ai-sdk/openaiappends/chat/completionsand@ai-sdk/anthropicappends only/messagesto the configured base, so a/v1base resolves to the proxy's existing/v1/chat/completionsand/v1/messagesroutes. A base without/v1would silently fall through the proxy's verbatim catch-all and forward uncompressed with no error.Type of Change
Changes Made
headroom/providers/opencode/__init__.py— re-exportsbuild_launch_envandproxy_base_url(mirrorsproviders/codex).runtime.py—proxy_base_url(port) -> http://127.0.0.1:{port}/v1;build_launch_env(port, environ, project)emitsOPENCODE_CONFIG_CONTENTJSON with both providers'baseURLset to the/v1proxy base, an optionalX-Headroom-Projectheader (viasanitize_project_name) for per-project savings attribution, andautoupdate: falseto pin OpenCode under the wrapper.headroom/cli/wrap.py— added the_build_opencode_launch_envimport and a new@wrap.command("opencode")(mirrors the additive Pattern A launch flow: resolve binary viashutil.which("opencode")→ build env →_launch_tool(..., tool_label="OPENCODE", agent_type="opencode")); updated thewrapgroup docstring; removed an obsolete note stating the command did not exist.headroom/telemetry/context.py— added"opencode"to_KNOWN_WRAP_AGENTS(telemetry stack label only).README.md— added an OpenCode row to the agent compatibility matrix and the wrap synopsis.CHANGELOG.md— added an Unreleased > Features entry.tests/test_provider_opencode.py— new unit tests (5 assertions).Additive only. No changes to the proxy, provider registry, or auth-mode classification — existing wraps are unaffected.
Testing
ruff check)mypyon the new package)Test Output
Real Behavior Proof
uv run --no-sync ruff check headroom/cli/wrap.py headroom/telemetry/context.py headroom/providers/opencode/ tests/test_provider_opencode.py;uv run --no-sync mypy headroom/providers/opencode/;uv run --no-sync pytest tests/test_provider_opencode.py -q;uv run --no-sync python -c "from headroom.providers.opencode import build_launch_env, proxy_base_url; print(build_launch_env(8787, {}, 'Demo Proj'))"; and Click CliRunner invocations ofwrap --helpandwrap opencode --help.headroom wrap --helplistsopencodewith claude/codex/aider/copilot/cursor still intact;opencodesubcommand --help exits 0; ruff "All checks passed!"; mypy "Success: no issues found in 2 source files"; pytest 5 passed (new) + 231 passed regression (0 failed/skipped).opencodebinary hitting a real upstream API (binary not available in sandbox); mypy was run only on the new headroom/providers/opencode package, not the full headroom tree.Review Readiness
Checklist
/v1rationale is documented inruntime.py)Additional Notes
/v1suffix on both provider baseURLs is intentional and the most important correctness detail —tests/test_provider_opencode.pyexplicitly asserts both end in/v1to guard against a silent regression (a wrong path falls through the proxy catch-all uncompressed with no 404 in logs).X-Headroom-Projectheader (OpenCode supports custom per-provider headers via config), consistent with the proxy's existing header-basedclassify_project.headroom install opencode, no Docker/scripts changes, and no auth-mode/UA classification (OpenCode is BYOK → PAYG, which is the correct aggressive-compression mode;classify_clientalready maps theopencode/user agent). These are noted as possible follow-ups.mypywas run against the newheadroom/providers/opencode/package (clean), not the entireheadroomtree.