mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description `headroom wrap claude` is the recommended Claude Code integration, but for subscription users entitled to the **1M** context window it silently caps usable context at **200k**. Root cause (upstream, anthropics/claude-code#68522): when `ANTHROPIC_BASE_URL` points at a custom host (the Headroom proxy), Claude Code does **not** send the `context-1m-2025-08-07` beta header and treats the window as 200k. The `/model opus[1m]` picker selection does not survive a custom base URL, and `CLAUDE_CODE_AUTO_COMPACT_WINDOW` alone does not lift the cap. Headroom itself already forwards `anthropic-beta` and sizes Opus at 1M internally — but since `wrap claude` owns the launched process's environment and is the documented path, users hit this and blame Headroom first. This adds the opt-in fix the issue proposes. Closes #1158 ## 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 - `headroom/cli/wrap.py`: new opt-in `--1m` flag on `wrap claude`. When set, `ANTHROPIC_MODEL=<opus>[1m]` is exported on the launched process so Claude Code sends the `context-1m` beta header. Logic extracted to a testable helper `_resolve_1m_model`: a model the user already selected via `ANTHROPIC_MODEL` is preserved (only the `[1m]` suffix is appended when missing); otherwise it falls back to the default Opus. Idempotent (no double suffix). Default behavior is unchanged (opt-in). - `tests/test_cli/test_wrap_helpers.py`: unit tests for `_resolve_1m_model` (append-to-user-model, idempotent, default fallback). - `README.md`: `--1m` added to the Claude Code row of the agent compatibility matrix. - `CHANGELOG.md`: Unreleased → Features 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_cli/test_wrap_helpers.py tests/test_cli/test_wrap_claude_base_url.py -q 61 passed in 0.46s $ uv run ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py All checks passed! $ uv run mypy headroom/cli/wrap.py Success: no issues found in 1 source file ``` #### TDD verification (RED → GREEN) RED — new tests with the prod change reverted (`_resolve_1m_model` absent): ```text E AttributeError: module 'headroom.cli.wrap' has no attribute '_resolve_1m_model' 3 failed, 40 deselected in 0.56s ``` GREEN — with the change applied: ```text 3 passed, 40 deselected in 0.34s ``` ## Real Behavior Proof - Environment: Linux, Python 3.13, headroom @ this branch. - Exact command / steps: `headroom wrap claude --1m --help` shows the new flag, and the flag resolves the model id that triggers the 1M window: ```text $ headroom wrap claude --help | grep -A1 -- --1m --1m Preserve the 1M context window. Behind a custom ANTHROPIC_BASE_URL Claude Code drops the ... # model-id resolution (what --1m exports as ANTHROPIC_MODEL): _resolve_1m_model("claude-opus-4-1-20250805") -> "claude-opus-4-1-20250805[1m]" _resolve_1m_model("claude-opus-4-8[1m]") -> "claude-opus-4-8[1m]" (idempotent) _resolve_1m_model(None) -> "claude-opus-4-8[1m]" (default) ``` - Observed result: with `--1m`, the launched Claude Code process gets `ANTHROPIC_MODEL=<opus>[1m]`, which is the documented trigger for the `context-1m` beta header (verified in the issue against `~/.headroom/logs/proxy.log`). - Not tested: the live Claude Code subscription handshake against Anthropic's servers (requires a 1M-entitled subscription + the proprietary client); the model-id → header behavior is Claude Code's, documented in the issue and upstream anthropics/claude-code#68522. Headroom's side (export the env var that flips it on) is covered above and by the unit tests. ## 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 Opt-in only — without `--1m` nothing changes. The `_DEFAULT_1M_MODEL` constant is only consulted when the user has no `ANTHROPIC_MODEL` set; users on a specific model keep it (suffix appended), so the default's freshness does not affect them. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e6bbc40b11
commit
b50d9c17ce
4 changed files with 75 additions and 1 deletions
|
|
@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Features
|
||||
|
||||
* **wrap:** `headroom wrap claude --1m` preserves the 1M context window. Behind a custom `ANTHROPIC_BASE_URL` (the proxy) Claude Code drops the `context-1m` beta header and caps the window at 200k for entitled subscription users; the opt-in flag sets `ANTHROPIC_MODEL=<opus>[1m]` on the launched process so the 1M window activates through Headroom. A model already selected via `ANTHROPIC_MODEL` is preserved (only the `[1m]` suffix is appended) ([#1158](https://github.com/chopratejas/headroom/issues/1158)).
|
||||
|
||||
### Changed
|
||||
|
||||
* **telemetry:** anonymous usage telemetry is now **opt-in** (off by default) instead of opt-out. Nothing is collected or sent unless you set `HEADROOM_TELEMETRY=on` or pass `--telemetry` to `headroom proxy` / `headroom install apply`. `is_telemetry_enabled()` is fail-closed — only explicit on-values (`on`/`true`/`1`/`yes`/`enable`/`enabled`) enable it; unset, empty, or unrecognized values stay disabled. The existing `--no-telemetry` flag and `HEADROOM_TELEMETRY=off` remain accepted for back-compat, and install manifests now write the `HEADROOM_TELEMETRY` value explicitly so generated deployments are unambiguous.
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ shows an **Output Tokens Saved** card next to input compression, labelled
|
|||
|
||||
| Agent | `headroom wrap` | Notes |
|
||||
|--------------|:---------------:|----------------------------------|
|
||||
| Claude Code | ✅ | `--memory` · `--code-graph` |
|
||||
| Claude Code | ✅ | `--memory` · `--code-graph` · `--1m` |
|
||||
| Codex | ✅ | shares memory with Claude |
|
||||
| Cursor | ✅ | prints config — paste once |
|
||||
| Aider | ✅ | starts proxy + launches |
|
||||
|
|
|
|||
|
|
@ -146,6 +146,29 @@ _TOOL_SEARCH_ENV = TOOL_SEARCH_ENV
|
|||
_TOOL_SEARCH_DEFAULT = TOOL_SEARCH_DEFAULT
|
||||
_AGENT_SAVINGS_WRAP_AGENTS = {"claude", "codex", "cursor"}
|
||||
|
||||
# 1M context window for `wrap claude` (#1158). Claude Code only sends the
|
||||
# `context-1m` beta header — unlocking the 1M window for entitled subscription
|
||||
# users — when the model id carries the `[1m]` suffix. Behind a custom
|
||||
# ANTHROPIC_BASE_URL (the proxy) its `/model` picker selection does not survive,
|
||||
# so `--1m` forces the suffix via ANTHROPIC_MODEL on the launched process.
|
||||
_ANTHROPIC_MODEL_ENV = "ANTHROPIC_MODEL"
|
||||
_CONTEXT_1M_SUFFIX = "[1m]"
|
||||
# Only used when no model is otherwise selected (no ANTHROPIC_MODEL set). The
|
||||
# current default Opus; the suffix logic preserves any model the user did set.
|
||||
_DEFAULT_1M_MODEL = "claude-opus-4-8"
|
||||
|
||||
|
||||
def _resolve_1m_model(current: str | None) -> str:
|
||||
"""Return the model id that makes Claude Code request the 1M window (#1158).
|
||||
|
||||
Preserves a model the user already selected via ``ANTHROPIC_MODEL`` (only
|
||||
appending the ``[1m]`` suffix when missing); falls back to the default Opus
|
||||
when none is set. Idempotent — a value already ending in ``[1m]`` is
|
||||
returned unchanged.
|
||||
"""
|
||||
base = (current or "").strip() or _DEFAULT_1M_MODEL
|
||||
return base if base.endswith(_CONTEXT_1M_SUFFIX) else f"{base}{_CONTEXT_1M_SUFFIX}"
|
||||
|
||||
|
||||
def _normalize_tool_search_mode(value: str) -> str:
|
||||
"""Validate an ``ENABLE_TOOL_SEARCH`` value and return it normalized.
|
||||
|
|
@ -3086,6 +3109,17 @@ def unwrap() -> None:
|
|||
default=None,
|
||||
help="Cloud region for Vertex/Bedrock backends (env: HEADROOM_REGION).",
|
||||
)
|
||||
@click.option(
|
||||
"--1m",
|
||||
"context_1m",
|
||||
is_flag=True,
|
||||
help=(
|
||||
"Preserve the 1M context window. Behind a custom ANTHROPIC_BASE_URL "
|
||||
"Claude Code drops the context-1m beta header and caps at 200k; this "
|
||||
"sets ANTHROPIC_MODEL=<opus>[1m] on the launched process so the 1M "
|
||||
"window activates through the proxy (issue #1158)."
|
||||
),
|
||||
)
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
||||
@click.option("--prepare-only", is_flag=True, hidden=True)
|
||||
@click.argument("claude_args", nargs=-1, type=click.UNPROCESSED)
|
||||
|
|
@ -3101,6 +3135,7 @@ def claude(
|
|||
tool_search: str | None,
|
||||
backend: str | None,
|
||||
region: str | None,
|
||||
context_1m: bool,
|
||||
verbose: bool,
|
||||
prepare_only: bool,
|
||||
claude_args: tuple,
|
||||
|
|
@ -3121,6 +3156,7 @@ def claude(
|
|||
headroom wrap claude --no-context-tool # Skip CLI context-tool setup
|
||||
headroom wrap claude --no-mcp # Skip MCP retrieve tool registration
|
||||
headroom wrap claude --no-serena # Skip Serena MCP registration
|
||||
headroom wrap claude --1m # Preserve the 1M context window
|
||||
"""
|
||||
if prepare_only:
|
||||
if not no_rtk:
|
||||
|
|
@ -3321,6 +3357,16 @@ def claude(
|
|||
"(using your existing environment value)"
|
||||
)
|
||||
|
||||
# Issue #1158: opt-in 1M context window. Claude Code only sends the
|
||||
# context-1m beta header when the model id carries the [1m] suffix, so
|
||||
# force it via ANTHROPIC_MODEL on the launched process.
|
||||
if context_1m:
|
||||
env[_ANTHROPIC_MODEL_ENV] = _resolve_1m_model(env.get(_ANTHROPIC_MODEL_ENV))
|
||||
click.echo(
|
||||
f" {_ANTHROPIC_MODEL_ENV}={env[_ANTHROPIC_MODEL_ENV]} "
|
||||
"(1M context window; issue #1158)"
|
||||
)
|
||||
|
||||
result = subprocess.run([claude_bin, *claude_args], env=env)
|
||||
raise SystemExit(result.returncode)
|
||||
|
||||
|
|
|
|||
|
|
@ -765,3 +765,27 @@ def test_ensure_proxy_already_running_prints_dashboard_url(
|
|||
output = _run_in_click_context(lambda: wrap_mod._ensure_proxy(port, no_proxy=False))
|
||||
|
||||
assert f"http://127.0.0.1:{port}/dashboard" in output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_1m_model — 1M context window suffix logic (#1158).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_resolve_1m_model_appends_suffix_to_user_model() -> None:
|
||||
"""A model the user already selected via ANTHROPIC_MODEL is preserved, with
|
||||
only the [1m] suffix appended so Claude Code requests the 1M window."""
|
||||
assert wrap_mod._resolve_1m_model("claude-opus-4-1-20250805") == (
|
||||
"claude-opus-4-1-20250805[1m]"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_1m_model_is_idempotent() -> None:
|
||||
"""A model that already carries [1m] is returned unchanged (no double suffix)."""
|
||||
assert wrap_mod._resolve_1m_model("claude-opus-4-8[1m]") == "claude-opus-4-8[1m]"
|
||||
|
||||
|
||||
def test_resolve_1m_model_falls_back_to_default_when_unset() -> None:
|
||||
"""With no model selected, fall back to the default Opus carrying [1m]."""
|
||||
assert wrap_mod._resolve_1m_model(None) == "claude-opus-4-8[1m]"
|
||||
assert wrap_mod._resolve_1m_model(" ") == "claude-opus-4-8[1m]"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue