Repository navigation
feat(core): use each model's real context window - #476
Conversation
Every generation run treated the model as having a 200k-token window: buildPiModel hard-coded contextWindow: 200000, and the context pack read a contextWindow that ModelRef never carries, so its history budget was the same for every model. Core now resolves the window with resolveContextWindow: the provider's configured value, then pi-ai's catalog entry for the provider and model, then the smallest catalog window for the same model id (relays usually serve upstream ids), then the 200k default. The agent model and the context pack both use it. Providers gain an optional contextWindow in config, editable next to Reasoning depth in Settings -> Models, for local runtimes and relays the catalog does not know.
There was a problem hiding this comment.
Review mode: initial
Findings
-
[Nit]
ContextWindowInputcan fire a secondupdateProvidercall while the first is still in flight.setSaving(true)re-renders the input withdisabled={saving}; disabling the currently focused element blurs it, which re-enterssave(). At that momentdraftstill differs from the not-yet-refreshedsavedprop, so thetrimmed === savedearly-return does not apply and a duplicate write + duplicate success toast can occur (apps/desktop/src/renderer/src/components/settings/primitives.tsx:651).
Suggested fix / next action: guard the entry, e.g. addif (saving) return;as the first line ofsave()(or track in-flight state in a ref instead of viadisabled). -
[Nit]
resolveContextWindowcasts the model id withmodel.modelId as never(packages/core/src/context-window.ts:24). Casting toneversatisfies any parameter type and silently disables the model-id type check, so a future pi-ai signature change or a wrongModelReffield would not be caught. Prefer a narrower cast (orString(model.modelId)if the API accepts a plain string) with a short comment on why the catalog lookup needs loosening.
Questions
- Does pi-ai 0.72.1
getModel/getModelsreturnundefined/[](rather than throw) for a provider id that is not in the catalog?resolveContextWindowandcontext-window.test.tsboth assume it degrades quietly for unknown providers (relays, local runtimes). If any of those helpers throws for an unknown provider, every custom/relay generation would fail at the resolver instead of falling through to the same-id or default branches.
Summary
- Review mode: initial
- The core change is directionally sound: a single
resolveContextWindow(model, configured)resolver is threaded through both the agent model (packages/core/src/agent.ts:319) and the context pack (apps/desktop/src/main/ipc/generate.ts:1319), replacing the previous hard-coded 200k and the deadcontextWindowForContextPackhelper. The precedence (configured → exact catalog entry → smallest same-id → 200k default) is documented and the “underestimate trims history, overestimate overflows” rationale is stated in the JSDoc. - Config plumbing is consistent across layers: zod schema (
packages/shared/src/config.ts:186), IPC parser with positive-integer/nullvalidation (apps/desktop/src/main/onboarding/provider-parsers.ts:403), CRUD mutation (providers-crud.ts),ProviderRow/ActiveModelResolution, and the preload contract all carry the new optional field.nullclears toundefinedin the same tri-state style astlsRejectUnauthorized, and an absent value is omitted from theGenerateInputpassthrough so existing configs keep prior behavior. - The PR correctly uses
Refs #475, #394rather than a closing keyword, and a changeset is included — no linked-issue completion claim to validate. - No new dependencies and no license/install-size impact.
Testing
- Not run (automation). Coverage is proportional to the change:
context-window.test.ts(mocked catalog) exercises all four resolver branches,agent.test.tsasserts the configured window reaches the pi model,generate.workspace-rename.test.tsasserts both the context pack and the agent input, plusprovider-settings.test.ts,onboarding-ipc.test.ts, andconfig.test.tsfor wiring/persistence/validation. The remaining gap is the UI-only path (the PR notes the input was rendered standalone, not in the running app), which matches the nits above.
Open-CoDesign Bot
There was a problem hiding this comment.
🟡 Changes recommended
Stored Gemini model IDs can miss known catalog entries and receive the incorrect fallback window.
1 open finding
What changed in this PR
Adds model-aware context-window resolution across core generation, provider configuration, and Settings.
Changes:
- Resolves context windows from provider overrides, pi-ai’s catalog, or a 200k fallback.
- Adds persisted provider settings, validation, UI controls, and translations.
- Adds coverage for resolution and end-to-end propagation.
| File | Description |
|---|---|
.changeset/provider-context-window.md |
Records the user-visible feature. |
packages/shared/src/config.ts |
Adds the provider context-window schema. |
packages/shared/src/config.test.ts |
Tests schema validation. |
packages/i18n/src/locales/en.json |
Adds English strings. |
packages/i18n/src/locales/es.json |
Adds Spanish strings. |
packages/i18n/src/locales/pt-BR.json |
Adds Portuguese strings. |
packages/i18n/src/locales/zh-CN.json |
Adds Chinese strings. |
packages/core/src/index.ts |
Exports the resolver and generation input. |
packages/core/src/context-window.ts |
Implements catalog resolution. |
packages/core/src/context-window.test.ts |
Tests resolution precedence. |
packages/core/src/agent.ts |
Applies the resolved window to agent models. |
packages/core/src/agent.test.ts |
Tests agent propagation. |
apps/desktop/src/preload/index.ts |
Extends renderer IPC types. |
apps/desktop/src/main/provider-settings.ts |
Exposes configured windows. |
apps/desktop/src/main/provider-settings.test.ts |
Tests active-model propagation. |
apps/desktop/src/main/onboarding/providers-crud.ts |
Persists and clears overrides. |
apps/desktop/src/main/onboarding/provider-parsers.ts |
Validates update payloads. |
apps/desktop/src/main/onboarding-ipc.test.ts |
Tests IPC persistence and validation. |
apps/desktop/src/main/ipc/generate.ts |
Uses resolved windows during generation. |
apps/desktop/src/main/ipc/generate.test.ts |
Removes obsolete helper tests. |
apps/desktop/src/main/ipc/generate.workspace-rename.test.ts |
Tests generation wiring. |
apps/desktop/src/renderer/src/components/settings/primitives.tsx |
Adds the settings input. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Gemini's OpenAI-compatible model listing stores ids such as models/gemini-2.5-pro, while pi-ai's catalog ids have no models/ prefix, so those models missed the catalog and got the 200k default. resolveContextWindow now takes the base URL and normalizes the id with normalizeGeminiModelId, as the request does. The exact lookup searches getModels(provider), which removes the cast to never.
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
- No new findings introduced by the follow-up commits (
f96b525..b776962). The follow-up stays within the PR's scope and completes Gemini id handling for the resolver.
Residual observations
- [Nit, carried over]
ContextWindowInputstill has no in-flight guard.setSaving(true)re-renders the focused input withdisabled={saving}; disabling a focused element blurs it and re-enterssave()whiledraftalready differs from the not-yet-refreshedsavedprop, so thetrimmed === savedearly-return does not apply and a secondupdateProviderwrite plus a second success toast can fire (apps/desktop/src/renderer/src/components/settings/primitives.tsx:651). Not a merge blocker; a one-lineif (saving) return;at the top ofsave()would close it. - Previously reported
model.modelId as nevernit is resolved:resolveContextWindownow looks up the catalog withgetModels(...).find(...)and normalizes Geminimodels/-prefixed ids vianormalizeGeminiModelId(packages/core/src/context-window.ts:23-24), with a matching test (packages/core/src/context-window.test.ts:35).
Questions
- Carried from the prior review and still unverified from the public repo: does pi-ai 0.72.1
getModels(provider)return[](rather than throw) for a provider id absent from the catalog? The resolver callsgetModels(model.provider as KnownProvider)directly as the first lookup (packages/core/src/context-window.ts:24), so a relay/local provider not in the catalog reaches this before the same-id and default branches.context-window.test.tsmocks@mariozechner/pi-ai, so it cannot confirm the real behavior. If you can confirm against the installed package, good; otherwise consider an existence check againstgetProviders()before the direct lookup.
Summary
- Review mode: follow-up after new commits.
- The diff since the last review is small and targeted:
baseUrl/canonicalBaseis now threaded intoresolveContextWindowat both call sites —packages/core/src/agent.ts:319andapps/desktop/src/main/ipc/generate.ts:1319— so Geminimodels/-prefixed ids resolve consistently for both the agent model and the context pack.context-window.test.tsand the providers mock ingenerate.workspace-rename.test.tswere updated to match. - Bundling, config, IPC, and preload contracts are unchanged from the reviewed revision; no new dependencies, no license or install-size impact, and no release/distribution files touched.
- Only residual items are the unverified pi-ai
getModelsbehavior for unknown providers and the still-present duplicate-save guard in the new UI input. On the available information neither is a merge blocker. - The UI input is still not exercised in the running app (the PR notes it was rendered standalone), so the field's end-to-end behavior is unverified.
Testing
- Not run (automation). Coverage remains proportional:
context-window.test.ts(configured → exact catalog → smallest same-id → default, plus the Gemini prefix case),agent.test.ts,generate.workspace-rename.test.ts(context pack + agent input),provider-settings.test.ts,onboarding-ipc.test.ts, andconfig.test.ts. Suggested addition: a test that calls the real (unmocked)resolveContextWindowwith a custom/relay provider id to confirmgetModelsdegrades quietly for unknown providers.
Open-CoDesign Bot
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
- No new findings introduced by the follow-up commit (
b776962..47d78bc). The only diff since the last review is the newpackages/core/src/context-window.catalog.test.ts, which exercises the real (unmocked)resolveContextWindowand closes the prior review's open question aboutgetModelsbehavior for unknown providers.
Residual observations
- [Nit, carried over; not a merge blocker]
ContextWindowInput.save()still has no in-flight guard (apps/desktop/src/renderer/src/components/settings/primitives.tsx:651).setSaving(true)re-renders the focused input withdisabled={saving}; if disabling a focused element firesblur,save()re-enters before the refreshedsavedprop lands, sotrimmed === saveddoes not short-circuit and a secondupdateProviderwrite / success toast can fire. A one-lineif (saving) return;at the top ofsave()closes it. Unchanged by this follow-up. - [Nit]
packages/core/src/context-window.catalog.test.ts:7asserts the installed pi-ai catalog does not knowqwen3:32b. That is the property under test (real-catalog fallback), but it couples the test to the pinned catalog contents: a future pi-ai patch that addsqwen3:32bunder any provider would fail this test for reasons unrelated to the resolver. Acceptable given the test's purpose; if you want it hermetic, use an id that cannot appear (e.g. a clearly synthetic one).
Summary
- Review mode: follow-up after new commits.
- The follow-up is a single new test file. It calls the real
resolveContextWindowfor a relay provider absent from the catalog (packages/core/src/context-window.catalog.test.ts) and confirms the resolver returnsDEFAULT_CONTEXT_WINDOWrather than throwing, which is exactly thegetModels(unknownProvider)question left open in the previous review (packages/core/src/context-window.ts:24). Run against the installed@mariozechner/pi-ai@0.72.1, the contract is now covered at the real boundary rather than only through mocks. - No changes to bundling, config/IPC contracts, dependency set, licenses, or install size; no release/distribution files touched. The rest of the PR is unchanged from the previously reviewed revision, and the earlier resolved item (catalog lookup by
normalizeGeminiModelIdinstead ofas never) still holds. - Remaining items are the two non-blocking nits above. Neither is a merge blocker; the PR is otherwise ready from this review's perspective.
- The new settings field is still only exercised in a standalone render, not in the running app (per the PR description); end-to-end UI behavior remains unverified.
Testing
- Not run (automation). New coverage:
context-window.catalog.test.ts(real catalog, unknown provider → default). Existing coverage from earlier revisions (mocked resolver, agent model, generate IPC context pack/agent input, provider-settings, onboarding IPC store/clear/validate, config schema) is unchanged.
Open-CoDesign Bot
Leaving the window blurs the focused input without moving focus, so disabling it during the save can blur it again and repeat the write and the success toast. The catalog test now uses a synthetic model id, so a future catalog entry cannot change its result.
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
- No new findings. The diff since the last review (
47d78bc..d5adbec) consists only of the two previously raised non-blocking items, and both are addressed:apps/desktop/src/renderer/src/components/settings/primitives.tsx:653now guardssave()withif (saving || !window.codesign?.config?.updateProvider) return;, closing the re-entrancy path wheresetSaving(true)→disabled={saving}could blur the focused input and fire a secondupdateProviderwrite plus a duplicate success toast.packages/core/src/context-window.catalog.test.ts:6now uses clearly synthetic ids (codesign-test-relay/codesign-test-unlisted-model), so a future pi-ai catalog addition can no longer fail this test for reasons unrelated to the resolver.
Residual observations (non-blocking, unchanged by this diff)
- The
savingguard is a best-effort lock: the re-entrant blur would be handled by the props closure React has committed by dispatch time. If double-fire is ever observed on real blur behavior, auseReflock removes the dependency on prop-commit timing. Not worth blocking on now. - The new "Context window" field is still only exercised in a standalone render, not in the running app (per the PR description). The persistence/validation paths are covered at the IPC and schema boundary (
apps/desktop/src/main/onboarding-ipc.test.ts,packages/shared/src/config.test.ts), but no test or manual run covers the component's owndraft/saved/savingstate machine or the clear-back-to-Auto round trip in the real app. - The Gemini
models/-prefix branch (packages/core/src/context-window.ts:26) is covered only by a mocked catalog (packages/core/src/context-window.test.ts);packages/core/src/context-window.catalog.test.tsexercises the real installed catalog but only the unknown-model fallback. If pi-ai's google entries ever carried themodels/prefix themselves, the normalized lookup would miss and return 200k — an underestimate, which is the safe direction by design, but silent. - The window is per provider, not per model, so one value applies to every model behind that provider. That matches the schema comment (
packages/shared/src/config.ts:185) and the PR description; noted only so the granularity reads as a deliberate choice.
Summary
- Review mode: follow-up after new commits.
- No blockers, majors, minors, or nits introduced by the follow-up commit. Both items from the previous review are resolved, and the rest of the PR is unchanged from the previously reviewed revision (resolver in
packages/core/src/context-window.ts, plumbing throughpackages/core/src/agent.ts,apps/desktop/src/main/provider-settings.ts,apps/desktop/src/main/ipc/generate.ts, config schema inpackages/shared/src/config.ts, and the settings field inapps/desktop/src/renderer/src/components/settings/primitives.tsx). - The PR links with
Refs #475, #394, i.e. partial work, so there is no completion claim to validate against those issues' acceptance criteria; the remaining proposal slices are correctly left open. - No new dependencies, no license or install-size impact, no release/distribution files touched, and the changeset (
.changeset/provider-context-window.md) covers shared/core/desktop/i18n. - Remaining items are the residual observations above and are not merge blockers. From this review's perspective the PR is ready to merge; the main unverified surface is end-to-end behavior of the new settings field in the running app.
Testing
- Not run (automation). Coverage present:
context-window.test.ts(mocked catalog: configured > exact > smallest same-id > default, Gemini prefix),context-window.catalog.test.ts(real catalog, unknown provider/model → default),agent.test.ts(agent model gets configured window),generate.workspace-rename.test.ts(context pack budget and agent input receive 32,768),provider-settings.test.ts,onboarding-ipc.test.ts(store/clear/validate),config.test.ts(schema accept/reject). Suggested addition if you want the UI state machine covered: a renderer test forContextWindowInputasserting invalid input restores the previous value and empty input sendscontextWindow: null.
Open-CoDesign Bot
## Summary Second step of #475. A typical run already sends about 7.5k tokens before any chat history: the system prompt is about 13k characters and the 13 tool definitions about 16k. Local runtimes often default to a 4k–8k window (Ollama's default `num_ctx`), and an over-long request is truncated by the server rather than rejected, so the model works from a cut-off system prompt. Core now measures the system prompt, tool definitions, and the prompt (which carries the brief, memory, and attachments as text) in tokens the way pi estimates them, and keeps a quarter of the window, at most the 32,000 tokens a run requests, free for the reply. The request asks for no more output than that reserve, so input plus requested output fits the window: models with a window of 128k tokens or more keep 32,000, a 32k window requests 8,192. - If those parts do not fit the window resolved in #476, the run fails before the request with `GENERATION_CONTEXT_EXCEEDED`. The message names the tokens needed and the window, and suggests shortening the request or its attachments, setting the provider's context window, or using a model with a larger window; the diagnostic offers Settings → Models, where the context window is set. - Chat history keeps its newest messages that fit what remains, starting at a user message. The context pack's own history cap is unchanged. - The budget (window, reserve, part sizes, history kept and dropped) is logged with each run, for the visibility step that follows. ## Type of change - [ ] Bug fix - [x] New feature - [ ] Refactor (no behavior change) - [ ] Documentation - [ ] Build / CI / tooling - [ ] Breaking change ## Linked issue Refs #475, #394 ## Checklist - [x] I checked the linked issue / relevant context before starting - [x] `pnpm lint && pnpm typecheck && pnpm test` passes locally - [x] Added/updated tests for the change - [x] Added a changeset (`pnpm changeset`) if user-visible - [ ] Updated docs if behavior changed ## Principles - [x] Compatibility: with today's 200k default and the catalog windows, normal runs fit and keep their history; only runs that cannot fit now stop early. - [x] Upgradeability: estimates use pi's `estimateTokens`. - [x] No bloat: no new dependencies; 118 lines outside tests, strings, and the changeset. - [x] Elegance: one budget function decides what fits; the agent starts with the history it returns. ## Checks - `pnpm -r typecheck`, `pnpm lint`, `CI=1 pnpm test`: 10 tasks passed; core 568 tests, shared 327, desktop 180 files / 2,629 tests. - `context-budget.test.ts`: reserve in large and small windows, tool definitions counted by name, description and parameters (a tool alone can push a run past the window), oldest history dropped first, a leading assistant message dropped, prompt images counted, and the error when the fixed parts do not fit. - `agent.test.ts`: a 32,768-token window gives the agent model that window and an 8,192-token output limit; a 2,048-token window fails before the agent starts; with a 32,768-token window the agent starts with the newest history that fits, opening at a user message. - `diagnostics.test.ts`: the new code maps to a Settings → Models fix. - Each of the fail-fast check, the budgeted history, and the user-message start fails its tests when removed.

