This commit is contained in:
Ayush Kumar Jha 2026-08-27 18:09:52 +00:00 committed by GitHub
commit c384deeef2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 207 additions and 6 deletions

View file

@ -235,7 +235,7 @@ shows an **Output Tokens Saved** card next to input compression, labelled
| Copilot CLI | ✅ | starts proxy + launches |
| VS Code Copilot | ✅ | transparent proxy; preserves selected model |
| OpenClaw | ✅ | installs as ContextEngine plugin |
| OpenCode | ✅ | injects config · starts proxy + launches |
| OpenCode | ✅ | injects config · starts proxy + launches [^third-party-upstream] |
| Cline | ✅ | starts proxy + injects config |
| Continue | ✅ | starts proxy + injects config |
| Goose | ✅ | starts proxy + launches |
@ -246,6 +246,8 @@ shows an **Output Tokens Saved** card next to input compression, labelled
| Kimi CLI | ✅ | OAuth bearer forwarded — log in once |
| ZCode | ✅ | starts proxy and prints base URLs for ZCode settings |
[^third-party-upstream]: `headroom wrap` routes OpenAI-compatible traffic to the default upstream (`https://api.openai.com/v1`). For a third-party OpenAI-compatible provider (DeepSeek, Together, OpenRouter, a self-hosted gateway), pass `--openai-api-url https://api.deepseek.com/v1` — or set `OPENAI_TARGET_API_URL` before wrapping — otherwise your provider key is sent to OpenAI and rejected with `401 Incorrect API key provided`. See [OpenCode + DeepSeek](docs/content/docs/opencode-deepseek.mdx).
Any OpenAI-compatible client works via `headroom proxy`. MCP-native: `headroom mcp install`.
Undo durable wrapping with `headroom unwrap <tool>` (supports: `claude`, `copilot`, `codex`, `grok`, `kimi`, `omp`, `opencode`, `openclaw`, `zcode`).
Registry authors can use the canonical [`server.json`](server.json) in the repo root instead of reconstructing the `headroom mcp serve` contract from prose.

View file

@ -193,9 +193,52 @@ API key to the proxy, and the proxy forwards it to DeepSeek. Make sure
### Models appear but requests fail
You ran `headroom wrap opencode`. That command replaces your config with Claude
and GPT models. **Do not use `headroom wrap`.** Configure OpenCode manually as
shown above, and launch OpenCode directly with `opencode`.
You ran `headroom wrap opencode` without telling it about DeepSeek. Wrap
rewrites the `headroom` provider block in your OpenCode config on every run, so
the manual `apiKey` / `models` entries from step 4 are replaced. Either launch
OpenCode directly with `opencode` after configuring it manually as shown above,
or use wrap with an explicit upstream — see the next section.
### I used `headroom wrap opencode` and get "Incorrect API key provided"
```
Incorrect API key provided: sk-13005***********************898d.
You can find your API key at https://platform.openai.com/account/api-keys.
```
That 401 comes from **OpenAI**, not DeepSeek. `headroom wrap opencode` starts
the proxy for you, and unless it is told otherwise the proxy forwards
OpenAI-compatible traffic to its default upstream, `https://api.openai.com/v1`.
Your DeepSeek key is then presented to OpenAI, which rejects it and points you
at a dashboard for a key you never had.
Pass the upstream to wrap, exactly like `headroom proxy --openai-api-url`:
```bash
headroom wrap opencode --openai-api-url https://api.deepseek.com/v1
```
The equivalent environment variable also works, and is honored by both
commands:
```bash
OPENAI_TARGET_API_URL=https://api.deepseek.com/v1 headroom wrap opencode
```
If a proxy is already running on the port with a different upstream, stop it
first (`headroom unwrap opencode`) — wrap will not silently re-point a proxy
that other clients may be sharing.
### When is `headroom wrap opencode` safe?
- **Safe as-is** when your traffic goes to the default upstreams — OpenAI
(`api.openai.com`) or Anthropic (`api.anthropic.com`).
- **Needs `--openai-api-url`** (or `OPENAI_TARGET_API_URL`) for any third-party
OpenAI-compatible provider: DeepSeek, Together, OpenRouter, Groq, a
self-hosted gateway, and so on.
- **Use the manual setup in this guide** when you want to pin specific model
IDs, context limits, or display names in `opencode.json`, since wrap
regenerates the `headroom` provider block each time it runs.
### "headroom" command not found
@ -223,7 +266,11 @@ shaper is active immediately — the numbers just need calibration.
## What's NOT in this guide
- **Claude or GPT models** — this setup uses DeepSeek exclusively
- **`headroom wrap`** — do not use it; it overrides the config
- **`headroom wrap` with a custom model list** — wrap regenerates the
`headroom` provider block on every run, so the model entries below are lost.
If you prefer wrap, run
`headroom wrap opencode --openai-api-url https://api.deepseek.com/v1` and skip
the manual config
- **Deprecated model names** — `deepseek-chat` and `deepseek-reasoner` are
compatibility aliases that will be deprecated on 2026-07-24; use
`deepseek-v4-pro` and `deepseek-v4-flash` instead

