headroom/plugins
Tejas Chopra ad0034f981
fix(opencode): route native providers + load transport plugin, fix Serena context (#1573)
## Description

`headroom wrap opencode` looked like it worked (proxy started, opencode
launched) but **no inference reached the proxy**, so users saw zero
savings (#1572). Root causes:

1. The injected synthetic `headroom` provider
(`@ai-sdk/openai-compatible`) had **no `models` and no `apiKey`** →
opencode raised `ProviderModelNotFoundError`, and it only ever targets
OpenAI.
2. The wrap injected a reference to the **unpublished
`headroom-opencode` npm plugin**, which opencode silently failed to
resolve → the transparent transport never loaded.
3. Serena was launched with `--context opencode`, a context Serena does
not ship → crash on launch (#1549).

This PR makes `headroom wrap opencode` route opencode's traffic through
the proxy with the user's **own API key** (no key written to disk), and
gets the transparent transport plugin actually loading.

Closes #1572
Closes #1549

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- **`runtime.py`** — two complementary routing layers (both verified
against opencode 1.17):
1. Override opencode's native `anthropic`/`openai` provider `baseURL` to
the proxy. Reliable, credential-independent (covers API key **and**
subscription), keeps native model metadata/limits, reuses the user's
existing key. This is the always-on layer and the only one a pip-only
install needs.
2. Load the transport plugin **by absolute path** when it has been built
(`headroom_opencode_plugin_path()`), self-configured via
`HEADROOM_PROXY_URL`. Covers providers we don't name (Gemini, Copilot,
custom gateways) and providers added mid-session. Loopback URLs aren't
double-routed, so the two layers coexist.
- **`wrap.py`** — Serena context `opencode` → `agent` (valid context).
- **`plugins/opencode/`** — new `src/entry.opencode.ts` loader entry
that exports **only** the plugin function (opencode rejects a module
with non-function exports: "Plugin export is not a function"); tsup
builds it as a second entry.
- **tests** — updated `test_providers_opencode_config.py` for path-based
plugin injection + a skip-when-unbuilt case.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ python -m pytest tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py -q
72 passed in 0.59s

$ (cd plugins/opencode && npm test)
Test Files  2 passed (2)
     Tests  9 passed (9)

$ ruff check headroom/providers/opencode/runtime.py headroom/cli/wrap.py tests/test_providers_opencode_config.py
✓ Ruff: No issues found
$ mypy headroom/providers/opencode/runtime.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- **Environment:** macOS, opencode 1.17.11 (npm), headroom proxy 0.28.0
(local), Anthropic API key from `.env`.
- **Exact command:**
  ```
headroom wrap opencode --no-serena --no-context-tool --no-proxy --port
8787 \
-- run -m anthropic/claude-haiku-4-5-20251001 "Reply with exactly:
WRAPWORKS"
  ```
- **Observed result:** opencode printed `plugin=headroom-opencode`
(loaded, no error) and returned `WRAPWORKS`. The proxy log shows the
request routed through it:
  ```
event=outbound_request method=POST
path=https://api.anthropic.com/v1/messages source=passthrough
  event=proxy_inbound_response path=/v1/messages status=200
  PERF model=claude-haiku-4-5-20251001 cache_hit_pct=97 client=opencode
  ```
  Compression verified on a large tool_result (`client=opencode`):
  ```
Pipeline complete: 170653 -> 77 tokens (saved 170576, 100.0% reduction)
PERF tok_before=151309 tok_after=67 tok_saved=151242
transforms=router:tool_result:log client=opencode
  ```
- **Not tested:** custom OpenAI-compatible gateways (need the proxy to
honor `x-headroom-base-url` in the dedicated OpenAI handler — open PR
#1502); interactive TUI (verified the headless `opencode run` path).

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Additional Notes

- **Plugin shipping:** the plugin loads by repo-relative path, which
works for source/editable installs. `plugins/opencode/dist/` is
gitignored, so the plugin must be built (`cd plugins/opencode && npm
install && npm run build`) for layer 2 to activate; pip-only installs
gracefully fall back to layer 1 (native baseURL override). Bundling
`dist/` into the package or publishing `headroom-opencode` to npm is a
follow-up for universal shipping.
- **CHANGELOG:** N/A — handled by Release Please from the conventional
commit.
- Custom-gateway support depends on existing PR #1502 (honor
`x-headroom-base-url` in the dedicated OpenAI handlers); not duplicated
here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-29 15:04:56 -07:00
..
headroom-agent-hooks ci(release): align manifest + pyproject + package.json to 0.22.3 2026-05-25 18:41:38 -07:00
headroom-oauth2 chore: remove committed node_modules + stray/internal markdown (repo hygiene) (#1528) 2026-06-27 23:32:54 -07:00
hermes fix(ccr): make headroom_retrieve a hash-only full-content lookup (#1532) 2026-06-28 10:32:43 -07:00
openclaw chore: release main (#1441) 2026-06-29 12:53:17 -07:00
opencode fix(opencode): route native providers + load transport plugin, fix Serena context (#1573) 2026-06-29 15:04:56 -07:00