mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description `headroom install apply --preset persistent-docker` pulls the image, starts the container, then fails after ~45s with "Deployment 'default' did not become ready after start." The rollback removes the container and manifest, leaving nothing running and no logs. Root cause: the published image already bakes the proxy invocation into its ENTRYPOINT (`Dockerfile`: `ENTRYPOINT ["headroom", "proxy"]`), but `build_runtime_command()` in `headroom/install/runtime.py` re-added `headroom proxy` after the image name. Docker concatenates ENTRYPOINT + args, so the container ran `headroom proxy headroom proxy --host 0.0.0.0 ...` and Click aborted with `Got unexpected extra arguments (headroom proxy)`. The runtime command now appends only the proxy flags after the image name, substituting the all-interface container bind host for the host pair carried in `proxy_args`. Closes #833 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] 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 - `headroom/install/runtime.py`: drop the duplicated `headroom proxy` from the docker `build_runtime_command` output; append only `--host <bind> *proxy_args[2:]`. Extracted `CONTAINER_BIND_HOST` and `_PROXY_ARGS_HOST_PAIR_LEN` named constants. - `tests/test_install/test_runtime.py`: new regression test asserting the args appended after the image name never re-add the `headroom proxy` ENTRYPOINT. - `CHANGELOG.md`: Unreleased → Bug Fixes entry. ## 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 pytest tests/test_install/ -q 91 passed, 1 skipped in 5.48s $ uv run ruff check headroom/install/runtime.py tests/test_install/test_runtime.py All checks passed! $ uv run mypy headroom/install/runtime.py Success: no issues found in 1 source file ``` #### RED → GREEN proof RED — new test with the prod fix reverted (test kept): ```text E AssertionError: container args re-add the ENTRYPOINT — got ['headroom', 'proxy', '--host', '0.0.0.0', '--port', '8787', '--backend', 'anthropic'] FAILED tests/test_install/test_runtime.py::test_build_runtime_command_for_docker_does_not_duplicate_entrypoint 1 failed in 0.17s ``` GREEN — with the fix applied: ```text tests/test_install/test_runtime.py::test_build_runtime_command_for_docker_does_not_duplicate_entrypoint 1 passed in 0.11s ``` ## Real Behavior Proof - Environment: Linux, Python 3.13, headroom @ this branch. - Exact command / steps: reproduce the exact concatenation Docker performs (ENTRYPOINT `headroom proxy` + the buggy CMD `headroom proxy --host 0.0.0.0 --port 8787`): ```text $ headroom proxy headroom proxy --host 0.0.0.0 --port 8787 Usage: headroom proxy [OPTIONS] Try 'headroom proxy --help' for help. Error: Got unexpected extra arguments (headroom proxy) ``` This is the exact error from the issue. After the fix, `build_runtime_command` appends only the flags after the image name: ```text args after image: ['--host', '0.0.0.0', '--port', '8787', '--backend', 'anthropic'] ``` so the container runs `headroom proxy --host 0.0.0.0 --port 8787 --backend anthropic` (ENTRYPOINT + flags) and Click accepts it. - Observed result: pre-fix Click aborts with the unexpected-arguments error (container crash-loops); post-fix the command line is valid. - Not tested: pulling and running the real `ghcr.io` image end-to-end (requires the published image + Docker host); the failure is fully determined by the generated argv, which is covered above and by the unit test. ## 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 - [x] 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 have updated the CHANGELOG.md if applicable ## Additional Notes Scope is limited to the docker runtime command construction. The Python (`runtime_kind=python`) path was already correct and is unchanged. Screenshots N/A (CLI-only change). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c9b42a919
commit
feedead077
3 changed files with 54 additions and 4 deletions
|
|
@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* **install:** stop duplicating the container ENTRYPOINT in the `persistent-docker` runtime command. The published image already runs `headroom proxy` as its ENTRYPOINT, but `build_runtime_command` re-added `headroom proxy` after the image name, so the container ran `headroom proxy headroom proxy --host 0.0.0.0 …` and Click aborted with "Got unexpected extra arguments (headroom proxy)" — the deployment never became ready and rollback left nothing running. The runtime command now appends only the proxy flags ([#833](https://github.com/chopratejas/headroom/issues/833)).
|
||||
* **code:** keep Python `from __future__` imports before executable code during AST compression and validate compressed Python with `compile(..., "exec")` so compile-time syntax rules are enforced ([#1233](https://github.com/chopratejas/headroom/issues/1233)).
|
||||
* **proxy:** report real input tokens on the streaming `message_start` event for LiteLLM/Bedrock-backed requests. LiteLLM streaming never surfaces prompt tokens mid-stream, so `message_start.usage.input_tokens` was always `0`; Anthropic clients (e.g. Claude Code) read input-token metrics from that event, underreporting token usage by ~99% in OTel/CloudWatch dashboards. The Bedrock streamer now backfills `input_tokens` with the count Headroom actually sent upstream when the backend leaves it unset, preserving any non-zero value the backend genuinely reports ([#1132](https://github.com/chopratejas/headroom/issues/1132)).
|
||||
* **proxy:** give buffered Anthropic request paths their own longer read timeout, so long `/v1/messages` turns and Anthropic batch or passthrough reads no longer trip the generic proxy cap while unrelated request timeouts stay unchanged.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ from .health import probe_ready
|
|||
from .models import DeploymentManifest, InstallPreset, RuntimeKind
|
||||
from .paths import log_path, pid_path, profile_root
|
||||
|
||||
# Inside the container the proxy must listen on every interface so the
|
||||
# host-side published port (127.0.0.1:<port>) can reach it.
|
||||
CONTAINER_BIND_HOST = "0.0.0.0" # noqa: S104 — container-internal bind, published only on 127.0.0.1
|
||||
# proxy_args always starts with the host flag/value pair (see planner.py); we
|
||||
# drop it and substitute CONTAINER_BIND_HOST for the in-container bind.
|
||||
_PROXY_ARGS_HOST_PAIR_LEN = 2
|
||||
|
||||
PASSTHROUGH_ENV_PREFIXES = (
|
||||
"HEADROOM_",
|
||||
"ANTHROPIC_",
|
||||
|
|
@ -136,14 +143,16 @@ def build_runtime_command(manifest: DeploymentManifest) -> list[str]:
|
|||
for name in sorted(os.environ):
|
||||
if name.startswith(PASSTHROUGH_ENV_PREFIXES):
|
||||
command.extend(["--env", name])
|
||||
# The image ENTRYPOINT already runs `headroom proxy` (see Dockerfile), so
|
||||
# the args appended after the image name are only the proxy flags — never
|
||||
# `headroom proxy` again, or Docker would run `headroom proxy headroom
|
||||
# proxy ...` and Click aborts on the extra arguments (issue #833).
|
||||
command.extend(
|
||||
[
|
||||
manifest.image,
|
||||
"headroom",
|
||||
"proxy",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
*manifest.proxy_args[2:],
|
||||
CONTAINER_BIND_HOST,
|
||||
*manifest.proxy_args[_PROXY_ARGS_HOST_PAIR_LEN:],
|
||||
]
|
||||
)
|
||||
return command
|
||||
|
|
|
|||
|
|
@ -94,6 +94,46 @@ def test_build_runtime_command_for_docker_matches_wrapper_parity(
|
|||
assert "OPENAI_API_KEY" in joined
|
||||
|
||||
|
||||
def test_build_runtime_command_for_docker_does_not_duplicate_entrypoint(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""The image ENTRYPOINT is already ``["headroom", "proxy"]`` (Dockerfile),
|
||||
so the args appended after the image name must NOT re-add ``headroom proxy``
|
||||
or Docker runs ``headroom proxy headroom proxy ...`` and Click aborts with
|
||||
"Got unexpected extra arguments (headroom proxy)" (issue #833)."""
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
manifest = DeploymentManifest(
|
||||
profile="default",
|
||||
preset="persistent-docker",
|
||||
runtime_kind="docker",
|
||||
supervisor_kind="none",
|
||||
scope="user",
|
||||
provider_mode="manual",
|
||||
targets=["claude"],
|
||||
port=8787,
|
||||
host="127.0.0.1",
|
||||
backend="anthropic",
|
||||
image="ghcr.io/chopratejas/headroom:latest",
|
||||
base_env={"HEADROOM_PORT": "8787"},
|
||||
proxy_args=["--host", "127.0.0.1", "--port", "8787", "--backend", "anthropic"],
|
||||
)
|
||||
|
||||
command = build_runtime_command(manifest)
|
||||
|
||||
# Everything after the image name is what Docker appends to the ENTRYPOINT.
|
||||
image_idx = command.index(manifest.image)
|
||||
container_args = command[image_idx + 1 :]
|
||||
assert "headroom" not in container_args, (
|
||||
f"container args re-add the ENTRYPOINT — got {container_args}"
|
||||
)
|
||||
assert "proxy" not in container_args, (
|
||||
f"container args re-add the ENTRYPOINT — got {container_args}"
|
||||
)
|
||||
# The container must still bind on all interfaces and keep the real flags.
|
||||
assert container_args[:2] == ["--host", "0.0.0.0"]
|
||||
assert container_args[2:] == ["--port", "8787", "--backend", "anthropic"]
|
||||
|
||||
|
||||
def test_resolve_headroom_command_prefers_headroom_binary(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"shutil.which", lambda name: "/usr/bin/headroom" if name == "headroom" else None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue