feat(hosted,web): a coding agent may opt into a model's smaller context window, per agent and as the default
Summary
Dean asked for a Context choice in the model picker (like 300K/1M) for models that support it. This PR is the service and wire half. The desktop picker is stacked on top of it in the next PR.
Which models offer more than one window, and how it is decided. AI Gateway's catalog (/v1/models) gives one context_window per model and no "selectable windows" field. It does give price tiers, pricing.input_tiers / output_tiers, with the boundary past which the provider bills a prompt at a higher rate. So the service reads it like this: a model offers two windows when its input is priced differently past a boundary inside its window. The boundary is the smaller "standard" window and the whole window is the "long" one. A model priced the same across its whole window gets one window and no control. There's no hard-coded model list. As the live catalog stands today (2026-10-12):
| Model(s) | Windows | Long-context premium (from the catalog tiers) |
|---|---|---|
| OpenAI GPT-5.4, 5.4-pro, 5.5, 5.5-pro, 5.6 Luna/Sol/Terra, GPT-6 Luna/Sol/Astra, GPT-6.1 Sol | 272K / 1M (1.05M) | 2× input, 1.5× output past 272K (OpenAI pricing) |
| Claude Haiku 5.5 | 100K / 1M | 5× input and output past 100K (Anthropic pricing) |
| Claude Opus 5.5 / 5 / 4.6–4.8, Sonnet 5.5 / 5 / 4.6, Fable 5 / 5.1 | 1M only | none: 1M is the default and maximum at standard price, no beta header |
| Claude Sonnet 4 / 4.5 | (200K / 1M behind the context-1m-2025-08-07 beta) | not offered: the catalog lists no effort for them, so Luke already leaves them out |
No request option or header is needed for any model Luke offers. OpenAI's 1M is the model's own limit, and Haiku 5.5's 1M is its default. Anthropic's 1M beta header only matters for Sonnet 4/4.5, which aren't offered. So the choice changes one thing: the window eve compacts against (modelContextWindowTokens) and that the context ring reads. On the default whole window an agent can run to about 945K. On the opted-in standard window it compacts at about 245K, under OpenAI's premium.
Default: the largest window the model offers (per Dean). The smaller standard window (272K or 100K) is an opt-in. That's also what every agent already ran on, because the coder host told eve the catalog's full context_window. So existing agents and account defaults keep their window with no special-casing: no grandfathering or stamping, and no agent starts compacting at 272K mid-task.
Changes
- Wire (
@sidecar/hosted):catalogModelSchemagets an optionalcontextWindows { standardTokens, longTokens, longInputPriceFactor?, longOutputPriceFactor? }.modelChoiceSchema, the Start request and the agent summary get an optionalcontextWindow: "standard" | "long"(CONTEXT_WINDOW).- A missing
contextWindowmeans the model's whole window, which is what older desktops already get. The desktop only sends"standard"when the smaller window is opted into, so a service that predates this never sees a key it would refuse on the default path. The summary names the window only when it's the standard one.
- Catalog (
model-catalog.ts):- Reads the tiers on their own, so a malformed tier costs a model its long window and not its price.
validateModelChoicekeepsstandardonly for a two-window model. On a one-window model it's accepted and dropped, aslong(the default) always is.
- Store:
- Migration 0077 adds
coding_agent.context_window(text NOT NULL DEFAULT 'long', so every existing row keeps the whole window) andaccount_preference.coding_agent_context_window(nullable; null means the whole window). - Start, PATCH and the account default read and write the window.
- Migration 0077 adds
- Coder host:
contextWindowOf(offered, model, window)picks the chosen window for a step and for the transcript's context. A change applies from the agent's next step, like a model or effort change. - Workers that an agent delegates to run on their model's whole window, as before.
No behavior change for anything that exists. Existing agents, new agents that name no window, workers, and older desktops all run on the model's whole window, as every agent did before this PR. Only an explicit opt-in to the standard window changes anything.
Evidence
- Platform-independent checks:
./scripts/check.shexit 0 (on the stacked head). - macOS Electron verification (
./scripts/verify.sh): not run. This is a Linux cloud VM and CI can't verify the Mac. This PR has no UI. LUKE-159 is the manual Mac pass. - New tests, each failing without the change:
model-catalog.test.ts(+2): catalog tiers → windows and factors (OpenAI-shaped 272K, Haiku-shaped100001boundary, a flat Opus, a boundary at the window's edge, an unreadable tier); standard kept or dropped by validation.coder-host.test.ts(+1): the step'smodelContextWindowTokensand the transcript'scontext.windowTokens(the ring) follow the row's window: 1.05M by default, 272K once the standard window is chosen, and 1.05M again on a change that names none.coding-agent-store.test.ts(+1, PGlite): Start, change and read of the window; a new agent defaults tolong.hosted-account-store.test.ts(+1, PGlite): the default round-trips the window.coding-agents-app.test.ts(+1): Start/PATCH round-trip over the wire, the default carries it, a one-window model drops it, a window without a model or an unknown window is refused.
- Test count: web +6,
@sidecar/hostedschema goldens +2.
Visual evidence
None here: no UI in this PR. Screenshots are on the desktop PR stacked on top.
Invariants
-
./scripts/check.shgreen; renderer/UI PRs also./scripts/verify.shwith evidence inspected. (CI) (no UI in this PR) - JSON Schema goldens: re-recorded with
LUKE_UPDATE_FIXTURES=1. The diff is the new optionalcontextWindowproperty,["standard","long"], onmodelChoiceSchema, the Start request, the agent summary and everything that embeds them (view params and answers, the default read/write), plus the optionalcontextWindowsobject oncatalogModelSchema/modelsAnswerSchema, and two new goldens (contextWindowSchema,contextWindowsSchema). None of these schemas is shown to a model (the voice tool'sstart_coding_agentreadsmodelChoiceSchema.fields.effortalone, which is unchanged), so no prompt cache moves. - Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged. (CI)
- No new
astype assertion outside the allowlisted files. (CI via anti-slop) - No
Effect.runPromise/runSync/runForkoutside the runtime edges. (CI) - No new raw async primitives. (CI)
- No Effect, Stream, or Layer written as a statement and dropped. (CI)
- Test count equals the pre-PR count or the delta is listed. (+6 web, +2 hosted goldens)
- Each hand-rolled file the PR replaces is deleted: n/a.
- Every
CLAUDE.md/AGENTS.mdsentence naming a changed part is edited: none names it.apps/web/server/README.md's preferences and models sections are updated. -
PRIVACY.mdunchanged. - Package
package.jsondeps match what the sources import. (CI) - Barrel/door rule unchanged. (CI)
Notes
- Usage is still costed at a model's base price. The service aggregates tokens per turn, not per request, so it can't apply the long-context tier. A long-window agent past the boundary is under-costed in the menu's "This agent: … $" line. That's a follow-up if wanted.
- Stacked: the desktop picker PR builds on this branch.
🤖 Generated with Claude Code