Summary
Every generation run treated the model as having a 200k-token context window.
buildPiModelhard-codedcontextWindow: 200000, and the context pack read acontextWindowthatModelRefnever carries, so the history budget meant to follow the model was 12,000 characters for every model. This is the first step of the context-control proposal in #475.Core now resolves the window with
resolveContextWindow, in this order:The catalog is searched with the id sent on the wire, so Gemini ids stored as
models/gemini-…match their catalog entries.The agent model and the context pack both use it. Providers gain an optional
contextWindow(tokens) inconfig.toml, editable next to Reasoning depth in Settings → Models; leaving it empty keeps the catalog value. This matters for local runtimes such as Ollama and LM Studio and for relays the catalog does not know. Existing configs and unknown models behave as before.Type of change
Linked issue
Refs #475, #394
Checklist
pnpm lint && pnpm typecheck && pnpm testpasses locallypnpm changeset) if user-visiblePrinciples
contextWindowForContextPackis removed.Screenshots / recordings (UI changes)
The new field next to Reasoning depth in a Settings → Models provider row. The two controls were rendered on their own in system Chrome with the app stylesheet (after saving 32768, and empty in Chinese); the full app was not run.
Checks
pnpm -r typecheck,pnpm lint(686 files),CI=1 pnpm test: 10 tasks passed; core 559 tests, shared 326, desktop 180 files / 2,629 tests (after rebasing one331e69).context-window.test.ts: configured value, exact catalog entry, smallest same-id window, a Geminimodels/-prefixed id on Google's OpenAI-compatible endpoint (fails without the id normalization), default. These run against a mocked catalog, so pi-ai upgrades do not change the expectations;context-window.catalog.test.tschecks against the installed pi-ai that an unknown provider and model fall back to the default instead of throwing.agent.test.ts), the context pack and the agent input (generate.workspace-rename.test.ts), the active model resolution (provider-settings.test.ts), and is stored, cleared, and validated throughconfig:v1:update-provider(onboarding-ipc.test.ts). Each of these fails when its wiring is removed.abcwith the previous value restored, and clearing back to Auto. Not exercised in the running app.