[FEATURE] Publish each model's context window in the initialize handshake (SDK hosts cannot obtain it)

Status Open
Reported on v2.1.287
Maintainer reply None cached
Activity 0 comments · opened Oct 2, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

The initialize control response lists the account's available models, and SDK hosts use it to build their model picker. Each row carries capability flags but no context window, so a host that needs to budget a conversation has no way to ask Claude Code how large the model's window is.

Verified on 2.1.287, every field present on a model row:

value, resolvedModel, displayName, description,
supportsEffort, supportedEffortLevels,
supportsAdaptiveThinking, supportsFastMode, supportsAutoMode

The window is the one number a host must have and the one it cannot get. Every SDK host therefore hardcodes a per-model table, which goes stale the day a model ships and silently mis-budgets until someone notices and edits it.

The failure is quiet, which is what makes it expensive. If a host guesses 200K for a 1M model, nothing errors — the conversation is simply capped at a fifth of the window. If it guesses 1M for a 200K model, the CLI rejects the request at its gateway default and the user sees a size error that appears to blame the server.

Hosts have tried to derive the window from the one signal that was available: the [1m] suffix on a resolved route. That inference broke in 2.1.284. The current generation is now announced as bare family aliases:

{ "value": "opus",   "resolvedModel": "claude-opus-5-5",  "displayName": "Opus"   }
{ "value": "sonnet", "resolvedModel": "claude-sonnet-5-5", "displayName": "Sonnet" }

No [1m] anywhere, though both are 1M models. Meanwhile the suffix survives on secondary full-id rows, and there it rides value while resolvedModel stays bare:

{ "value": "claude-fable-5-1[1m]", "resolvedModel": "claude-fable-5-1", "displayName": "Fable" }

So the suffix is present exactly where it is least needed and absent on the newest models, where getting it wrong costs the most. A derivation built on it silently downgraded both Opus 5.5 and Sonnet 5.5 to 200K the day each shipped. This is not a complaint about that payload change — it is evidence that the suffix was never a specified window signal, and hosts should not be inferring from it at all.

Related but distinct: #85499 covers Claude Code's own enforcement for unknown-model windows. This request is about the CLI publishing the window it already knows to SDK hosts.

Proposed Solution

Add the window to each model row in the initialize control response, alongside the existing capability flags:

{
  "value": "sonnet",
  "resolvedModel": "claude-sonnet-5-5",
  "displayName": "Sonnet",
  "description": "Sonnet 5.5 · Efficient for routine tasks",
  "supportsEffort": true,
  "supportedEffortLevels": ["low", "medium", "high", "xhigh", "max"],
  "supportsAdaptiveThinking": true,
  "supportsAutoMode": true,

  "contextWindow": 1000000,     // NEW — tokens, for the route as resolved
  "maxOutputTokens": 128000     // NEW (optional but equally unobtainable today)
}

Three properties matter more than the exact field names:

  1. It should describe the route as actually resolved. If an account reaches a model only at the short window, the row says so, rather than the host inferring from a naming convention.
  2. Absence should stay meaningful. If a field is omitted the host must read that as unknown, never as a default. The present bug is that silence gets read as a negative fact; a published field with honest omission fixes that.
  3. It should cost no upstream request. initialize is already a local handshake. Keeping this in the same response means discovery stays free, which is the property that makes it usable at picker-build time.

maxOutputTokens is listed because it has the same shape of problem, but contextWindow alone would resolve the reported failures. Happy to split that out if preferred.

Alternative Solutions

All three in use today, all unsatisfactory:

| Approach | Why it fails |
|---|---|
| Infer 1M from the [1m] suffix | Broke in 2.1.284 — current models are announced as bare aliases. Never a documented contract. |
| Hardcode a per-model table | Goes stale the day a model ships; needs a release to correct, so every new model is wrong first. |
| Scrape the docs / changelog | Not machine-readable, lags the CLI, and cannot express per-account differences. |

A read-only CLI subcommand (claude models --json) would also work if adding fields to the handshake is undesirable. The requirement is a machine-readable window from the binary that already knows it — not specifically the handshake.

Priority

High - Significant impact on productivity

Feature Category

Developer tools/SDK

Use Case Example

  1. A host builds its model picker from the initialize handshake at startup. Discovery is local, so it costs nothing.
  2. Anthropic ships Sonnet 5.5 at 1M. The user upgrades to 2.1.284+ and the model appears in the picker immediately — visibility already works correctly.
  3. The host needs the window to budget the conversation. The handshake does not provide it, so the host falls back to a hardcoded table that predates the model, or to a suffix heuristic that no longer fires.
  4. Either way it concludes 200K. The user runs a 1M model at 200K for days with no error anywhere — the model works, it is just quietly five times smaller than they are paying for.
  5. With contextWindow in the row, step 3 is a lookup and step 4 does not happen.

Additional Context

  • Reproduce the payload with a single local handshake, no upstream request:

``sh
printf '%s\n' '{"type":"control_request","request_id":"probe","request":{"subtype":"initialize"}}' \
| claude -p --model sonnet --input-format stream-json --output-format stream-json --verbose \
--tools "" --setting-sources "" --strict-mcp-config --mcp-config '{"mcpServers":{}}' \
--disable-slash-commands --no-session-persistence \
| python3 -c 'import json,sys;[print(json.dumps(json.loads(l)["response"]["response"].get("models"),indent=2)) for l in sys.stdin if l.startswith("{") and json.loads(l).get("type")=="control_response"]'
``

  • Observed on 2.1.287, macOS 27, native installer. Account details omitted; the account object in that response carries subscription info and should be redacted before sharing.
  • Encountered while maintaining a third-party SDK host (a Hermes Agent model provider that drives claude for subscription users). The hardcoded-table and derive-from-suffix approaches above are both from that codebase; this request is the upstream half neither can fix.

View original on GitHub ↗