Skip to content

feat(providers): generalize accounts UI and per-provider fallback - #36

Merged
OnlineChef merged 3 commits into
devfrom
feat/all-provider-account-management
Aug 1, 2026
Merged

feat(providers): generalize accounts UI and per-provider fallback#36
OnlineChef merged 3 commits into
devfrom
feat/all-provider-account-management

Conversation

@OnlineChef

@OnlineChef OnlineChef commented Aug 1, 2026

Copy link
Copy Markdown

Summary

  • Remove Codex-only Auth nav; Providers owns account/key pools for all providers.
  • Add configurable per-provider fallback list (reuses combo failover engine).
  • Provider Security Plane: OpenCodex stays the runtime plane; no CPM merge.

Retargeted from main to dev per the branch policy, and the single feature commit was
replayed onto dev so this PR carries only the feature diff instead of dragging main's
lineage into the integration branch. Cross-lineage resolutions: the provider list response keeps
dev's apiKeyTransport / client-hide fields plus the new fallback field, the per-provider
fallback hop in src/server/responses/core.ts now sits next to dev's subagent quota priming
(the two branches are mutually exclusive, thread spawns skip the fallback path), and the
gui/src/i18n/nl.ts edit was dropped because dev has no Dutch locale.

Test plan

  • Focused GUI routing tests
  • bun run typecheck, bun run test (only pre-existing Docker-sandbox service/CLI failures), bun run privacy:scan
  • cd gui && bun test tests && bun run lint && bun run lint:i18n && bun run build
  • Sofie fallback smoke when deployed

Made with Cursor


View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

Summary by CodeRabbit

  • New Features
    • Added configurable provider and model fallback chains for retryable request failures.
    • Added fallback provider/model selectors, ordering, validation, and removal controls in provider settings.
    • Provider responses and logs now expose configured fallback targets with readable labels.
  • Changes
    • Removed the legacy Codex Auth page; existing links redirect to Providers.
    • Renamed the Providers navigation section to “Providers & accounts” with updated translations.
  • Bug Fixes
    • Prevented invalid, duplicate, disabled, or unavailable fallback targets from being used.

Greptile Summary

This change adds provider-level fallback configuration and management controls, routing eligible requests through the existing failover engine. Two reproduced failures remain: Anthropic requests with a configured fallback can return incomplete tool-driven responses even when the primary succeeds, and deleting a referenced provider can leave the saved configuration invalid so the application falls back to defaults on reload.

Confidence Score: 4/5

T-Rex T-Rex Logs

