ProviderWindow
Asks a provider what context window it ALLOCATED, and answers in tokens or `null`. ── WHY ASK AT ALL ─────────────────────────────────────────────────────────────────────────────── Until now the number that governs compaction was a human assertion that nothing verified: an app declared `agent.contextTokens` (or exported `MILPA_AGENT_CONTEXT_TOKENS`), and if nobody declared anything the orchestrator gave up on a derived budget and fell back to a fixed cap. A declaration that is too large is not caught by anything until the provider rejects the prompt mid-run — greenhouse evidence/0443 measured exactly that: a 32,768-token model re-entered at 35.6k. The provider knows the answer and will say it. So the house asks (greenhouse decisions/0233). ── ONLY THE ALLOCATED WINDOW COUNTS, AND THE TRAP IS MEASURED ─────────────────────────────────── Measured 2026-09-08 against `qwen3.8-27b` on llama over Tailscale: GET /v1/models → data[0].meta.n_ctx 32768 ← what the SERVER allocated data[0].meta.n_ctx_train 262144 ← what the MODEL was trained for GET /props → default_generation_settings.n_ctx 32768 Eight times apart. A reader that took `n_ctx_train` would believe it had 262k of room and blow up at 32k — a worse defect than not asking, because it arrives with the authority of a measurement. So {@see NOT_A_CEILING} is read by NOBODY here: a number that describes the model rather than the server that serves it is not a ceiling. When the training window is all a provider exposes, the answer is `null` — «I could not ask», never a guess. ── WHY `/v1/models` FIRST AND `/props` SECOND ─────────────────────────────────────────────────── `/v1/models` is the OpenAI-compatible surface this gateway ALREADY drives: every base URL it can talk to answers under `/v1` by construction ({@see LlmService}'s `/v1/chat/completions`), so the primary path asks where the provider is already known to live. `/props` is llama.cpp's own and carries the same figure, which is why it is kept as the fallback — but it cannot be primary: the very payload measured above declares `"endpoint_props": false` about itself, so a provider may serve it, refuse it, or not have it at all. Two doors, one fact, and the portable door first. ── ASKING IS EGRESS ───────────────────────────────────────────────────────────────────────────── One question per instance, memoised — including the negative answer, because a provider that stayed silent will not become talkative inside one run and re-asking would turn a missing figure into a per-call network round trip (greenhouse decisions/0233, point 5). ── THE NETWORK IS A SEAM ──────────────────────────────────────────────────────────────────────── The fetcher is a `callable(string): ?string`, the shape milpa/app-runtime's `CapabilityIndex` established: logic that can only be exercised against a live provider is logic nobody exercises.
ProviderWindow::__construct()
public function __construct(string $baseUrl, ?callable $fetch = null):Parameters
| Name | Type | Description |
|---|---|---|
| $baseUrl | string | where the provider lives — the same root {@see LlmService} is given, with or without a trailing `/v1` |
| $fetch | (null | callable(string): ?string) | the network seam; the default reads over HTTP/HTTPS with a short ceiling |
ProviderWindow::tokens()
public function tokens(): ?intThe allocated context window in tokens, or `null` when the provider did not say. `null` covers every way the question can fail to produce a ceiling — unreachable, non-JSON, JSON without the field, a value that is not a positive whole number, or a provider that exposes only the training window. None of them raise: the caller asked for a better number, not for a new way to die.