View file

@ -41,6 +41,7 @@ headroom wrap opencode \
--no-serena \
--code-graph \
--no-proxy \
--openai-api-url https://api.deepseek.com/v1 \
--learn \
--memory \
--backend anthropic \
@ -49,6 +50,14 @@ headroom wrap opencode \
-- <opencode args>
```
Pass `--openai-api-url` when OpenCode should route through a third-party
OpenAI-compatible provider instead of OpenAI itself — DeepSeek, Together,
OpenRouter, or a self-hosted gateway. Without it, OpenAI-compatible traffic is
forwarded to the default upstream (`https://api.openai.com/v1`), so a
non-OpenAI key is rejected with `401 Incorrect API key provided`. Setting
`OPENAI_TARGET_API_URL` before wrapping has the same effect. See
[OpenCode + DeepSeek](/docs/opencode-deepseek) for a full walkthrough.
## Provider Model Mapping
The generated `headroom` provider exposes these models through the proxy:
@ -69,6 +78,7 @@ The default model is `headroom/claude-sonnet-4-6`. Change it in `opencode.json`
|---|---|
| `OPENCODE_CONFIG_CONTENT` | JSON payload with provider, plugin, and optional local MCP config injected by `wrap` |
| `HEADROOM_PROXY_URL` | Proxy URL passed to Headroom MCP when a non-default port is used, and to the native plugin when configured |
| `OPENAI_TARGET_API_URL` | Upstream for OpenAI-compatible traffic when `--openai-api-url` is not passed; defaults to `https://api.openai.com/v1` |
## Failure Learning

View file

