mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Merge 65ace61663 into d30e3e052d
This commit is contained in:
commit
c384deeef2
5 changed files with 207 additions and 6 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue