Skip to content

feat(models): add context window controls - #1203

Closed
estelledc wants to merge 3 commits into
lidge-jun:devfrom
estelledc:fix/1073-context-window-gui
Closed

feat(models): add context window controls#1203
estelledc wants to merge 3 commits into
lidge-jun:devfrom
estelledc:fix/1073-context-window-gui

Conversation

@estelledc

@estelledc estelledc commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a Context windows dialog to the Models dashboard for provider defaults and per-model overrides.
  • Expose and persist contextWindow and modelContextWindows through the existing provider management route, including positive-integer validation and per-model merge/delete behavior.
  • Document the fallback/cap semantics and add the new labels to all six dashboard locales.

Closes #1073

Models context window controls

Verification

  • bun run test - 9640 passed, 8 skipped, 0 failed across 598 files.
  • bun test tests/management-provider-validation.test.ts - 47 passed, 0 failed.
  • bun run typecheck - passed.
  • cd gui && bun test - 647 passed, 0 failed across 116 files.
  • cd gui && bun run lint - passed.
  • cd gui && bun run build - passed.
  • cd docs-site && bun run build - passed, 221 pages built.
  • Browser check against an isolated local provider: initial values and model switching were correct; 0 stayed in the dialog with a validation error; a valid save returned PATCH 200; reopening showed 256000 and 64000; no application errors appeared in the console.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • New Features

    • Added configurable provider-wide and per-model context-window settings.
    • Added validation, save feedback, automatic discovery guidance, and the ability to clear overrides.
    • Added localized settings text in English, German, Japanese, Korean, Russian, and Chinese.
  • Documentation

    • Clarified that configured context windows act as fallbacks when metadata is unavailable and caps when live limits are smaller.
  • Bug Fixes

    • Improved persistence and handling of context-window settings through provider management.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 78d0c016-9f83-47a3-895d-6c98f1d07014

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds provider-level and per-model context-window settings. The management API validates and persists these values. The Models page provides localized controls for non-native providers, with validation, save feedback, catalog refresh, and integration coverage.

Changes

Provider context-window configuration

Layer / File(s) Summary
Provider API validation and persistence
src/server/management/provider-routes.ts, src/types.ts, tests/management-provider-validation.test.ts, docs-site/src/content/docs/reference/configuration/providers.md
The provider API accepts positive finite integers or null for contextWindow and modelContextWindows. The API validates, merges, persists, exposes, and clears these values. Documentation describes fallback and cap behavior.
Context-window model data and localization
gui/src/models-groups.ts, gui/src/i18n/*.ts
Provider summaries and model groups carry optional context-window values. English, German, Japanese, Korean, Russian, and Chinese strings describe the settings, automatic discovery, save status, and validation.
Models page settings workflow
gui/src/pages/Models.tsx, gui/src/styles-models-workspace.css
The Models page adds a non-native provider settings button and modal. The workflow edits provider and per-model values, validates positive integers, submits a PATCH /api/providers request, refreshes catalog data, and reports errors or success.
Models page integration coverage
gui/tests/models-empty-provider.test.tsx, gui/tests/models-provider-head.test.ts
Tests cover initial context-window data, PATCH handling, modal editing, submitted values, dialog closure, model-group fields, translated UI, and context-field spacing.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Reviewer as Models page
  participant API as Provider routes
  participant Config as Provider configuration
  participant Catalog as Catalog refresh

  Reviewer->>API: PATCH provider and model context windows
  API->>Config: Validate and persist settings
  API-->>Reviewer: Save response
  Reviewer->>Catalog: Refresh provider catalog
  Catalog-->>Reviewer: Updated provider and model metadata
Loading

Possibly related PRs

Suggested reviewers: lidge-jun, wibias, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding context-window controls to the Models dashboard.
Linked Issues check ✅ Passed The PR adds provider and per-model controls, persists both fields, validates and clears values, and documents fallback and cap semantics required by #1073.
Out of Scope Changes check ✅ Passed All changes support the linked issue through UI, API, types, documentation, localization, styling, and tests; no unrelated changes are evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 7, 2026
@estelledc
estelledc marked this pull request as ready for review August 7, 2026 09:10
@github-actions
github-actions Bot marked this pull request as draft August 7, 2026 09:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gui/src/pages/Models.tsx`:
- Around line 339-345: Update selectContextModel to preserve unsaved context
overrides in a per-model contextModelDrafts map instead of overwriting the
current draft when switching models; load each model’s existing draft or
persisted value when selected, and reset contextModelDrafts in
openContextSettings when opening settings for a new provider.
- Around line 356-398: Update saveContextSettings so the context modal closes
and publishFeedback reports success immediately after readJsonOrThrow confirms
the PATCH succeeded. Then call load(true) without gating or reversing the
successful save flow on its return value; remove the refreshed-result check and
contextError assignment for refresh failure, allowing the existing catalog error
handling to surface it separately.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ac238eff-f247-4502-8a44-7cb79cbe2d3d

📥 Commits

Reviewing files that changed from the base of the PR and between 1e5fa3d and 5f06a20.

⛔ Files ignored due to path filters (1)
  • docs-site/public/pr-screenshots/1073-context-window-controls.jpg is excluded by !**/*.jpg
📒 Files selected for processing (15)
  • docs-site/src/content/docs/reference/configuration/providers.md
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/zh.ts
  • gui/src/models-groups.ts
  • gui/src/pages/Models.tsx
  • gui/src/styles-models-workspace.css
  • gui/tests/models-empty-provider.test.tsx
  • gui/tests/models-provider-head.test.ts
  • src/server/management/provider-routes.ts
  • src/types.ts
  • tests/management-provider-validation.test.ts

Comment thread gui/src/pages/Models.tsx
Comment on lines +339 to +345
const selectContextModel = (modelId: string) => {
const group = groups.find(candidate => candidate.provider === contextModalProvider);
setContextModelId(modelId);
setContextModelDraft(group?.modelContextWindows?.[modelId]
? String(group.modelContextWindows[modelId])
: "");
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Switching the model selector silently discards the unsaved override draft.

selectContextModel at Line 339 always overwrites contextModelDraft from group.modelContextWindows?.[modelId] the moment the user picks a different model in the Select at Line 1336-1342. If the user types a new override value for the currently selected model and then reselects a different model to check its value, the typed-but-unsaved value is lost with no warning, since nothing persists it before the overwrite.

Preserve per-model drafts across selector changes so browsing other models does not erase in-progress edits.
[recommended_refactor_or_suggestion]

♻️ Proposed fix: keep a per-model draft map
   const [contextModelDraft, setContextModelDraft] = useState("");
+  const [contextModelDrafts, setContextModelDrafts] = useState<Record<string, string>>({});
   const selectContextModel = (modelId: string) => {
     const group = groups.find(candidate => candidate.provider === contextModalProvider);
+    setContextModelDrafts(prev => ({ ...prev, [contextModelId]: contextModelDraft }));
     setContextModelId(modelId);
-    setContextModelDraft(group?.modelContextWindows?.[modelId]
-      ? String(group.modelContextWindows[modelId])
-      : "");
+    setContextModelDraft(
+      contextModelDrafts[modelId]
+      ?? (group?.modelContextWindows?.[modelId] ? String(group.modelContextWindows[modelId]) : ""),
+    );
   };

contextModelDrafts should also be reset in openContextSettings when the dialog opens for a new provider.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/Models.tsx` around lines 339 - 345, Update selectContextModel
to preserve unsaved context overrides in a per-model contextModelDrafts map
instead of overwriting the current draft when switching models; load each
model’s existing draft or persisted value when selected, and reset
contextModelDrafts in openContextSettings when opening settings for a new
provider.

Comment thread gui/src/pages/Models.tsx
Comment on lines +356 to +398
const saveContextSettings = async () => {
if (!contextModalProvider) return;
const providerWindow = parseContextWindowDraft(contextDefaultDraft);
const modelWindow = parseContextWindowDraft(contextModelDraft);
if (providerWindow === undefined || modelWindow === undefined) {
setContextError(t("models.contextInvalid"));
return;
}
const group = groups.find(candidate => candidate.provider === contextModalProvider);
if (!group) {
setContextError(t("models.contextSaveFailed"));
return;
}

setContextSaving(true);
setContextError("");
try {
const body: Record<string, unknown> = { contextWindow: providerWindow };
if (contextModelId) {
body.modelContextWindows = { [contextModelId]: modelWindow };
}
const response = await fetch(
`${apiBase}/api/providers?name=${encodeURIComponent(contextModalProvider)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
await readJsonOrThrow(response, t("models.contextSaveFailed"));
const refreshed = await load(true);
if (!refreshed) {
setContextError(t("models.loadFail"));
return;
}
setContextModalProvider(null);
publishFeedback(true, t("models.contextSaved"));
} catch (error) {
setContextError(error instanceof Error ? error.message : t("models.contextSaveFailed"));
} finally {
setContextSaving(false);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A refresh failure after a successful save reports the save as failed, unlike every other modal mutation in this file.

At Line 385, readJsonOrThrow(response, ...) does not throw, so the PATCH succeeded and the context-window values are persisted. At Line 386, load(true) is only a post-save catalog refresh. If it fails (transient network error, slow proxy), Lines 387-390 set contextError to t("models.loadFail") ("Failed to load models — is the proxy running?") and return before Line 391-392 ever run — so the modal stays open, showing an error that implies the save itself failed, when it did not.

Compare with addCustomModel (Lines 690-697) and updateCustomModel (Lines 714-721): both close the modal and call publishFeedback(true, ...) immediately once the write succeeds, then await load(true) afterward without gating success on its result. saveContextSettings is the only modal-driven mutation in this file that reverses that order and conditions the success path on the refresh outcome.

A user hitting a refresh hiccup right after saving will see a misleading "proxy not running" style message and may retry the save unnecessarily. Report success as soon as the PATCH succeeds, and let the refresh failure surface separately (the page already has catalogState.showError for a stale-catalog banner at Line 1554).

🐛 Proposed fix: match the addCustomModel/updateCustomModel pattern
       await readJsonOrThrow(response, t("models.contextSaveFailed"));
-      const refreshed = await load(true);
-      if (!refreshed) {
-        setContextError(t("models.loadFail"));
-        return;
-      }
       setContextModalProvider(null);
       publishFeedback(true, t("models.contextSaved"));
+      await load(true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const saveContextSettings = async () => {
if (!contextModalProvider) return;
const providerWindow = parseContextWindowDraft(contextDefaultDraft);
const modelWindow = parseContextWindowDraft(contextModelDraft);
if (providerWindow === undefined || modelWindow === undefined) {
setContextError(t("models.contextInvalid"));
return;
}
const group = groups.find(candidate => candidate.provider === contextModalProvider);
if (!group) {
setContextError(t("models.contextSaveFailed"));
return;
}
setContextSaving(true);
setContextError("");
try {
const body: Record<string, unknown> = { contextWindow: providerWindow };
if (contextModelId) {
body.modelContextWindows = { [contextModelId]: modelWindow };
}
const response = await fetch(
`${apiBase}/api/providers?name=${encodeURIComponent(contextModalProvider)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
await readJsonOrThrow(response, t("models.contextSaveFailed"));
const refreshed = await load(true);
if (!refreshed) {
setContextError(t("models.loadFail"));
return;
}
setContextModalProvider(null);
publishFeedback(true, t("models.contextSaved"));
} catch (error) {
setContextError(error instanceof Error ? error.message : t("models.contextSaveFailed"));
} finally {
setContextSaving(false);
}
};
const saveContextSettings = async () => {
if (!contextModalProvider) return;
const providerWindow = parseContextWindowDraft(contextDefaultDraft);
const modelWindow = parseContextWindowDraft(contextModelDraft);
if (providerWindow === undefined || modelWindow === undefined) {
setContextError(t("models.contextInvalid"));
return;
}
const group = groups.find(candidate => candidate.provider === contextModalProvider);
if (!group) {
setContextError(t("models.contextSaveFailed"));
return;
}
setContextSaving(true);
setContextError("");
try {
const body: Record<string, unknown> = { contextWindow: providerWindow };
if (contextModelId) {
body.modelContextWindows = { [contextModelId]: modelWindow };
}
const response = await fetch(
`${apiBase}/api/providers?name=${encodeURIComponent(contextModalProvider)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
await readJsonOrThrow(response, t("models.contextSaveFailed"));
setContextModalProvider(null);
publishFeedback(true, t("models.contextSaved"));
await load(true);
} catch (error) {
setContextError(error instanceof Error ? error.message : t("models.contextSaveFailed"));
} finally {
setContextSaving(false);
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/Models.tsx` around lines 356 - 398, Update saveContextSettings
so the context modal closes and publishFeedback reports success immediately
after readJsonOrThrow confirms the PATCH succeeded. Then call load(true) without
gating or reversing the successful save flow on its return value; remove the
refreshed-result check and contextError assignment for refresh failure, allowing
the existing catalog error handling to surface it separately.

@lidge-jun

Copy link
Copy Markdown
Owner

Thank you — this is adopted, and the layering you chose held up under review.

Your three commits are the first three of #1223, cherry-picked with authorship intact. The structural decision that mattered most was leaving catalog derivation alone: configuredContextWindow already reads both fields and materializes them when upstream metadata is absent, so the only thing genuinely missing was the management surface and the control. Routing the edit through the existing provider mutation path kept validation, cache invalidation, and convergence unified rather than growing a second write path.

Four corrections on top, all found by independent audit rather than preference:

Only the selected model was saved. saveContextSettings keyed the PATCH on contextModelId while contextModelDrafts held edits for every model, so a value typed into one model and then abandoned by switching the picker vanished — no error, no warning. The test in models-empty-provider.test.tsx asserted { "claude-opus": 100000 } after the user had also typed 80000 into claude-sonnet, which pinned the loss as correct.

Sending everything that differs would have been wrong the other way. The 10s poll can refresh a field mid-modal; comparing drafts against live state then marks an untouched field dirty and reverts a change the user never made. It now requires both conditions — the user touched it, and the value differs from an opening snapshot held as canonical numbers, so retyping 64000 as "64,000" is not an edit. The provider default needed the same treatment; it was previously sent on every Apply.

Number.isInteger(1e100) is true. Both the validator and the form accepted it. It persists and serializes into the catalog as an enormous number that can make Codex reject the file. Both are safe-integer now, and the default is only validated when touched so an inherited bad value cannot block a per-model save.

An override for a model that left live discovery was unreachable — in the drafts map, absent from the picker.

One note on your test for the post-save refresh failure: it was already correct. I initially wrote it up as a defect and was wrong — load() catches its own fetch errors and returns false rather than throwing, and your third commit had already moved the modal close and the success feedback ahead of the refresh.

Closing in favor of #1223. The credit is yours.

@lidge-jun lidge-jun closed this Aug 7, 2026
lidge-jun added a commit that referenced this pull request Aug 7, 2026
…reverting concurrent changes

Adopts #1203 by @estelledc — the first three commits are theirs, cherry-picked
with authorship intact. The approach was right: expose the existing
`providers.<id>.contextWindow` / `modelContextWindows` contract at the
management and UI layers without touching catalog derivation, which already
materializes those values when upstream metadata is absent.

Four corrections, all found by independent audit.

**Only the selected model was saved.** The drafts map held edits for every
model but the PATCH was keyed on `contextModelId`, so a value typed into
model A and then abandoned by switching to B vanished — no error, no warning.
The PR's own test pinned that as correct. It now sends every model the user
typed into.

**But "every model that differs" would have been wrong the other way.** The
10s poll can refresh a field while the modal is open; diffing drafts against
live state would then call an untouched field dirty and revert someone else's
change. Two conditions are required: the user touched it, AND the value
differs from what the modal opened with. Both apply to the provider default
too, which was previously sent unconditionally and would stamp a stale number
over a concurrent update.

The snapshot holds canonical numbers, not the raw text. Retyping 64000 as
"64,000" is not an edit, and treating it as one would resurrect the same
stale-write. When nothing survives the comparison, no PATCH is sent at all
and the feedback says so rather than claiming an update.

**`Number.isInteger(1e100)` is true.** Both the management validator and the
form accepted it; it would persist and serialize into the catalog as an
enormous number that can make Codex reject the file. Both now require a safe
integer. The default is only validated when touched, so a value inherited
from a hand-edited config cannot block an unrelated per-model save.

**An override for a model that left live discovery was unreachable.** It sat
in the drafts map, absent from the picker, impossible to inspect or clear.

Tests: the exact #1073 reproduction is split in two, because a single case
setting `modelContextWindows` keeps passing with the provider-wide fallback
deleted. Ablations were driven red in their real defect shape rather than as
artificially strong mutants — notably, comparing against live `groups` while
keeping the touched guard is only visible when a field is edited, reverted,
and changed server-side, which the suite now covers.

Translated provider docs (ko/ja/ru/zh-cn) described both fields as caps only,
which reads as the opposite of the fix for non-English users.

Co-authored-by: zhouxun <zhouxun.13@bytedance.com>
Closes #1073
lidge-jun added a commit that referenced this pull request Aug 7, 2026
…reverting concurrent changes

Adopts #1203 by @estelledc — the first three commits are theirs, cherry-picked
with authorship intact. The approach was right: expose the existing
`providers.<id>.contextWindow` / `modelContextWindows` contract at the
management and UI layers without touching catalog derivation, which already
materializes those values when upstream metadata is absent.

Four corrections, all found by independent audit.

**Only the selected model was saved.** The drafts map held edits for every
model but the PATCH was keyed on `contextModelId`, so a value typed into
model A and then abandoned by switching to B vanished — no error, no warning.
The PR's own test pinned that as correct. It now sends every model the user
typed into.

**But "every model that differs" would have been wrong the other way.** The
10s poll can refresh a field while the modal is open; diffing drafts against
live state would then call an untouched field dirty and revert someone else's
change. Two conditions are required: the user touched it, AND the value
differs from what the modal opened with. Both apply to the provider default
too, which was previously sent unconditionally and would stamp a stale number
over a concurrent update.

The snapshot holds canonical numbers, not the raw text. Retyping 64000 as
"64,000" is not an edit, and treating it as one would resurrect the same
stale-write. When nothing survives the comparison, no PATCH is sent at all
and the feedback says so rather than claiming an update.

**`Number.isInteger(1e100)` is true.** Both the management validator and the
form accepted it; it would persist and serialize into the catalog as an
enormous number that can make Codex reject the file. Both now require a safe
integer. The default is only validated when touched, so a value inherited
from a hand-edited config cannot block an unrelated per-model save.

**An override for a model that left live discovery was unreachable.** It sat
in the drafts map, absent from the picker, impossible to inspect or clear.

Tests: the exact #1073 reproduction is split in two, because a single case
setting `modelContextWindows` keeps passing with the provider-wide fallback
deleted. Ablations were driven red in their real defect shape rather than as
artificially strong mutants — notably, comparing against live `groups` while
keeping the touched guard is only visible when a field is edited, reverted,
and changed server-side, which the suite now covers.

Translated provider docs (ko/ja/ru/zh-cn) described both fields as caps only,
which reads as the opposite of the fix for non-English users.

Co-authored-by: zhouxun <zhouxun.13@bytedance.com>
Closes #1073
lidge-jun added a commit that referenced this pull request Aug 7, 2026
…reverting concurrent changes

Adopts #1203 by @estelledc — the first three commits are theirs, cherry-picked
with authorship intact. The approach was right: expose the existing
`providers.<id>.contextWindow` / `modelContextWindows` contract at the
management and UI layers without touching catalog derivation, which already
materializes those values when upstream metadata is absent.

Four corrections, all found by independent audit.

**Only the selected model was saved.** The drafts map held edits for every
model but the PATCH was keyed on `contextModelId`, so a value typed into
model A and then abandoned by switching to B vanished — no error, no warning.
The PR's own test pinned that as correct. It now sends every model the user
typed into.

**But "every model that differs" would have been wrong the other way.** The
10s poll can refresh a field while the modal is open; diffing drafts against
live state would then call an untouched field dirty and revert someone else's
change. Two conditions are required: the user touched it, AND the value
differs from what the modal opened with. Both apply to the provider default
too, which was previously sent unconditionally and would stamp a stale number
over a concurrent update.

The snapshot holds canonical numbers, not the raw text. Retyping 64000 as
"64,000" is not an edit, and treating it as one would resurrect the same
stale-write. When nothing survives the comparison, no PATCH is sent at all
and the feedback says so rather than claiming an update.

**`Number.isInteger(1e100)` is true.** Both the management validator and the
form accepted it; it would persist and serialize into the catalog as an
enormous number that can make Codex reject the file. Both now require a safe
integer. The default is only validated when touched, so a value inherited
from a hand-edited config cannot block an unrelated per-model save.

**An override for a model that left live discovery was unreachable.** It sat
in the drafts map, absent from the picker, impossible to inspect or clear.

Tests: the exact #1073 reproduction is split in two, because a single case
setting `modelContextWindows` keeps passing with the provider-wide fallback
deleted. Ablations were driven red in their real defect shape rather than as
artificially strong mutants — notably, comparing against live `groups` while
keeping the touched guard is only visible when a field is edited, reverted,
and changed server-side, which the suite now covers.

Translated provider docs (ko/ja/ru/zh-cn) described both fields as caps only,
which reads as the opposite of the fix for non-English users.

Co-authored-by: zhouxun <zhouxun.13@bytedance.com>
Closes #1073
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants