Skip to content

Commit 2a84725

Browse files
feat(wrap/claude): make the --1m fallback model configurable via HEADROOM_1M_MODEL (#2983)
## Description The model `headroom wrap claude --1m` falls back to (when no model is otherwise selected) was a hardcoded constant `claude-opus-4-8`, with no env var or config key to override it. So it goes stale with every new Opus release, and the only workaround is pinning `ANTHROPIC_MODEL` globally -- which also changes every non-`--1m` session and overrides Claude Code's own `/model` picker. The knob the user actually wants ("what should `--1m` default to") did not exist (#2937). ## Fix Add a `HEADROOM_1M_MODEL` env override that `_resolve_1m_model` consults for its fallback default, and bump the built-in default to `claude-opus-5` (Opus 5 has shipped): ```python _1M_MODEL_ENV = "HEADROOM_1M_MODEL" _DEFAULT_1M_MODEL = "claude-opus-5" def _resolve_1m_model(current: str | None) -> str: fallback = (os.environ.get(_1M_MODEL_ENV) or "").strip() or _DEFAULT_1M_MODEL base = (current or "").strip() or fallback return base if base.endswith(_CONTEXT_1M_SUFFIX) else f"{base}{_CONTEXT_1M_SUFFIX}" ``` Precedence is unchanged: an explicit `ANTHROPIC_MODEL` (or a pass-through `--model`, via the existing `_apply_1m_to_claude_args`) still wins. `HEADROOM_1M_MODEL` only supplies the fallback when nothing else is selected. The `[1m]` suffixing and idempotency are unchanged. Fixes #2937 ## 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `docs/content/docs/configuration.mdx`: document `HEADROOM_1M_MODEL` (new "Claude 1M context window" subsection covering `--1m` resolution order and `[1m]` acceptance) and register it in the Environment Variables catalog with its current default. - `tests/test_cli/test_wrap_helpers.py`: assert the knob stays documented and the documented default tracks `_DEFAULT_1M_MODEL`, so it cannot silently drift. - `headroom/cli/wrap.py`: add `HEADROOM_1M_MODEL` env override in `_resolve_1m_model`; bump `_DEFAULT_1M_MODEL` to `claude-opus-5`. - `tests/test_cli/test_wrap_helpers.py`: env override wins the fallback; an explicit current model still wins over the env; blank env falls back to the built-in; env value is idempotent for an already-`[1m]` value. Updated the existing "falls back to default" test to assert against the constant (robust to future bumps) and to clear the env var. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added ### Test Output ```text tests/test_cli/test_wrap_helpers.py -k "resolve_1m or apply_1m" 11 passed tests/test_cli/test_wrap_claude_vertex_proxy_env.py -k 1m 4 passed # uvx ruff@0.15.22 check -> All checks passed! # uvx mypy@1.20.2 headroom/cli/wrap.py -> Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12.11, project venv, pytest 9.1.1, ruff 0.15.22 and mypy 1.20.2 via uvx. - Exact command / steps: exercised `_resolve_1m_model` directly with the env var set/unset. With `HEADROOM_1M_MODEL=claude-opus-9` and no `ANTHROPIC_MODEL`, `--1m` resolves to `claude-opus-9[1m]`; with the env var unset it resolves to `claude-opus-5[1m]`; a set `ANTHROPIC_MODEL` (e.g. `claude-sonnet-5`) still wins as `claude-sonnet-5[1m]`. - Observed result: operators can point `--1m` at the current Opus without a code change and without pinning `ANTHROPIC_MODEL` globally, and a fresh install no longer silently opts `--1m` into the previous generation. - Not tested: a live Claude Code 1M session (no entitled account here). The resolution is verified at the helper the launch path uses. ## Runtime Rollout Safety - Rollout-managed feature(s): none. `wrap claude --1m` model resolution is a launch-time CLI helper, not a rollout-channel-gated runtime feature. - Minimum rollout channel: N/A (no rollout-managed behavior). - Stable/default behavior changed: yes, narrowly. The built-in `--1m` fallback default moves from `claude-opus-4-8` to `claude-opus-5` only when neither `HEADROOM_1M_MODEL` nor `ANTHROPIC_MODEL` is set; any explicit selection is unaffected. - Kill switch / disable path: set `HEADROOM_1M_MODEL` (or `ANTHROPIC_MODEL`) to pin any model; both override the default. - Unsafe override required: no. - Qualification impact: none. No proxy request path, routing, or token accounting is touched. - Rollback path: revert this PR, or set `HEADROOM_1M_MODEL=claude-opus-4-8` to restore the prior default without a code change. ## 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 - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md`: it is generated by release-please from my Conventional Commit PR title ## Additional Notes The default bump (`claude-opus-4-8` -> `claude-opus-5`) is the second half of the issue's request. If you would rather keep the constant and ship only the env override, I can drop that one line; the override alone already lets operators avoid the stale default. --------- Co-authored-by: JD Davis <mxjerrett@gmail.com>
1 parent ddd9f76 commit 2a84725

3 files changed

Lines changed: 82 additions & 11 deletions

File tree

docs/content/docs/configuration.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,24 @@ response = client.chat.completions.create(
210210

211211
The `RollingWindowConfig`, `IntelligentContextConfig`, and `ScoringWeights` classes are no longer part of Headroom. Context management now happens automatically inside the pipeline (live-zone-only compression).
212212

213+
### Claude 1M context window (`headroom wrap claude --1m`)
214+
215+
`headroom wrap claude --1m` opts a Claude Code session into Anthropic's 1M-token context window by selecting a `[1m]`-suffixed model id, which makes Claude Code send the `context-1m` beta header. The model that `--1m` targets is resolved in this order:
216+
217+
1. an explicit `--model` / `ANTHROPIC_MODEL` value (used as-is, with a `[1m]` suffix appended when missing),
218+
2. otherwise `HEADROOM_1M_MODEL`, when set,
219+
3. otherwise the built-in default (currently `claude-opus-5`).
220+
221+
Set `HEADROOM_1M_MODEL` to point `--1m` at a specific model without pinning `ANTHROPIC_MODEL` globally, so the default can follow a new Opus generation without a code change:
222+
223+
```bash
224+
# Route --1m at a specific model for this shell / session
225+
export HEADROOM_1M_MODEL=claude-opus-5
226+
headroom wrap claude --1m
227+
```
228+
229+
`HEADROOM_1M_MODEL` is a fallback only: an explicit `--model` or `ANTHROPIC_MODEL` always wins. The value may be given with or without the `[1m]` suffix; both `claude-opus-5` and `claude-opus-5[1m]` are accepted, and the suffix is added when absent.
230+
213231
## Pipeline Extensions
214232

215233
Use a `headroom.pipeline_extension` entry point when you need to normalize or annotate requests before they leave Headroom. The `PRE_SEND` stage is the right place for provider-specific request cleanup, such as turning `content: null` into `content: ""` for upstreams that reject OpenAI-spec tool-call messages.
@@ -317,6 +335,7 @@ headroom proxy --learn --min-evidence 3
317335
| `HEADROOM_DEDUPE` | Whole-conversation verbatim cross-turn dedup in the router (cache-safe, information-preserving via retrieval markers). Superseded-read drop + lossless folds run without it; this adds verbatim dedup. | `off` |
318336
| `HEADROOM_CACHE_TTL_LEARN` | Append per-turn cache-outcome observations (provider, model, idle, hit/miss) to `cache_ttl_observations.jsonl` for the offline `headroom-cache-ttl` learner. Observation-only (no request-behavior change); respects `HEADROOM_STATELESS`; the log is size-bounded. | `off` |
319337
| `HEADROOM_KOMPRESS_ENDPOINT` / `HEADROOM_KOMPRESS_ENDPOINT_TOKEN` | Offload ML compression (Kompress) to a remote endpoint instead of the local ONNX model — used by reasoning compaction and the router when set. | -- |
338+
| `HEADROOM_1M_MODEL` | Fallback model that `headroom wrap claude --1m` targets when neither `--model` nor `ANTHROPIC_MODEL` is set. Accepts the id with or without the `[1m]` suffix (added when absent); an explicit `--model` / `ANTHROPIC_MODEL` always wins. See [Claude 1M context window](#claude-1m-context-window-headroom-wrap-claude---1m). | `claude-opus-5` |
320339

321340
For provider-only proxying, prefer `HEADROOM_HTTP_PROXY` over process-wide variables such as `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, or `NO_PROXY`. HTTPX reads those global variables, but Headroom also passes them through to tool executions.
322341

headroom/cli/wrap.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -321,21 +321,26 @@ def _append_text(path: Path, content: str) -> None:
321321
# so `--1m` forces the suffix via ANTHROPIC_MODEL on the launched process.
322322
_ANTHROPIC_MODEL_ENV = "ANTHROPIC_MODEL"
323323
_CONTEXT_1M_SUFFIX = "[1m]"
324-
# Only used when no model is otherwise selected (no ANTHROPIC_MODEL set). The
325-
# current default Opus; the suffix logic preserves any model the user did set.
326-
_DEFAULT_1M_MODEL = "claude-opus-4-8"
324+
_1M_MODEL_ENV = "HEADROOM_1M_MODEL"
325+
# Fallback model for `--1m` when nothing else selects one (no ANTHROPIC_MODEL,
326+
# no explicit --model). Overridable via HEADROOM_1M_MODEL so it can track new
327+
# Opus releases without a code change and without pinning ANTHROPIC_MODEL
328+
# globally (which would also change non-`--1m` sessions and override Claude
329+
# Code's /model picker). #2937.
330+
_DEFAULT_1M_MODEL = "claude-opus-5"
327331
_OPENCLAUDE_INSTRUCTIONS_FILE = "CONVENTIONS.md"
328332

329333

330334
def _resolve_1m_model(current: str | None) -> str:
331335
"""Return the model id that makes Claude Code request the 1M window (#1158).
332336
333337
Preserves a model the user already selected via ``ANTHROPIC_MODEL`` (only
334-
appending the ``[1m]`` suffix when missing); falls back to the default Opus
335-
when none is set. Idempotent — a value already ending in ``[1m]`` is
336-
returned unchanged.
338+
appending the ``[1m]`` suffix when missing). When none is set it falls back
339+
to ``HEADROOM_1M_MODEL`` if defined, else the built-in default Opus (#2937).
340+
Idempotent — a value already ending in ``[1m]`` is returned unchanged.
337341
"""
338-
base = (current or "").strip() or _DEFAULT_1M_MODEL
342+
fallback = (os.environ.get(_1M_MODEL_ENV) or "").strip() or _DEFAULT_1M_MODEL
343+
base = (current or "").strip() or fallback
339344
return base if base.endswith(_CONTEXT_1M_SUFFIX) else f"{base}{_CONTEXT_1M_SUFFIX}"
340345

341346

tests/test_cli/test_wrap_helpers.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -752,10 +752,57 @@ def test_resolve_1m_model_is_idempotent() -> None:
752752
assert wrap_mod._resolve_1m_model("claude-opus-4-8[1m]") == "claude-opus-4-8[1m]"
753753

754754

755-
def test_resolve_1m_model_falls_back_to_default_when_unset() -> None:
756-
"""With no model selected, fall back to the default Opus carrying [1m]."""
757-
assert wrap_mod._resolve_1m_model(None) == "claude-opus-4-8[1m]"
758-
assert wrap_mod._resolve_1m_model(" ") == "claude-opus-4-8[1m]"
755+
def test_resolve_1m_model_falls_back_to_default_when_unset(
756+
monkeypatch: pytest.MonkeyPatch,
757+
) -> None:
758+
"""With no model selected, fall back to the built-in default carrying [1m]."""
759+
monkeypatch.delenv("HEADROOM_1M_MODEL", raising=False)
760+
expected = f"{wrap_mod._DEFAULT_1M_MODEL}[1m]"
761+
assert wrap_mod._resolve_1m_model(None) == expected
762+
assert wrap_mod._resolve_1m_model(" ") == expected
763+
764+
765+
def test_resolve_1m_model_env_overrides_builtin_default(monkeypatch: pytest.MonkeyPatch) -> None:
766+
"""HEADROOM_1M_MODEL overrides the built-in fallback so --1m can track new
767+
Opus releases without a code change or pinning ANTHROPIC_MODEL (#2937)."""
768+
monkeypatch.setenv("HEADROOM_1M_MODEL", "claude-opus-9")
769+
assert wrap_mod._resolve_1m_model(None) == "claude-opus-9[1m]"
770+
771+
772+
def test_resolve_1m_model_current_wins_over_env(monkeypatch: pytest.MonkeyPatch) -> None:
773+
"""An explicit ANTHROPIC_MODEL still wins; HEADROOM_1M_MODEL is only the
774+
fallback default when nothing else is selected."""
775+
monkeypatch.setenv("HEADROOM_1M_MODEL", "claude-opus-9")
776+
assert wrap_mod._resolve_1m_model("claude-sonnet-5") == "claude-sonnet-5[1m]"
777+
778+
779+
def test_resolve_1m_model_env_idempotent_on_suffixed_value(
780+
monkeypatch: pytest.MonkeyPatch,
781+
) -> None:
782+
"""A HEADROOM_1M_MODEL that already carries [1m] is not double-suffixed."""
783+
monkeypatch.setenv("HEADROOM_1M_MODEL", "claude-opus-9[1m]")
784+
assert wrap_mod._resolve_1m_model(None) == "claude-opus-9[1m]"
785+
786+
787+
def test_resolve_1m_model_blank_env_falls_back_to_builtin(monkeypatch: pytest.MonkeyPatch) -> None:
788+
"""A blank/whitespace HEADROOM_1M_MODEL falls back to the built-in default."""
789+
monkeypatch.setenv("HEADROOM_1M_MODEL", " ")
790+
assert wrap_mod._resolve_1m_model(None) == f"{wrap_mod._DEFAULT_1M_MODEL}[1m]"
791+
792+
793+
def test_headroom_1m_model_is_documented_and_default_matches_code() -> None:
794+
"""The HEADROOM_1M_MODEL knob must stay documented, and the documented
795+
default must track the code, so the supported configuration surface cannot
796+
silently drift or disappear (#2937).
797+
"""
798+
docs = Path(__file__).resolve().parents[2] / "docs" / "content" / "docs" / "configuration.mdx"
799+
text = docs.read_text(encoding="utf-8")
800+
assert wrap_mod._1M_MODEL_ENV in text, f"{wrap_mod._1M_MODEL_ENV} is not documented"
801+
# The env-var catalog row must advertise the current built-in default.
802+
assert f"`{wrap_mod._DEFAULT_1M_MODEL}`" in text, (
803+
"documented HEADROOM_1M_MODEL default is out of sync with "
804+
f"_DEFAULT_1M_MODEL={wrap_mod._DEFAULT_1M_MODEL!r}"
805+
)
759806

760807

761808
class TestFindAvailablePort:

0 commit comments

Comments
 (0)