What T-Rex did

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (3)

  1. src/server/management/provider-routes.ts, line 387 (link)

    P1 Provider deletion preserves dangling fallback targets

    Deleting a provider checks dependencies from combos but not from other providers' fallback lists. Deleting B after configuring A to fall back to B returns success while retaining A's target for B. On the next configuration load, strict fallback validation rejects the persisted file and the application replaces the active configuration with defaults.

    Context Used: Focking gretig zijn en niet stoppen tot perfectie.... (source)

    Artifacts

    Focused API reproduction source after PR #36

    • This executed Bun source starts the proxy, creates A and B, assigns A's fallback to B, deletes B, and checks persisted reload validation; it captures the stale-reference failure.

    Endpoint behavior before PR #36

    • The parent-commit HTTP run shows provider creation succeeds, fallback PATCH is rejected with HTTP 400 Bad Request because the feature does not yet exist, deletion returns HTTP 200 OK, and reload stays valid.

    Endpoint behavior after PR #36 with stale fallback

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/server/management/provider-routes.ts
    Line: 387
    
    Comment:
    **Provider deletion preserves dangling fallback targets**
    
    Deleting a provider checks dependencies from combos but not from other providers' `fallback` lists. Deleting B after configuring A to fall back to B returns success while retaining A's target for B. On the next configuration load, strict fallback validation rejects the persisted file and the application replaces the active configuration with defaults.
    
    **Context Used:** Focking gretig zijn en niet stoppen tot perfectie.... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
    
    ---
    
    For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

    Fix in Cursor Fix in Codex Fix in Claude Code Fix in Conductor

  2. General comment

    P1 Provider fallback disables Anthropic terminal continuation after a successful primary response

    • Bug
      • At src/server/responses/core.ts:955, an Anthropic route with a fallback enters handleComboResponses. That function invokes the primary child with comboAttempt: true at line 742. The terminal guard requires !options.comboAttempt at line 1911, so a successful Anthropic primary cannot perform its normal bounded continuation. The reproduced response completed but omitted the requested tool call after the model only announced its plan.
    • Cause
      • comboAttempt represents both actual fallback-loop execution and a condition that disables the Anthropic terminal guard; the successful first hop retains that flag even though no fallback was needed.
    • Fix
      • Allow the Anthropic terminal guard for a successful primary hop in provider-fallback mode, or introduce a separate option that distinguishes combo retry mechanics from terminal-continuation suppression. Preserve combo failure handling while ensuring the bounded continuation executes before treating the primary hop as successful.

    T-Rex Ran code and verified through T-Rex

  3. General comment

    P1 Deleting a provider leaves dangling per-provider fallback references that invalidate persisted configuration

    • Bug
      • DELETE /api/providers?name=b returns HTTP 200 OK and persists the removal even when provider A contains fallback: [{ provider: "b", model: "fallback-model" }]. The fallback remains in the file. On a later configuration reload, strict fallback validation rejects the file, readConfigDiagnostics() reports source=fallback, and loadConfig() backs up the invalid file and replaces the active configuration with defaults.
    • Cause
      • The deletion handler in src/server/management/provider-routes.ts checks combo dependencies and removes provider context-cap state, but neither checks provider fallback dependencies nor removes fallback targets pointing at the deleted provider. In contrast, src/config.ts validates every fallback target against the current provider map.
    • Fix
      • Before deleting, either return a conflict response that identifies providers whose fallback lists reference the target, or atomically filter deleted-provider targets from every provider fallback list before saving. Add an endpoint-level regression test that persists A→B fallback, deletes B, and verifies readConfigDiagnostics().source === "file" after reload.

    T-Rex Ran code and verified through T-Rex

Fix All in Cursor Fix All in Codex Fix All in Claude Code Fix All in Conductor

Prompt To Fix All With AI
### Issue 1
src/server/responses/core.ts:955
**Fallback disables Anthropic terminal continuation**

A request to an Anthropic provider with a usable fallback is delegated to `handleComboResponses`, which invokes the successful primary hop with `comboAttempt: true`. The Anthropic terminal guard requires `!options.comboAttempt`, so the primary response cannot make its normal bounded continuation even when no fallback is needed. This produces a completed but incomplete response when the first response announces a plan and needs the continuation to emit its tool call.

### Issue 2
src/server/management/provider-routes.ts:387
**Provider deletion preserves dangling fallback targets**

Deleting a provider checks dependencies from combos but not from other providers' `fallback` lists. Deleting B after configuring A to fall back to B returns success while retaining A's target for B. On the next configuration load, strict fallback validation rejects the persisted file and the application replaces the active configuration with defaults.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(providers): generalize account UI a..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - Focking gretig zijn en niet stoppen tot perfectie.... (source)

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Target branch corrected

This pull request now targets dev.

The [WRONG BRANCH] title prefix has been removed. The pull request has been marked ready for review again.

@github-actions github-actions Bot changed the title feat(providers): generalize accounts UI and per-provider fallback [WRONG BRANCH] feat(providers): generalize accounts UI and per-provider fallback Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR removes Codex Auth from the GUI and redirects legacy hashes to Providers. It adds ordered provider/model fallback configuration, validation, management API support, retry routing, logging, localization, and end-to-end coverage.

Changes

Provider fallback and navigation

Layer / File(s) Summary
Remove Codex Auth and redirect legacy hashes
gui/src/App.tsx, gui/src/app-routing.ts, gui/tests/dashboard-tabs.test.ts, gui/tests/sidebar-codex-auth.test.ts
The sidebar, page type, rendering branch, and translation mapping no longer include Codex Auth. Legacy Codex Auth hashes resolve to Providers without adding history entries.
Define and validate fallback targets
src/types.ts, src/providers/fallback.ts, src/config.ts, src/server/management/provider-routes.ts, src/server/auth-cors.ts, tests/provider-fallback.test.ts
Provider configuration supports ordered fallback targets. Validation rejects malformed, unknown, self-referencing, duplicate, and incomplete targets. API responses expose the configuration, and PATCH requests normalize and store it.
Add fallback configuration to provider settings
gui/src/components/provider-workspace/*, gui/src/pages/Providers.tsx, gui/src/pages/providers-shared.ts, gui/src/provider-workspace/catalog.ts, gui/src/styles/provider-workspace-settings.css, gui/src/i18n/*, gui/tests/provider-settings-fallback.test.tsx
Provider settings display peer providers and allow users to add, edit, remove, save, and discard ordered fallback provider/model pairs.
Route retryable responses through fallbacks
src/server/responses/core.ts, tests/server-combo-failover-e2e.test.ts
Plain model requests use synthetic fallback combos for eligible retryable failures. Explicit combo requests retain existing behavior, and fallback logs preserve the requested model and winning route.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProvidersPage
  participant ProviderSettings
  participant ProviderRoutes
  participant ResponsesCore
  participant UpstreamProvider
  Client->>ProvidersPage: open provider settings
  ProvidersPage->>ProviderSettings: provide peer provider metadata
  ProviderSettings->>ProviderRoutes: PATCH fallback targets
  ProviderRoutes-->>ProviderSettings: normalized provider configuration
  Client->>ResponsesCore: send plain model request
  ResponsesCore->>UpstreamProvider: attempt configured route
  UpstreamProvider-->>ResponsesCore: retryable failure
  ResponsesCore->>UpstreamProvider: attempt fallback route
  UpstreamProvider-->>ResponsesCore: fallback response
  ResponsesCore-->>Client: response with preserved model identity
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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: provider-wide account management and per-provider fallback configuration.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/all-provider-account-management

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.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

// below, which the combo path deliberately skips.
if (!options.comboAttempt && !isThreadSpawnRequest(req.headers)) {
const plan = providerFallbackPlan(config, { provider: route.providerName, modelId: route.modelId });
if (plan) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Fallback disables Anthropic terminal continuation

A request to an Anthropic provider with a usable fallback is delegated to handleComboResponses, which invokes the successful primary hop with comboAttempt: true. The Anthropic terminal guard requires !options.comboAttempt, so the primary response cannot make its normal bounded continuation even when no fallback is needed. This produces a completed but incomplete response when the first response announces a plan and needs the continuation to emit its tool call.

Context Used: Focking gretig zijn en niet stoppen tot perfectie.... (source)

Artifacts

Focused reproduction source for direct and fallback Anthropic primary requests

  • This executable Bun script sends matched successful Anthropic primary requests with and without a usable fallback and checks whether the terminal continuation forwards the tool call, demonstrating the fallback-specific bypass.

Observed fallback reproduction output

  • This captured command output shows the direct case made two upstream calls and forwarded the tool call, while the usable-fallback case made one call and omitted it, proving the incomplete successful response.

Existing direct Anthropic terminal guard test output

  • This captured test output shows the existing direct Anthropic terminal-guard integration test passed, establishing the control behavior that the fallback path loses.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/responses/core.ts
Line: 955

Comment:
**Fallback disables Anthropic terminal continuation**

A request to an Anthropic provider with a usable fallback is delegated to `handleComboResponses`, which invokes the successful primary hop with `comboAttempt: true`. The Anthropic terminal guard requires `!options.comboAttempt`, so the primary response cannot make its normal bounded continuation even when no fallback is needed. This produces a completed but incomplete response when the first response announces a plan and needs the continuation to emit its tool call.

**Context Used:** Focking gretig zijn en niet stoppen tot perfectie.... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Codex Fix in Claude Code Fix in Conductor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/management/provider-routes.ts (1)

373-395: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add a dependent-fallback check to DELETE /api/providers.

This handler already blocks deleting a provider referenced by a combo target (dependentCombos, lines 377-386), but it does not check whether any OTHER provider's new fallback array (introduced by this PR) references the provider being deleted.

Failure mode: with provider a configured as fallback: [{ provider: "b", model: "m2" }], DELETE /api/providers?name=b succeeds today with no 409. providerFallbackPlan's usable() check in src/providers/fallback.ts (lines 145-148) will silently skip the now-missing target at runtime, so a live request does not crash — but provider a's config now carries a permanently dangling reference that providerFallbackIssues would reject as fallback[0].provider "b" is not configured on the next full-schema validation pass (src/config.ts lines 564-570), for example on process restart via loadConfig(). That is exactly the kind of validation failure the streamMode comment in src/config.ts warns can trip a "backup-and-defaults repair path" that wipes providers/pool accounts.

Mirror the existing dependentCombos pattern: scan config.providers for any OTHER provider whose fallback array references name, and return 409 with the offending provider names if any are found.

🛠️ Proposed fix: block deletion when another provider's fallback references this one
     const dependentCombos = Object.entries(config.combos ?? {})
       .filter(([, combo]) => combo.targets.some(target => target.provider === name))
       .map(([id]) => id)
       .sort((a, b) => a.localeCompare(b));
     if (dependentCombos.length > 0) {
       return jsonResponse({
         error: `cannot delete provider "${name}" while combos depend on it`,
         combos: dependentCombos,
       }, 409);
     }
+    const dependentFallbackProviders = Object.entries(config.providers)
+      .filter(([providerName, p]) => providerName !== name
+        && Array.isArray(p.fallback)
+        && p.fallback.some(target => target?.provider === name))
+      .map(([providerName]) => providerName)
+      .sort((a, b) => a.localeCompare(b));
+    if (dependentFallbackProviders.length > 0) {
+      return jsonResponse({
+        error: `cannot delete provider "${name}" while other providers list it as a fallback target`,
+        providers: dependentFallbackProviders,
+      }, 409);
+    }
     const { saveConfigPreservingClaudeCode: save } = await import("../../config");
     delete config.providers[name];

Run this to confirm whether providerManagementConfigError (used by the POST handler) already validates a new provider's fallback field against currently configured providers, in case POST has the same class of gap:

#!/bin/bash
#!/bin/bash
ast-grep run --pattern 'function providerManagementConfigError($$$) { $$$ }' --lang typescript src
rg -n -A 20 'function providerManagementConfigError' src
🤖 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 `@src/server/management/provider-routes.ts` around lines 373 - 395, Update the
DELETE /api/providers handler around dependentCombos to also scan other
providers’ fallback arrays for references to the provider being deleted,
excluding the provider itself. Collect and deterministically sort the offending
provider names, and return a 409 response identifying them before mutating
config; preserve the existing combo dependency and deletion behavior.
🤖 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/app-routing.ts`:
- Around line 84-89: Update the app route initialization and hash-change
handling in use-app-route-state.ts to pass hashes through resolveAppHashChange,
ensuring legacy codex-auth and codex-auth/... hashes resolve to the providers
page instead of readPageFromHash’s dashboard fallback. Preserve replacement of
the legacy hash with the providers hash for both initial loads and subsequent
hash changes.

In `@gui/tests/provider-settings-fallback.test.tsx`:
- Around line 34-52: Add an interaction-focused test alongside “ProviderSettings
renders configured fallback targets” that edits a fallback provider or model,
submits the form, and asserts the emitted onUpdateProvider patch contains the
exact ordered fallback array. Add a second assertion covering an incomplete
fallback row and verify submission does not emit an update.

In `@src/providers/fallback.ts`:
- Around line 33-47: Bound retained state for synthetic fallback combo IDs
created by syntheticComboId and providerFallbackPlan, grouping entries by
physical provider rather than model ID. Add eviction or a per-provider cap for
these IDs and apply the same policy to both targetCooldowns used by
coolComboTarget/isComboTargetInCooldown and selectionState used by
pickComboTarget. Preserve existing behavior for explicit user combo IDs and
ensure evicted synthetic IDs are removed from both state maps.

In `@src/server/responses/core.ts`:
- Around line 950-959: Clear previous_response_id after
expandPreviousResponseInput completes, or immediately before
concreteComboRequestBody creates each fallback child body, so recursive
handleResponses calls cannot expand the same history twice. Preserve normal
previous-response behavior for the initial request, and add a regression case in
tests/server-combo-failover-e2e.test.ts covering a chained plain provider
fallback and asserting the child request contains no duplicated restored input.

---

Outside diff comments:
In `@src/server/management/provider-routes.ts`:
- Around line 373-395: Update the DELETE /api/providers handler around
dependentCombos to also scan other providers’ fallback arrays for references to
the provider being deleted, excluding the provider itself. Collect and
deterministically sort the offending provider names, and return a 409 response
identifying them before mutating config; preserve the existing combo dependency
and deletion behavior.
🪄 Autofix (Beta)

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: ceb34580-7da8-4905-8f50-ed00f903983a

📥 Commits

Reviewing files that changed from the base of the PR and between ae6efe6 and a20daf9.

📒 Files selected for processing (27)
  • gui/src/App.tsx
  • gui/src/app-routing.ts
  • gui/src/components/provider-workspace/ProviderDetails.tsx
  • gui/src/components/provider-workspace/ProviderSettings.tsx
  • gui/src/components/provider-workspace/types.ts
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/nl.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/zh.ts
  • gui/src/pages/Providers.tsx
  • gui/src/pages/providers-shared.ts
  • gui/src/provider-workspace/catalog.ts
  • gui/src/styles/provider-workspace-settings.css
  • gui/tests/dashboard-tabs.test.ts
  • gui/tests/provider-settings-fallback.test.tsx
  • gui/tests/sidebar-codex-auth.test.ts
  • src/config.ts
  • src/providers/fallback.ts
  • src/server/auth-cors.ts
  • src/server/management/provider-routes.ts
  • src/server/responses/core.ts
  • src/types.ts
  • tests/provider-fallback.test.ts
  • tests/server-combo-failover-e2e.test.ts

Comment thread gui/src/app-routing.ts
Comment on lines +84 to +89
// Account management used to be a Codex-only destination. Providers now owns
// OAuth accounts, API-key pools and the OpenAI/Codex pool in one place.
if (rawHash === "codex-auth" || rawHash.startsWith("codex-auth/")) {
return { page: "providers", replaceTo: "providers" };
}

Copy link
Copy Markdown

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

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 \
  'readPageFromHash|resolveAppHashChange|hashchange|location\.hash' \
  gui/src --glob '*.ts' --glob '*.tsx'

Repository: OnlineChefGroep/opencodex

Length of output: 16661


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '--- gui/src/app-routing.ts ---\n'
cat -n gui/src/app-routing.ts

printf '\n--- gui/src/use-app-route-state.ts ---\n'
cat -n gui/src/use-app-route-state.ts

printf '\n--- gui/src/App.tsx relevant ---\n'
sed -n '1,170p' gui/src/App.tsx | cat -n

printf '\n--- deterministic legacy hash behavior probe ---\n'
python3 - <<'PY'
from pathlib import Path
src = Path('gui/src/app-routing.ts').read_text()
checks = {
    'VALID_PAGES_contains_codex_auth': '"codex-auth"' in split_line(src, 'VALID_PAGES'),
    'resolveAppHashChange_reads_page_first': 'const nextPage = readPageFromHash(rawHash);' in src and 'if (pageId === ("debug" as Page)) return "logs";' in extract_function(src, 'resolveAppHashChange'),
    'resolveAppHashChange_redirects_codex_auth': 'rawHash === "codex-auth" || rawHash.startsWith("codex-auth/")' in extract_function(src, 'resolveAppHashChange'),
    'useAppRouteState_initializes_from_readPageFromHash': 'useState<Page>(readPageFromHash)' in Path('gui/src/use-app-route-state.ts').read_text(),
    'useAppRouteState_hashchange_uses_applyHashAction': 'applyHashAction(normalizeHashPath(window.location.hash));' in Path('gui/src/use-app-route-state.ts').read_text(),
}
for k, v in checks.items():
    print(f'{k}={v}')

def split_line(text, start):
    for line in text.splitlines():
        if start in line:
            return line
    return ''
def extract_function(text, name):
    i=text.find(f'export function {name}')
    j=text.find('\n}\n', i)
    if j == -1:
        j=text.find('};', i)
    return text[i:j+2] if i!=-1 else ''
PY

Repository: OnlineChefGroep/opencodex

Length of output: 254


Route codex-auth legacy hashes through resolveAppHashChange.

#codex-auth/accounts initializes or updates to the fallback page "dashboard" because readPageFromHash returns "dashboard" when "codex-auth" is not in VALID_PAGES. Only resolveAppHashChange maps those legacy hashes to "providers". In gui/src/use-app-route-state.ts, the initial useState<Page>(readPageFromHash) can pick up that legacy hash before hashchange; route the initial hash and hashchange updates through resolveAppHashChange so bookmarked codex-auth links open the Providers page without the old hash.

🤖 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/app-routing.ts` around lines 84 - 89, Update the app route
initialization and hash-change handling in use-app-route-state.ts to pass hashes
through resolveAppHashChange, ensuring legacy codex-auth and codex-auth/...
hashes resolve to the providers page instead of readPageFromHash’s dashboard
fallback. Preserve replacement of the legacy hash with the providers hash for
both initial loads and subsequent hash changes.

Comment on lines +34 to +52
test("ProviderSettings renders configured fallback targets", () => {
const html = renderToStaticMarkup(
<LanguageProvider>
<ProviderSettings
item={item}
peerProviders={[
{ name: "google-antigravity", models: ["gemini-3.6-flash"] },
{ name: "deepseek", models: ["deepseek-v4-flash"], defaultModel: "deepseek-v4-flash" },
{ name: "cursor", models: ["claude-sonnet-5"] },
]}
/>
</LanguageProvider>,
);

expect(html).toContain("Fallback providers");
expect(html).toContain("deepseek");
expect(html).toContain("deepseek-v4-flash");
expect(html).toContain("Add fallback");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add an interaction test for fallback persistence.

This test only verifies static markup. It does not invoke onUpdateProvider.

Add a focused UI test that changes a fallback provider or model, saves the form, and asserts the exact ordered fallback array in the emitted patch. Also assert that an incomplete row blocks the update.

🤖 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/tests/provider-settings-fallback.test.tsx` around lines 34 - 52, Add an
interaction-focused test alongside “ProviderSettings renders configured fallback
targets” that edits a fallback provider or model, submits the form, and asserts
the emitted onUpdateProvider patch contains the exact ordered fallback array.
Add a second assertion covering an incomplete fallback row and verify submission
does not emit an update.

Comment thread src/providers/fallback.ts
Comment on lines +33 to +47
function syntheticComboId(provider: string, model: string): string {
return `provider-fallback\u0000${provider}\u0000${model}`;
}

/** True when `id` came from `syntheticComboId` rather than the user's combos map. */
export function isProviderFallbackComboId(id: string): boolean {
return id.startsWith("provider-fallback\u0000");
}

/** Human-readable form of a combo id for logs and error messages (NUL is not printable). */
export function comboIdLabel(id: string): string {
if (!isProviderFallbackComboId(id)) return id;
const [, provider, model] = id.split("\u0000");
return `fallback:${provider}/${model}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate the combo cooldown/target-state store to check for eviction/TTL bounding.
set -euo pipefail

fd -e ts . src/combos --exec rg -n -C5 'isComboTargetInCooldown|advanceComboAfterFailure|noteComboSuccess|pickComboTarget' {} \;
rg -n -C5 'new Map|WeakMap|setInterval|evict|ttl' src/combos

Repository: OnlineChefGroep/opencodex

Length of output: 3255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching failover/resolve/types/fallback:"
fd -e ts '^(failover|resolve|types|fallback)\.ts$' src | sort

echo
echo "Relevant source snippets:"
for f in src/combos/failover.ts src/combos/resolve.ts src/combos/types.ts src/providers/fallback.ts; do
  echo "--- $f ---"
  if [ -f "$f" ]; then
    wc -l "$f"
    sed -n '1,220p' "$f" | nl -ba
  fi
done

echo
echo "Cross-repo usages of comboId creation/path:"
rg -n -C3 "syntheticComboId|providerFallbackPlan|pickComboTarget|tryPickComboModel|comboIdLabel|clearComboSelectionState" src tests || true

Repository: OnlineChefGroep/opencodex

Length of output: 489


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
ls -l src/combos/*.ts src/providers/fallback.ts

echo
echo "src/combos/failover.ts:"
cat -n src/combos/failover.ts | sed -n '1,180p'

echo
echo "src/combos/resolve.ts:"
cat -n src/combos/resolve.ts | sed -n '1,230p'

echo
echo "src/combos/types.ts:"
cat -n src/combos/types.ts | sed -n '1,220p'

echo
echo "src/providers/fallback.ts:"
cat -n src/providers/fallback.ts | sed -n '1,220p'

echo
echo "Code references:"
rg -n -C3 "syntheticComboId|providerFallbackPlan|pickComboTarget|tryPickComboModel|comboIdLabel|clearComboSelectionState|combineProviderFallback|fallback" src tests || true

Repository: OnlineChefGroep/opencodex

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Combos failover:"
sed -n '1,160p' src/combos/failover.ts

echo
echo "Combos resolve:"
sed -n '1,220p' src/combos/resolve.ts

echo
echo "Combos types:"
sed -n '1,160p' src/combos/types.ts

echo
echo "Provider fallback:"
sed -n '1,210p' src/providers/fallback.ts

echo
echo "Focused search for synthetic combo id callers and cooldown helpers:"
rg -n -C4 "syntheticComboId|providerFallbackPlan|providerFallbackTargets|clearComboTargetCooldowns|coolComboTarget|isComboTargetInCooldown|advanceComboAfterFailure|noteComboSuccess|pickComboTarget" src -g '*.ts'

echo
echo "Programmatic static lookup for cooldownMapKey definitions/usages and interval/eviction references in src/combos/src/providers:"
rg -n "cooldownMapKey\\(|cooldownMap|new Map|WeakMap|setInterval|evict|ttl|clearComboTargetCooldowns" src/combos src/providers src/types.ts

Repository: OnlineChefGroep/opencodex

Length of output: 46645


Bound cooldown/growth state for synthetic fallback combo ids.

providerFallbackPlan at src/providers/fallback.ts:161 creates a distinct syntheticComboId per (provider, modelId) pair. coolComboTarget() and isComboTargetInCooldown() use src/combos/failover.ts:10 targetCooldowns, and pickComboTarget() uses src/combos/resolve.ts:29 selectionState, both plain process maps. These entries are only removed when the key expires (MAX_COOLDOWN_MS for cooldowns) or when an explicit combo/management path deletes a combo id; plain model fallback ids have no eviction or per-provider cap. If callers request many distinct model ids against a provider with fallback configured, memory grows unboundedly. Add an eviction policy or cap the number of retained synthetic combo ids per physical provider, and apply the same bound to selectionState.

🤖 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 `@src/providers/fallback.ts` around lines 33 - 47, Bound retained state for
synthetic fallback combo IDs created by syntheticComboId and
providerFallbackPlan, grouping entries by physical provider rather than model
ID. Add eviction or a per-provider cap for these IDs and apply the same policy
to both targetCooldowns used by coolComboTarget/isComboTargetInCooldown and
selectionState used by pickComboTarget. Preserve existing behavior for explicit
user combo IDs and ensure evicted synthetic IDs are removed from both state
maps.

Comment on lines +950 to +959
// Per-provider fallback replays the request across the provider's configured targets using the
// combo hop loop. Thread spawns are excluded: they carry their own Codex account/model fallback
// below, which the combo path deliberately skips.
if (!options.comboAttempt && !isThreadSpawnRequest(req.headers)) {
const plan = providerFallbackPlan(config, { provider: route.providerName, modelId: route.modelId });
if (plan) {
return handleComboResponses(req, body, plan.comboId, plan.config, logCtx, options);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect expandPreviousResponseInput and concreteComboRequestBody for idempotency
# on an already-expanded body / whether previous_response_id survives into the combo child body.
set -euo pipefail

rg -n -B2 -A 25 'function expandPreviousResponseInput' src
rg -n -B2 -A 30 'function concreteComboRequestBody' src
rg -n 'previous_response_id' src/server/responses src/combos

Repository: OnlineChefGroep/opencodex

Length of output: 4778


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- locate core combo helpers and request module ---\n'
rg -n 'handleComboResponses|expandPreviousResponseInput|sanitizeEncryptedContentInPlace|previous_response_id|concreteComboRequestBody' src/server/responses/core.ts src/combos/request.ts src/responses/state.ts

printf '\n--- core.ts relevant sections ---\n'
sed -n '840,975p' src/server/responses/core.ts
sed -n '1130,1205p' src/server/responses/core.ts
sed -n '2035,2070p' src/server/responses/core.ts

printf '\n--- combos request.ts ---\n'
cat -n src/combos/request.ts

printf '\n--- state.ts relevant section ---\n'
sed -n '300,340p' src/responses/state.ts

Repository: OnlineChefGroep/opencodex

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate core combo helpers and request module ---'
rg -n 'handleComboResponses|expandPreviousResponseInput|sanitizeEncryptedContentInPlace|previous_response_id|concreteComboRequestBody' src/server/responses/core.ts src/combos/request.ts src/responses/state.ts || true

echo
echo '--- core.ts relevant sections ---'
sed -n '840,975p' src/server/responses/core.ts
sed -n '1130,1205p' src/server/responses/core.ts
sed -n '2035,2070p' src/server/responses/core.ts || true

echo
echo '--- combos request.ts ---'
cat -n src/combos/request.ts

echo
echo '--- state.ts relevant section ---'
sed -n '300,340p' src/responses/state.ts

Repository: OnlineChefGroep/opencodex

Length of output: 19024


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- core.ts handleComboResponses section ---'
sed -n '620,730p' src/server/responses/core.ts

echo
echo '--- tests file presence ---'
git ls-files | rg 'server-combo-failover-e2e\.test\.ts$' || true

echo
echo '--- deterministic state shape probe from source ---'
python3 - <<'PY'
import re
from pathlib import Path
src = Path('src/responses/state.ts').read_text()
m = re.search(r'export function expandPreviousResponseInput\(body: unknown\): unknown \{(?P<body>.*?)\n\}', src, re.S)
print(m.group(0) if m else 'expandPreviousResponseInput not found')
print('reassigns input:', 'input: [...previous.items, ...inputItems(request.input)]' in src)
print('clears previous_response_id:', 'delete request.previous_response_id' in src or 'delete \\.previous_response_id' in src)
PY

Repository: OnlineChefGroep/opencodex

Length of output: 5441


Clear previous_response_id before replaying fallback combo child requests.

expandPreviousResponseInput expands input but leaves previous_response_id on the request; it also reuses the cached expanded request shape via replayedInputPrefixLengths. Plain requests that fall back through providerFallbackPlan call handleComboResponses with that expanded body, then concreteComboRequestBody clones it as childBody; the recursive handleResponses call can expand the same previous_response_id again and duplicate the restored items in the child request sent to the fallback target. This is less likely to persist through an already-expanded body, but plain provider fallback now enters this route for the first time, so clear the field after the first expansion or before concreteComboRequestBody builds the child request, and add a regression test in tests/server-combo-failover-e2e.test.ts that sends a previous_response_id-chained plain request against a fallback provider and asserts duplicated input is not sent to the child.

🤖 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 `@src/server/responses/core.ts` around lines 950 - 959, Clear
previous_response_id after expandPreviousResponseInput completes, or immediately
before concreteComboRequestBody creates each fallback child body, so recursive
handleResponses calls cannot expand the same history twice. Preserve normal
previous-response behavior for the initial request, and add a regression case in
tests/server-combo-failover-e2e.test.ts covering a chained plain provider
fallback and asserting the child request contains no duplicated restored input.

Collapse Codex-only auth nav into Providers, add configurable
per-provider fallback hops, and keep runtime failover on the
existing combo engine so sofie patches stay reproducible.

Co-authored-by: Cursor <cursoragent@cursor.com>

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
@OnlineChef
OnlineChef force-pushed the feat/all-provider-account-management branch from a20daf9 to c23de7a Compare August 1, 2026 07:04
@OnlineChef
OnlineChef changed the base branch from main to dev August 1, 2026 07:04
@github-actions github-actions Bot changed the title [WRONG BRANCH] feat(providers): generalize accounts UI and per-provider fallback feat(providers): generalize accounts UI and per-provider fallback Aug 1, 2026
@github-actions github-actions Bot added the enhancement New feature or request label Aug 1, 2026
OnlineChef and others added 2 commits August 1, 2026 07:20
… fallback hops

- keep the Anthropic terminal-continuation guard active on the primary hop of a
  per-provider fallback chain (synthetic combos are not user combos)
- hand the pre-expansion body to the fallback combo so previous_response_id is
  expanded exactly once
- sweep expired combo-target cooldowns so synthetic combo ids cannot accumulate
  state forever
- resolve the initial GUI route from the hash through resolveAppHashChange so a
  bookmarked #codex-auth lands on Providers
- cover the fallback editor interaction (ordered patch, incomplete row blocks save)

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
React Doctor's no-array-index-as-key fired on the fallback editor: rows are
user-reorderable and removable, so the index is not a stable identity. Carry a
per-row id in form state and drive updates/removal off it.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
@OnlineChef
OnlineChef merged commit a6d347c into dev Aug 1, 2026
10 checks passed
@OnlineChef
OnlineChef deleted the feat/all-provider-account-management branch August 1, 2026 07:41
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.

1 participant