From c8310819a4221b0d120436786fc499a24c8e55f1 Mon Sep 17 00:00:00 2001 From: Nestor G Pestelos Jr Date: Mon, 17 Aug 2026 06:04:59 +0800 Subject: [PATCH] fix(wrap): set xAI upstream for grok-build proxy (#2772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description `headroom wrap grok-build` injected the client hop into `~/.grok/config.toml` but started the local proxy **without** setting the OpenAI-compatible upstream to xAI. The proxy defaulted to `api.openai.com`, so Grok session auth returned **401** on every chat completion even though compression still ran. `wrap grok` already passes `openai_api_url` → xAI. This PR aligns `wrap grok-build` and the Grok-only persistent `install` path on the shared `DEFAULT_API_URL` (`https://api.x.ai`). Closes # (none — discovered in live Grok Build pilot) ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which 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 - Pass `openai_api_url=_GROK_DEFAULT_API_URL` into `_run_proxy_only_watcher` from `wrap grok-build` - Use shared `DEFAULT_API_URL` from `wrap grok` (no hard-coded string drift) - Print proxy upstream in Grok Build setup lines - Persistent install: when targets are Grok-only, set `OPENAI_TARGET_API_URL` + `--openai-api-url` (skip when Codex/Copilot/Aider/OpenCode share the proxy; explicit env still wins) - Regression tests for wrap kwargs, setup lines, and install planner ## Testing - [x] Unit tests pass (`pytest` targeted suite) - [ ] Linting passes (`ruff check .`) — not run in this environment (no native editable build) - [ ] Type checking passes (`mypy headroom`) — not run - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ PYTHONPATH=$PWD python -m pytest \ tests/test_cli/test_wrap_bridge.py::test_wrap_grok_build_passes_xai_openai_api_url \ tests/test_cli/test_wrap_bridge.py::test_wrap_grok_build_uses_actual_proxy_port \ tests/test_install/test_planner.py::test_build_manifest_grok_build_only_sets_xai_upstream \ tests/test_install/test_planner.py::test_build_manifest_grok_with_codex_does_not_force_xai \ tests/test_install/test_planner.py::test_build_manifest_extra_env_wins_over_grok_xai_default \ tests/test_provider_grok_build.py::test_grok_build_setup_lines_include_proxy_url -q ...... 6 passed in 0.33s ``` ## Real Behavior Proof - Environment: macOS (darwin), Headroom 0.33.0 via `uv tool install "headroom-ai[proxy,mcp,code]==0.33.0"`, Grok Build CLI, models `grok-build` and `grok-4.5`, proxy on `127.0.0.1:8787`, upstream must be xAI - Exact command / steps: (1) Before: stock `headroom wrap grok-build` then `grok -m grok-build` one-shot prompt. (2) After: same wrap path with this branch (`openai_api_url=DEFAULT_API_URL` into `_run_proxy_only_watcher`) then `grok -m grok-build -p '…HEADROOM_XAI_OK…'`. Also exercised `grok-4.5` via `[model."grok-4.5"] base_url` → same proxy. - Observed result: Before — proxy log outbound `api.openai.com` → HTTP 401; client failed while local compression still ran. After — setup line prints Proxy upstream `https://api.x.ai`; proxy log `POST https://api.x.ai/v1/chat/completions` (and `/v1/responses` for grok-4.5) → status=200; dashboard shows 0 failed requests and accumulating token savings on live traffic. - Not tested: full `uv run` editable/maturin native build on this host; multi-tool install matrix beyond planner unit tests; Windows; ruff/mypy full tree ## 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 commented my code, particularly in hard-to-understand areas - [ ] I made corresponding changes to the documentation (CLI help text / setup lines only) - [x] My changes generate no new warnings - [x] I 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` — generated by release-please from Conventional Commit PR title (a CI guard enforces this) ## Additional Notes - Intentional non-goal: changing default model, savings %, or Grok Build context-tool defaults - Mixed-target install (e.g. `grok_build` + `codex`) does **not** force xAI — operator must set upstream explicitly if they share one proxy - Related live routing: manual `[model."grok-4.5"] base_url` through the same proxy works once upstream is xAI (`/v1/responses`) --------- Co-authored-by: Grok 4.5 Co-authored-by: Nestor G Pestelos Jr Co-authored-by: JD Davis --- headroom/cli/wrap.py | 18 ++++++++---- headroom/install/planner.py | 17 ++++++++++++ headroom/providers/grok_build/runtime.py | 3 ++ tests/test_cli/test_wrap_bridge.py | 34 +++++++++++++++++++++++ tests/test_install/test_planner.py | 35 ++++++++++++++++++++++++ tests/test_provider_grok_build.py | 8 ++++++ 6 files changed, 110 insertions(+), 5 deletions(-) diff --git a/headroom/cli/wrap.py b/headroom/cli/wrap.py index 09c6642bb..585e633cc 100644 --- a/headroom/cli/wrap.py +++ b/headroom/cli/wrap.py @@ -136,7 +136,12 @@ from headroom.providers.copilot import ( validate_configuration as _validate_copilot_configuration, ) from headroom.providers.cursor import render_setup_lines as _render_cursor_setup_lines -from headroom.providers.grok import build_launch_env as _build_grok_launch_env +from headroom.providers.grok import ( + DEFAULT_API_URL as _GROK_DEFAULT_API_URL, +) +from headroom.providers.grok import ( + build_launch_env as _build_grok_launch_env, +) from headroom.providers.grok_build import render_setup_lines as _render_grok_build_setup_lines from headroom.providers.grok_build.config import ( inject_grok_provider_config, @@ -6417,7 +6422,7 @@ def grok( backend=backend, anyllm_provider=anyllm_provider, region=region, - openai_api_url="https://api.x.ai", + openai_api_url=_GROK_DEFAULT_API_URL, ) @@ -6507,9 +6512,9 @@ def grok_build( \b Grok Build reads model endpoints from ``~/.grok/config.toml``. This - command starts the proxy, optionally sets up the selected CLI context - tool, injects a Headroom-managed ``[model.grok-build]`` override, and - prints next steps. + command starts the proxy (upstream ``https://api.x.ai``, same as + ``wrap grok``), injects a Headroom-managed ``[model.grok-build]`` + override, and prints next steps. \b Example: @@ -6536,6 +6541,8 @@ def grok_build( for line in _render_grok_build_setup_lines(actual_port, project=project): click.echo(line) + # Client hop is local proxy via config.toml; upstream must be xAI (not + # the OpenAI default). Omitting this caused 401s with Grok auth headers. _run_proxy_only_watcher( agent_label="grok-build", port=port, @@ -6544,6 +6551,7 @@ def grok_build( memory=memory, agent_type="grok_build", print_setup_lines=_print_grok_build_setup, + openai_api_url=_GROK_DEFAULT_API_URL, ) diff --git a/headroom/install/planner.py b/headroom/install/planner.py index 290cff063..518688abf 100644 --- a/headroom/install/planner.py +++ b/headroom/install/planner.py @@ -9,6 +9,7 @@ from collections.abc import Iterable import click from headroom import paths as _paths +from headroom.providers.grok.runtime import DEFAULT_API_URL as _GROK_DEFAULT_API_URL from headroom.providers.install_registry import build_install_target_envs from headroom.rollout import RolloutChannel @@ -177,6 +178,19 @@ def build_manifest( base_env["HEADROOM_TELEMETRY"] = "on" if telemetry_enabled else "off" if memory_enabled: base_env["HEADROOM_MEMORY_ENABLED"] = "1" + # Grok / Grok Build need proxy upstream = xAI. Only auto-set when no other + # OpenAI-compatible tools share this proxy (those may need api.openai.com / + # Copilot). Explicit OPENAI_TARGET_API_URL in extra_env still wins below. + _openai_native = { + ToolTarget.CODEX.value, + ToolTarget.COPILOT.value, + ToolTarget.AIDER.value, + ToolTarget.OPENCODE.value, + } + _grok_targets = {ToolTarget.GROK.value, ToolTarget.GROK_BUILD.value} + target_set = set(resolved_targets) + if target_set & _grok_targets and not (target_set & _openai_native): + base_env.setdefault("OPENAI_TARGET_API_URL", _GROK_DEFAULT_API_URL) # Applied last so explicit --env overrides win over the auto-derived # defaults above (e.g. a custom HEADROOM_WORKSPACE_DIR). if extra_env: @@ -241,6 +255,9 @@ def build_manifest( proxy_args.extend(["--protect-tool-results", protect_tool_results]) if bedrock_profile: proxy_args.extend(["--bedrock-profile", bedrock_profile]) + openai_target = base_env.get("OPENAI_TARGET_API_URL") + if openai_target: + proxy_args.extend(["--openai-api-url", openai_target]) container_name = f"headroom-{normalized_profile}" return DeploymentManifest( diff --git a/headroom/providers/grok_build/runtime.py b/headroom/providers/grok_build/runtime.py index f8bc7f123..26ca11e4e 100644 --- a/headroom/providers/grok_build/runtime.py +++ b/headroom/providers/grok_build/runtime.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass +from headroom.providers.grok.runtime import DEFAULT_API_URL from headroom.proxy.project_context import with_project_prefix @@ -41,6 +42,8 @@ def render_setup_lines(port: int, project: str | None = None) -> list[str]: " [model.grok-build]", f' base_url = "{target.base_url}"', "", + f" Proxy upstream (OpenAI-compatible): {DEFAULT_API_URL}", + "", " Start Grok Build in this project directory:", " grok", "", diff --git a/tests/test_cli/test_wrap_bridge.py b/tests/test_cli/test_wrap_bridge.py index c6c98172d..9f79dd8a6 100644 --- a/tests/test_cli/test_wrap_bridge.py +++ b/tests/test_cli/test_wrap_bridge.py @@ -74,6 +74,40 @@ def test_wrap_grok_build_uses_actual_proxy_port(monkeypatch, tmp_path: Path) -> assert "http://127.0.0.1:8787/" not in result.output +def test_wrap_grok_build_passes_xai_openai_api_url(monkeypatch, tmp_path: Path) -> None: + """Grok Build must set proxy upstream to xAI (same as wrap grok). + + Without openai_api_url, the proxy defaults to api.openai.com and Grok + session auth returns 401 on every chat completion. + """ + from headroom.providers.grok import DEFAULT_API_URL + + _set_test_home(monkeypatch, tmp_path) + runner = CliRunner() + captured: dict = {} + + def fake_watcher(**kwargs) -> None: + captured.update(kwargs) + kwargs["print_setup_lines"](kwargs["port"]) + + monkeypatch.setattr("headroom.cli.wrap._run_proxy_only_watcher", fake_watcher) + + result = runner.invoke(main, ["wrap", "grok-build", "--port", "8787"]) + + assert result.exit_code == 0, result.output + assert captured.get("openai_api_url") == DEFAULT_API_URL + # Equality on the constant (not substring containment) keeps CodeQL + # incomplete-url-substring-sanitization quiet while pinning the host. + assert DEFAULT_API_URL == "https://api.x.ai" + expected_upstream = f" Proxy upstream (OpenAI-compatible): {DEFAULT_API_URL}" + upstream_lines = [ + line + for line in result.output.splitlines() + if line.startswith(" Proxy upstream (OpenAI-compatible): ") + ] + assert upstream_lines == [expected_upstream] + + def test_wrap_rejects_retired_context_tool_flag(monkeypatch, tmp_path: Path) -> None: """A surviving --context-tool must fail loudly, not be silently ignored. diff --git a/tests/test_install/test_planner.py b/tests/test_install/test_planner.py index b4389e611..30ab8a070 100644 --- a/tests/test_install/test_planner.py +++ b/tests/test_install/test_planner.py @@ -330,3 +330,38 @@ def test_build_manifest_extra_env_overrides_derived_defaults() -> None: # telemetry_enabled=False in _base_manifest_kwargs would normally set "off"; # an explicit --env must win. assert manifest.base_env["HEADROOM_TELEMETRY"] == "on" + + +def test_build_manifest_grok_build_only_sets_xai_upstream() -> None: + """Persistent install for Grok Build alone must route proxy upstream to xAI.""" + from headroom.providers.grok import DEFAULT_API_URL + + manifest = build_manifest(**_base_manifest_kwargs(targets=["grok_build"], backend="openai")) + + assert manifest.base_env.get("OPENAI_TARGET_API_URL") == DEFAULT_API_URL + idx = manifest.proxy_args.index("--openai-api-url") + assert manifest.proxy_args[idx + 1] == DEFAULT_API_URL + + +def test_build_manifest_grok_with_codex_does_not_force_xai() -> None: + """Do not override OpenAI upstream when OpenAI-native tools share the proxy.""" + manifest = build_manifest( + **_base_manifest_kwargs(targets=["grok_build", "codex"], backend="openai") + ) + + assert "OPENAI_TARGET_API_URL" not in manifest.base_env + assert "--openai-api-url" not in manifest.proxy_args + + +def test_build_manifest_extra_env_wins_over_grok_xai_default() -> None: + manifest = build_manifest( + **_base_manifest_kwargs( + targets=["grok_build"], + backend="openai", + extra_env={"OPENAI_TARGET_API_URL": "https://gateway.example/v1"}, + ) + ) + + assert manifest.base_env["OPENAI_TARGET_API_URL"] == "https://gateway.example/v1" + idx = manifest.proxy_args.index("--openai-api-url") + assert manifest.proxy_args[idx + 1] == "https://gateway.example/v1" diff --git a/tests/test_provider_grok_build.py b/tests/test_provider_grok_build.py index 43ae61e7e..e0918f792 100644 --- a/tests/test_provider_grok_build.py +++ b/tests/test_provider_grok_build.py @@ -5,6 +5,7 @@ from pathlib import Path import pytest +from headroom.providers.grok import DEFAULT_API_URL from headroom.providers.grok_build import build_proxy_targets, render_setup_lines from headroom.providers.grok_build.config import ( inject_grok_provider_config, @@ -46,6 +47,13 @@ def test_grok_build_setup_lines_include_proxy_url() -> None: assert "http://127.0.0.1:8787/v1" in joined assert "[model.grok-build]" in joined + # Exact labeled line equality (not bare host substring containment) so + # CodeQL does not flag incomplete URL substring sanitization. + expected_upstream = f" Proxy upstream (OpenAI-compatible): {DEFAULT_API_URL}" + upstream_lines = [ + line for line in lines if line.startswith(" Proxy upstream (OpenAI-compatible): ") + ] + assert upstream_lines == [expected_upstream] def test_grok_build_build_install_env_returns_proxy_url() -> None: