Repository navigation
fix(management): preserve the account-failover opt-out across a provider overwrite (#2568d) #2642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # 030 — closing F5: the Add Provider path also had to stop dropping the opt-out | ||
|
|
||
| `011_audit_response.md` scoped F5 out with a reason: `/api/providers` POST replaces a provider | ||
| row wholesale and already discards every unrecognized key, so it looked like a general payload | ||
| contract problem rather than this feature's problem. | ||
|
|
||
| That reasoning was half right and the conclusion was wrong. | ||
|
|
||
| ## Why it could not stay deferred | ||
|
|
||
| Every other field this path drops fails toward something neutral — a missing `modelCosts` means | ||
| the Usage estimate falls back to registry prices, a missing `contextWindow` means the seed value | ||
| applies. Losing `oauthAccountFailover` does not fail toward neutral. Activation is now | ||
| presence-driven, so deleting an operator's `enabled: false` does not restore a default: it | ||
| **enables** rotation across their second subscription account, as a side effect of an edit that | ||
| had nothing to do with failover. | ||
|
|
||
| That is the same failure shape as F4 (login rebuilding the row), which was accepted as blocking. | ||
| The two paths differ only in which unrelated action triggers the loss. | ||
|
|
||
| ## What changed | ||
|
|
||
| One preservation line in `src/server/management/provider-routes.ts`, next to the ones that | ||
| already exist for `apiKeyPool`, `modelCosts`, `requestPacing`, and the context-window maps. The | ||
| comment there records why absence means "not carried" rather than "deleted": `ProviderPayload` | ||
| (`gui/src/provider-payload.ts`) structurally cannot express the field, so the dashboard's | ||
| add/edit form can never send it. Deletion goes through PATCH with an explicit null, exactly as | ||
| #1409 established for context windows. | ||
|
|
||
| No GUI change. Widening `ProviderPayload` would be the wrong fix for the same reason it was the | ||
| wrong fix for `modelCosts`: the form has no control for this setting, so a payload member would | ||
| only give it a way to send `undefined` and re-create the problem. | ||
|
|
||
| ## Verification | ||
|
|
||
| A regression next to the existing `modelCosts` overwrite test in | ||
| `tests/management-provider-validation.test.ts`, driven against a real server: create a provider | ||
| with `enabled: false`, POST an overwrite without the field, assert the opt-out survived. | ||
| Falsified by disabling the preservation branch — the test fails, 74 pass / 1 fail. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -596,6 +596,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp | |
| // erase hand-edited per-model prices from Logs/Usage estimates. | ||
| const existingCosts = config.providers[name]?.modelCosts; | ||
| if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts; | ||
| // And to the per-provider account-failover opt-out (#2568d). `ProviderPayload` has no | ||
| // member for it either, so an add/edit save structurally cannot carry it — and dropping it | ||
| // silently ENABLES rotation, because activation is presence-driven once the knob is gone. | ||
| // An overwrite must not spend a second subscription account's quota as a side effect. | ||
| const existingFailover = config.providers[name]?.oauthAccountFailover; | ||
| if (existingFailover && !prov.oauthAccountFailover) prov.oauthAccountFailover = existingFailover; | ||
|
Comment on lines
+603
to
+604
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an operator wants a provider to stop overriding the global/presence-driven failover policy, this preservation makes both an omitted value and an explicit Useful? React with 👍 / 👎. |
||
| // ...and to hand-edited context windows. `ProviderPayload` (gui/src/provider-payload.ts) | ||
| // has no member for either field, so the add/edit form structurally cannot send them: | ||
| // absence in the request means "not carried", never "the user deleted it". Deletion goes | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the issue reference formatting.
Line [28] starts with
#1409, and markdownlint reports MD018. Wrap the reference in backticks or writeissue 1409so it remains prose.Proposed fix
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 28-28: No space after hash on atx style heading
(MD018, no-missing-space-atx)
🤖 Prompt for AI Agents
Source: Linters/SAST tools