Repository navigation
feat(fx): choose a backend per thread - #29388
jakeleventhal wants to merge 5 commits into
Conversation
fx can run on Vercel AI Gateway, a Codex or Grok subscription, or a configured OpenAI-compatible endpoint, but T3 Code only offered the backend `fx provider` had set, so switching meant a terminal. The fx snapshot now probes every backend with `FX_PROVIDER` (`fx status` for sign-in, `fx models` for its catalog) and lists models as `<backend>:<model>`. A new `subProviders` field on the provider snapshot lets the web picker and the mobile thread settings group them under collapsible backend headings; signed-out backends stay listed as unavailable with the `fx login` command to run. The adapter writes fx's per-session `provider` option before `model`, so each thread keeps its own backend without touching fx's global default. "Default" now resolves with `fx status` in the thread's folder, so a model fx's settings give one project is picked up there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR requires human approval — The PR adds per-thread fx backend selection and grouped model pickers. It also changes Default to honor project-folder backend/model settings, which needs human confirmation because existing Default selections may run on a different backend. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Sub-provider data and grouping packages/contracts/src/server.ts, packages/client-runtime/src/providerSubProviders.ts, packages/client-runtime/package.json, apps/mobile/src/lib/modelOptions.ts, apps/mobile/src/lib/modelOptions.test.ts |
Provider contracts and model options now carry sub-provider descriptors. The shared grouping helper retains descriptor order and empty groups. The mobile test covers grouped, ungrouped, and empty-group cases. |
Backend discovery and status packages/provider-fx/src/server/status.ts, packages/provider-fx/src/server/status.test.ts |
The status check discovers configured backends and probes authentication and model lists per backend. The resulting snapshot includes backend-specific models and availability details. Tests cover probe results, status cases, and default choices read for a working directory. |
Backend-qualified model selection packages/provider-fx/src/server/acpSupport.ts, packages/provider-fx/src/server/adapter.ts, packages/provider-fx/src/server/driver.ts, packages/provider-fx/src/server/acpSupport.test.ts, apps/server/src/orchestration-v2/Adapters/FxAdapterV2.test.ts |
Model choices can specify a backend and model. The adapter resolves default choices using each runtime’s working directory and applies backend changes before model changes. Tests cover backend changes and restoration after a failed default selection. |
Backend sections in model pickers apps/web/src/components/chat/ModelPickerContent.tsx, apps/web/src/components/chat/modelPickerKeys.ts, apps/mobile/src/features/threads/ThreadSettingsSheet.tsx, docs/user/providers-fx.md |
The web and mobile pickers display provider models in backend sections with availability details and expansion controls. Search and Favorites remain flat. The fx documentation describes backend selection and default behavior. |
Estimated code review effort: 4 (Complex) | ~45 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant Picker as Model picker
participant Adapter as FxAdapterV2
participant Choice as readFxDefaultChoice
participant CLI as fx CLI
participant Session as ACP session config
Picker->>Adapter: Send backend-qualified model selection
Adapter->>Choice: Resolve default choice for runtime cwd
Choice->>CLI: Run fx status --json in runtime cwd
CLI-->>Choice: Return model and model_source
Choice-->>Adapter: Return backend and model
Adapter->>Session: Set provider before model when backend changes
Suggested reviewers: agadzik, juliusmarminge
Merge Risk: 
Minimal · up to 31616
-
No unresolved issue is established for the changed Default-selection behavior; the PR is mergeable after normal checks.
Pre-merge checks: 
3 · 
1
-
Failed checks: 1 inconclusive Passed checks: 3 passed-
Full details: Title check
-
Explanation
The title "feat(fx): choose a backend per thread" matches the main change: per-thread fx backend selection, with grouping of models by backend and a folder-aware Default. It is concise, follows the conventional commit style used in the template, and names the primary feature.
-
Full details: Description check
-
Explanation
The description follows the template's sections (Problem, Change, Scope and approval, Verification) with substantial content. It explains the problem (T3 Code only offers the machine's active fx backend) and the change (per-thread backend selection, grouping in web and mobile, folder-aware Default, docs). Verification lists focused tests, the check run, and states what was not checked. Gaps remain: the Scope and approval section cites a closed PR and asks for reconsideration instead of linking a triaged issue or maintainer approval, and the template's requirement for before/after UI screenshots is only partly met by historical captures that predate the rebased head. The description is detailed but long, and its Problem section is not a clear one- or two-sentence statement with reproduction steps.
Resolution
Add a concise Problem statement with expected behavior and reproduction steps. In Scope and approval, link the triaged issue or the maintainer discussion that approved this scope, including the approval comment. The Default folder-aware and backend-switching behavior needs explicit maintainer approval, or a justification under the template's exemption rules. Provide before/after UI screenshots or a recording for the current rebased head, or state clearly that they are pending. Trim the Verification and history sections so the current validation is easy to find.
-
Finishing Touches
-
Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
|
Note Written by Verified in a real dev environmentverified head
command transcript and saved-session transitions before, same thread and model: pr, matching picker state: folder-aware default selection and a completed real turn. the session log in the transcript confirms Not verifiednative ios/android rendering, electron shell behavior, macos/windows, and relay/tunnel connections were not exercised on this linux web run. fx built-in subscription and gateway turns require credentials unavailable here; real backend-switching checks used configured endpoints with the same |