@ -7655,6 +7655,16 @@ def openclaw(
is_flag=True,
help="Route headroom/* models through the authenticated GitHub Copilot subscription",
)
@click.option(
"--openai-api-url",
default=None,
envvar="OPENAI_TARGET_API_URL",
help=(
"Upstream base URL for OpenAI-compatible traffic, e.g. "
"https://api.deepseek.com/v1. Without it the proxy forwards to "
"https://api.openai.com/v1 (env: OPENAI_TARGET_API_URL)."
),
)
@click.option("--learn", is_flag=True, help="Enable live traffic learning")
@click.option("--memory", is_flag=True, help="Enable persistent cross-session memory")
@click.option(
@ -7672,6 +7682,7 @@ def opencode(
code_graph: bool,
no_proxy: bool,
copilot_subscription: bool,
openai_api_url: str | None,
learn: bool,
memory: bool,
backend: str | None,
@ -7697,9 +7708,25 @@ def opencode(
headroom wrap opencode --port 9999 # Custom proxy port
headroom wrap opencode --backend anyllm --anyllm-provider groq
headroom wrap opencode --copilot-subscription # Use a GitHub Copilot subscription
headroom wrap opencode --openai-api-url https://api.deepseek.com/v1

Without --openai-api-url the proxy forwards OpenAI-compatible traffic to
https://api.openai.com/v1, so a third-party key (DeepSeek, Together,
OpenRouter, ...) is rejected upstream with OpenAI's 401 "Incorrect API key
provided". Point the proxy at the real upstream instead:

headroom wrap opencode --openai-api-url https://api.deepseek.com/v1
OPENAI_TARGET_API_URL=https://api.deepseek.com/v1 headroom wrap opencode
"""
subscription_resolution = None
if copilot_subscription:
if openai_api_url:
raise click.ClickException(
"--openai-api-url cannot be combined with --copilot-subscription; "
"the subscription resolves its own upstream."
)
effective_backend = backend or os.environ.get("HEADROOM_BACKEND")
if effective_backend not in (None, "", "anthropic"):
raise click.ClickException(
@ -7793,7 +7820,9 @@ def opencode(
backend=backend,
anyllm_provider=anyllm_provider,
region=region,
openai_api_url=(subscription_resolution.api_url if subscription_resolution else None),
openai_api_url=(
subscription_resolution.api_url if subscription_resolution else openai_api_url
),
copilot_api_token=(subscription_resolution.token if subscription_resolution else None),
copilot_refresh_oauth_token=(
subscription_resolution.refresh_oauth_token if subscription_resolution else None

View file

@ -1140,3 +1140,116 @@ def test_unwrap_opencode_preserves_utf8_user_content(
assert "“smart quotes”" in content
assert "" in content
assert wrap_mod._PROVIDER_MARKER_START not in content
def _capture_ensure_proxy_kwargs(
runner: CliRunner,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
argv: list[str],
) -> dict[str, object]:
"""Run `wrap opencode` with a stubbed proxy/launch and return _ensure_proxy kwargs."""
monkeypatch.chdir(tmp_path)
_set_test_home(monkeypatch, tmp_path)
captured: dict[str, object] = {}
def fake_ensure_proxy(port: int, no_proxy: bool, **kwargs): # noqa: ANN003
captured.update(kwargs)
return None, port
with (
patch.object(wrap_mod.shutil, "which", return_value="opencode"),
patch.object(wrap_mod, "_ensure_proxy", side_effect=fake_ensure_proxy),
patch.object(wrap_mod, "_launch_tool"),
):
result = runner.invoke(main, argv)
assert result.exit_code == 0, result.output
return captured
def test_wrap_opencode_forwards_openai_api_url_to_proxy(
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""--openai-api-url points the proxy at a third-party OpenAI-compatible upstream (#3107)."""
monkeypatch.delenv("OPENAI_TARGET_API_URL", raising=False)
captured = _capture_ensure_proxy_kwargs(
runner,
monkeypatch,
tmp_path,
[
"wrap",
"opencode",
"--port",
"9000",
"--no-mcp",
"--no-serena",
"--openai-api-url",
"https://api.deepseek.com/v1",
],
)
assert captured["openai_api_url"] == "https://api.deepseek.com/v1"
def test_wrap_opencode_honors_openai_target_api_url_env(
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""OPENAI_TARGET_API_URL is honored without the flag, matching `headroom proxy`."""
monkeypatch.setenv("OPENAI_TARGET_API_URL", "https://api.deepseek.com/v1")
captured = _capture_ensure_proxy_kwargs(
runner,
monkeypatch,
tmp_path,
["wrap", "opencode", "--port", "9000", "--no-mcp", "--no-serena"],
)
assert captured["openai_api_url"] == "https://api.deepseek.com/v1"
def test_wrap_opencode_without_openai_api_url_leaves_upstream_unset(
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""No override means the proxy keeps its own default upstream resolution."""
monkeypatch.delenv("OPENAI_TARGET_API_URL", raising=False)
captured = _capture_ensure_proxy_kwargs(
runner,
monkeypatch,
tmp_path,
["wrap", "opencode", "--port", "9000", "--no-mcp", "--no-serena"],
)
assert captured["openai_api_url"] is None
def test_wrap_opencode_rejects_openai_api_url_with_copilot_subscription(
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The Copilot subscription resolves its own upstream; a manual override would fight it."""
monkeypatch.chdir(tmp_path)
_set_test_home(monkeypatch, tmp_path)
_clear_copilot_route_config(monkeypatch)
monkeypatch.delenv("OPENAI_TARGET_API_URL", raising=False)
with patch.object(wrap_mod, "_ensure_proxy", side_effect=AssertionError("proxy launched")):
result = runner.invoke(
main,
[
"wrap",
"opencode",
"--copilot-subscription",
"--openai-api-url",
"https://api.deepseek.com/v1",
],
)
assert result.exit_code != 0
assert "cannot be combined with --copilot-subscription" in result.output