Headroom Configuration Scopes and Precedence
Learning Headroom
From Pipeline to Knobs: Why Precedence Matters
Builds on ← 3.1 Headroom Pipeline and Integration Boundaries: the pipeline stages (detect → route → compress) all need *settings* to run — a keep-turns count, an output buffer size, a model's context limit.
**The problem:** the same setting can be specified in up to 4 places at once:
- Built-in defaults
- Environment variables
- SDK constructor arguments (client config)
- Per-request overrides
**The question this node answers:** if two of these disagree, which one wins — and why?
Where this leads → 6.2 (Context Budget and Output Buffer Tuning) and 6.3 (Simulation-Driven Configuration Comparison) both assume you can trace *any* setting back to the layer that actually controls it.
The Four Scopes, Defined
**1. Default configuration**
Built into Headroom's code. No file, no env var, nothing set by you.
Example: unknown model inferred as 200K context via pattern matching (`*sonnet*`).
**2. Environment configuration**
Set via environment variables or config files on disk.
Examples: `HEADROOM_CONFIG_DIR`, `HEADROOM_MODEL_LIMITS`, `${HEADROOM_CONFIG_DIR}/models.json`.
**3. Constructor configuration**
Passed as arguments when the SDK client is instantiated in code.
Example: `Headroom(model_limits={...})` in Python/JS at startup.
**4. Per-request override**
Passed on a single call, affecting only that request.
Example: `headroom_keep_turns=6`, `headroom_output_buffer_tokens=2000` on one API call.
```mermaid
flowchart LR
A["Default config"] --> B["Environment config"]
B --> C["Constructor config"]
C --> D["Per-request override"]
D --> E["Effective value used"]
```
Each arrow means "can be overridden by" — narrower scope, later in the chain, wins.
The Precedence Rule — Why Narrower Beats Broader
**Precedence rule** (documented resolution order, later overrides earlier):
1. Built-in defaults
2. Config file (`${HEADROOM_CONFIG_DIR}/models.json`, falls back to legacy `~/.headroom/models.json`)
3. Environment variable (`HEADROOM_MODEL_LIMITS`)
4. SDK constructor arguments
5. Per-request override *(most specific — wins for that call only)*
**Why this shape?** Each layer is *more specific* to the situation than the last:
| Scope | Applies to | Set by |
|---|---|---|
| Default | every install | library author |
| Env/config file | one machine/deployment | ops/admin |
| Constructor | one client instance | app developer |
| Per-request | one call | calling code, in-the-moment |
\[ \text{effective value} = \text{last non-empty setting in the chain} \]
If a scope doesn't set a value, it's simply skipped — resolution falls back to the next broader scope.
Worked Example 1: Model Context Limit Resolution
**Setting:** context limit for `claude-4-opus-20250301`
**Given:**
- Default (pattern inference, `*opus*`): 200,000 tokens
- Config file `models.json`: `"claude-4-opus-20250301": 200000`
- Env var `HEADROOM_MODEL_LIMITS`: not set for this model
- Constructor arg: `context_limits={"claude-4-opus-20250301": 190000}`
- Per-request override: none
**Resolve step by step:**
1. Start with default → value = 200,000
2. Check config file → present, value 200,000 → update: value = 200,000
3. Check env var → absent for this model → no change: value = 200,000
4. Check constructor arg → present, value 190,000 → update: value = 190,000
5. Check per-request override → absent → no change: value = 190,000
**Effective value: 190,000 tokens**
The constructor value wins — not because it's "first" but because it's the last scope in the chain that actually set a value.
Worked Example 2: Per-Request Override in the Live Pipeline
**Setting:** `headroom_keep_turns` (how many recent turns stay uncompressed in the live zone)
**Given:**
- Default: 4 turns
- Env var `HEADROOM_KEEP_TURNS`: 8 (set on the deployment server)
- Constructor arg: not passed (client built with no override)
- Per-request override on this call: `headroom_keep_turns=2`
**Resolve step by step:**
1. Start with default → value = 4
2. Check env var → present, value 8 → update: value = 8
3. Check constructor arg → absent → no change: value = 8
4. Check per-request override → present, value 2 → update: value = 2
**Effective value for this call: 2 turns**
Every *other* request to this same client still uses 8 (the env var value) — the override of 2 applies only to this one call.
**Common mix-up to avoid:** setting an env var does NOT permanently change the app's behavior everywhere if per-request overrides exist for specific calls — it only changes the fallback baseline.
Edge Cases and How This Feeds the Next Lessons
**Edge cases in resolution:**
- A scope set to an *empty or null* value is usually treated as "not set" → falls through to the next broader scope (check your SDK's exact null-handling, but this is the standard behavior).
- Legacy paths still resolve: `~/.headroom/models.json` (old location) is used only if `${HEADROOM_CONFIG_DIR}/models.json` (new canonical location) is **absent** — this is itself a mini precedence chain nested inside the "config file" scope.
- Per-resource env vars (`HEADROOM_SAVINGS_PATH`, `HEADROOM_TOIN_PATH`, etc.) are **additive**, not replaced by `HEADROOM_CONFIG_DIR` / `HEADROOM_WORKSPACE_DIR` — both mechanisms coexist.
**General resolution algorithm (any setting):**
```mermaid
flowchart TD
S["Start: value = undefined"] --> D{"Default sets it?"}
D -->|yes| D2["value = default"]
D -->|no| D2
D2 --> E{"Env or config sets it?"}
E -->|yes| E2["value = env/config"]
E -->|no| E2
E2 --> C{"Constructor sets it?"}
C -->|yes| C2["value = constructor"]
C -->|no| C2
C2 --> R{"Per-request sets it?"}
R -->|yes| R2["value = per-request"]
R -->|no| R2
R2 --> F["Effective value"]
```
Where this leads →
- **6.2 Context Budget and Output Buffer Tuning**: tuning `headroom_output_buffer_tokens` means knowing which scope you're editing.
- **6.3 Simulation-Driven Configuration Comparison**: comparing configurations means holding some scopes fixed while varying others — impossible without this resolution model.
بازگشت به دوره