Replacement for #26994, preserving its full discussion and evidence links. GitHub rejected reopening the original through GraphQL and REST (
state cannot be changed. The fx-backend-picker branch was force-pushed or recreated.); restoring the closing head also did not allow reopening. The validated rebased head remains on the original fork branch.fx can run on Vercel AI Gateway, a Codex or Grok subscription, or a configured OpenAI-compatible endpoint, but T3 Code only offers the machine's active backend. This adds per-thread backend selection and restores Default as a valid backend/model pair, using the thread's folder configuration.
Latest review follow-up
31616ad restores the original backend/model pair when selection fails after changing backends. Restoring the backend can itself select a saved model, so rollback also restores the original model when needed. Missing-Default and rejected-model errors retain their original failure. If fx rejects rollback, that error is logged and selection still fails before a turn starts.
The two state-based regressions failed before the fix with the session left on Gateway, then passed with the original Codex custom-model pair restored. All eight focused cases and
vp run check:changed HEAD --concurrency 2passed: nine adjacent tests, 10 affected-package typechecks, formatting, lint, both Knip passes and GraphQL checks. The loaded-session protocol case now expects the rollback provider request.git diff --checkpassed. Rollback validation receipt.The preceding 0e58bfc head passed CI, but rejecting selection alone did not restore the native session pair; this follow-up supersedes that limitation. 31616ad awaits fresh CI and bot review. Maintainer approval of Default’s folder-aware/backend-switching behavior and the missing fresh before/native UI evidence remain pending. No real fx or integrated UI run was performed for this follow-up.
Scope and reconsideration
Reconsidering the closure explanation and closed event 34691805908. Upstream still exposes only the machine-default fx backend; per-backend catalogs, grouping, switching, and folder-aware Default remain absent.
Backend selection and resetting Default share a concrete invariant: restore a backend and a model that belong together. After a thread switches from Gateway to Codex, selecting Default must restore the default backend before its model; writing the Gateway model on Codex can fail or leave the previous model running. The adapter replay test covers both switching backends in one session and restoring a loaded session to the default backend, with the provider write before the model write.
Folder-aware lookup is independently separable. It is retained here deliberately because the same Default resolution path returns that backend/model pair in the thread's folder; it is not a required technical dependency of backend grouping. This is a request to reconsider the combined scope, not a claim that maintainers approved it. The closure's missing before/native visual evidence remains a limitation, explicitly recorded below.
The rebase preserves upstream's compact picker, trailing Show more/less control, effort column and IME handling (#28057/#28076/#28409), mobile policy and reduced-motion behavior, and the longer fx version-probe timeout (#28400). It also retains the final auth fallback correction: unknown Gateway sign-in cannot hide a signed-in Codex backend.
Fix
fx status --json(sign-in) andfx models --json(catalog) once per backend withFX_PROVIDER. Custom endpoints come from theproviderskeys in~/.fx/settings.json. Nothing opens an ACP session, which fx would save. Models are listed as<backend>:<model>(fx backend ids never contain:).ServerProvider.subProviders(name plus optionalunavailableReason) tells clients how to group a provider's models bysubProvider. Only fx sets it, so other providers are unchanged.fx login <backend>hint. Search and Favorites stay flat, retaining upstream's compact rows and inline backend labels.client-runtimeis shared with web.provideroption beforemodel, so each thread keeps its own backend and fx's global default is never changed. Logins stay machine-wide, which is why signed-out backends show as unavailable everywhere. A model id with no backend prefix (existing threads) still runs on the session's current backend.fx status --jsonrun in the thread's folder, so a per-project model in fx's settings is used there. On a session that is on another backend, Default now moves it to the default backend rather than staying put.docs/user/providers-fx.mdis updated.Prior visual evidence (2026-10-29)
These are historical captures of the original implementation, before upstream's later compact picker changes; they are not refreshed screenshots of this rebased head. Web, against a scripted fake
fx(Codex and Gateway signed in, Grok signed out).Before: the fx tab was a flat list of Default plus the models of the machine's active backend, with no way to switch backends.
Picker open, Grok signed out:
Switching backend: open Vercel AI Gateway from a thread on Codex:
The same thread ran one turn on Codex and the next on the gateway (the fake echoes the backend and model the session ran on):
Search across backends:
There is no before screenshot or interaction recording in the prior evidence. Mobile has no device capture: the remote iOS simulators were either owned by other sessions or stuck on the dev client's "Open in" prompt. The mobile grouping is covered by a unit test and typecheck only.
Prior validation
Original implementation attribution: Claude Opus 5.5 in Claude Code. This revival and rebase used GPT-6.1 Sol through Codex.
5a4461a033b56a3b866e6a6d9bfb14e593ad3e6a. CI run 40894823238 completed successfully on that head; this is prior CI, not a result for the rebased head.FX_PROVIDER/FX_MODEL, a custom endpoint, and a per-folder model. Those probes were not rerun during revival.check:changedreport noted unrelated macOS/varversus/private/varfailures inServerEnvironment.test.tsandbuild-npm-launcher.test.ts. Current results are listed separately below.Current validation
ac8dce0cc3abd8a337f2d806b5082a6834b9b81a.vp test run packages/provider-fx/src/server/status.test.ts apps/server/src/orchestration-v2/Adapters/FxAdapterV2.test.ts apps/mobile/src/lib/modelOptions.test.ts: 43 tests passed across three files. Covers per-backend catalogs/auth, unreadable-sign-in fallback, folder-aware defaults, provider-before-model writes, same-session backend switching, loaded-session Default reset, and grouping with a signed-out backend. The upstream six-second/hung-version probe cases are preserved using TestClock.vp run check:changed upstream/main --concurrency 2: passed. Format, lint, unused patches, both knip passes, GraphQL generation, 39 selected package typechecks, 54 tests in seven adjacent files, and nine selected React Compiler cases for four changed client files passed. CI still owns the full suites, builds, Rust, and release smoke.git diff --check: passed.Follow-up
a4dbade6c95f913fad03c45c281672dbbe2ae453fixes Default targeting an unavailable backend. Default retains the current backend/model pair when its target backend is neither current nor selectable. The existing adapter regression now covers selectable, unlisted and missing backend selectors; it failed before the fix and passes afterward.vp run check:changed HEAD --concurrency 2passed before committing, including both Knip passes, GraphQL checks, 10 affected-package typechecks and 7 adjacent tests. CI and bot reviews are rerunning for this follow-up.Follow-up
0e58bfcad4095b955759df19d70d19e89ae18d63addresses a partial Default backend/model switch: if the backend is selectable but cannot provide the requested Default model, selection fails before a turn can run. An unselectable backend still retains the previous pair. The expanded adapter case failed before the fix and now covers successful restoration and unavailable-model rejection. All five fx adapter cases andvp run check:changed HEAD --concurrency 2passed before committing (format, lint, both Knip passes, GraphQL checks, 10 affected-package typechecks and 7 adjacent tests). That head passed CI; the rollback correction and its current validation above supersede this earlier status.GPT-6.1 Sol through Codex.