mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
The OpenCode transport plugin - the piece that gives `wrap opencode`
all-provider routing by tagging each request with `x-headroom-base-url`
- only exists in repo checkouts today. `headroom_opencode_plugin_path()`
resolves `plugins/opencode/dist/entry.opencode.js`, which pip wheels do
not ship, so every pip install silently degrades to the two-provider
(anthropic/openai) baseURL fallback. The function's own docstring
documents the gap ("a pip-only install that does not ship `plugins/`").
Shipping the existing build output is not enough: the regular tsup build
leaves `headroom-ai` and `@opencode-ai/plugin` as bare external imports,
which only resolve next to the checkout's `node_modules`. Copied into
site-packages, the file fails to load. This PR ships a self-contained
bundle inside the wheel instead.
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `plugins/opencode/tsup.standalone.config.ts` + `npm run
build:standalone`: a second build of the loader entry with `noExternal:
[/.*/]` and `splitting: false` - a single self-contained file whose only
imports are node builtins.
- `headroom/providers/opencode/_dist/entry.opencode.js`: the committed
standalone bundle (452 KB). It sits inside the package directory, so
maturin's `python-source = "."` packaging picks it up into the wheel
with no build-system changes.
- `headroom_opencode_plugin_path()`: falls back to the packaged bundle.
Precedence otherwise unchanged: `HEADROOM_OPENCODE_PLUGIN_PATH` env
override, then a repo-checkout build (fresher during development), then
the packaged bundle.
- CI (`opencode-plugin.yml`): rebuilds the standalone bundle and fails
the run if the committed artifact drifted from source, with a one-line
fix instruction; workflow path triggers extended to
`headroom/providers/opencode/_dist/**`.
- `tests/test_providers_opencode_plugin_path.py`: packaged bundle exists
and is self-contained (no bare npm imports), env override wins, fallback
resolution order.
## 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
$ uv run --frozen --extra dev pytest tests/test_providers_opencode_plugin_path.py \
tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py
============================== 49 passed in 0.31s ==============================
$ uvx ruff check headroom/providers/opencode/runtime.py tests/test_providers_opencode_plugin_path.py
All checks passed!
$ uvx ruff format --check headroom/providers/opencode/runtime.py tests/test_providers_opencode_plugin_path.py
2 files already formatted
$ uv run --frozen --extra dev mypy headroom/providers/opencode/runtime.py
Success: no issues found in 1 source file
$ cd plugins/opencode && npm run build:standalone
ESM dist-standalone/entry.opencode.js 452.28 KB
ESM Build success in 28ms
```
## Real Behavior Proof
- Environment: macOS 15 (arm64), opencode 1.18.5 (Homebrew), node 22 /
npm 10, isolated `XDG_*` dirs so no real user config was touched.
- Exact command / steps:
1. `npm run build:standalone` in `plugins/opencode`.
2. Started a local header-logging HTTP listener on `127.0.0.1:9977`
(stands in for the proxy; logs method, path, headers, returns 401).
3. Registered the standalone bundle by absolute path in a scratch
`opencode.json` (`"plugin":
["<abs>/dist-standalone/entry.opencode.js"]`) with a `google` provider
entry and a fake API key. Note: the bundle's directory has **no**
`node_modules` - this is exactly the site-packages situation.
4. `HEADROOM_PROXY_URL=http://127.0.0.1:9977 opencode run -m
google/gemini-2.5-flash "say hi"`.
- Observed result: the listener received `POST
/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse` with
`User-Agent: opencode/1.18.5 ...` - i.e. the plugin loaded standalone
and rerouted a provider that the baseURL fallback cannot cover (native
Gemini wire format) to the proxy URL from `HEADROOM_PROXY_URL`.
- Not tested: Windows path resolution (pure `pathlib`, no platform
branches); wheel-build byte-determinism of the tsup output across OSes
(the CI drift check will surface it on the first divergent build).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)
## Screenshots (if applicable)
Not applicable - CLI/packaging change.
## Additional Notes
- Documentation checklist item: unchecked because the only doc surface I
found is the `headroom_opencode_plugin_path()` docstring, which this PR
rewrites to describe the three-step resolution order. Happy to add a
line to `docs/content/docs/` if there is a preferred page.
- A committed build artifact is not free: the CI drift check keeps it
honest, and the byte-compare relies on tsup/esbuild determinism under
`npm ci` (pinned lockfile). If you'd rather avoid the committed artifact
entirely, the alternative is publishing `headroom-opencode` to npm (its
`package.json` is publish-ready) and registering the plugin by package
name - happy to rework in that direction; the wheel-bundled path has the
advantage of version-locking the plugin to the backend it ships with.
- Downstream motivation: Headroom Desktop manages a long-lived shared
proxy (no `wrap` launcher) and wants to register this plugin from the
installed wheel path so OpenCode users get all-provider routing there
too.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
50 lines
2 KiB
Python
50 lines
2 KiB
Python
"""Resolution of the OpenCode transport-plugin path across install layouts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import headroom.providers.opencode.runtime as oc_runtime
|
|
from headroom.providers.opencode.runtime import headroom_opencode_plugin_path
|
|
|
|
_PACKAGED = Path(oc_runtime.__file__).resolve().parent / "_dist" / "entry.opencode.js"
|
|
_REPO_BUILD = (
|
|
Path(oc_runtime.__file__).resolve().parents[3]
|
|
/ "plugins"
|
|
/ "opencode"
|
|
/ "dist"
|
|
/ "entry.opencode.js"
|
|
)
|
|
|
|
|
|
def test_packaged_bundle_is_committed_and_self_contained() -> None:
|
|
# The wheel picks this file up via maturin's python-source packaging; if
|
|
# it goes missing, pip installs silently lose all-provider routing again.
|
|
assert _PACKAGED.is_file(), "committed wheel bundle missing - run npm run build:standalone"
|
|
text = _PACKAGED.read_text(encoding="utf-8")
|
|
assert len(text) > 10_000, "bundle suspiciously small - not the standalone build?"
|
|
# Self-contained: no bare npm imports; node builtins are the only imports
|
|
# allowed (site-packages has no node_modules to resolve anything else).
|
|
assert 'from "headroom-ai"' not in text
|
|
assert 'from "@opencode-ai/plugin"' not in text
|
|
|
|
|
|
def test_plugin_path_env_override_wins(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
override = tmp_path / "custom.js"
|
|
override.write_text("// plugin")
|
|
monkeypatch.setenv("HEADROOM_OPENCODE_PLUGIN_PATH", str(override))
|
|
assert headroom_opencode_plugin_path() == str(override)
|
|
|
|
|
|
def test_plugin_path_falls_back_to_packaged_bundle(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.delenv("HEADROOM_OPENCODE_PLUGIN_PATH", raising=False)
|
|
resolved = headroom_opencode_plugin_path()
|
|
assert resolved is not None
|
|
# In a repo checkout with a built plugins/opencode/dist the repo build wins
|
|
# (fresher during development); otherwise the packaged bundle must resolve.
|
|
if _REPO_BUILD.is_file():
|
|
assert resolved == str(_REPO_BUILD)
|
|
else:
|
|
assert resolved == str(_PACKAGED)
|