Skip to content

fix(web): list the providers the server actually has, not a hardcoded copy - #2203

Merged
RaghavChamadiya merged 5 commits into
repowise-dev:mainfrom
oldschoola:fix/settings-providers-from-api
Sep 11, 2026
Merged

fix(web): list the providers the server actually has, not a hardcoded copy#2203
RaghavChamadiya merged 5 commits into
repowise-dev:mainfrom
oldschoola:fix/settings-providers-from-api

Conversation

@oldschoola

@oldschoola oldschoola commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

What this is

Not a fix for a reported symptom. It came out of wiring a new provider: it reached PROVIDER_CATALOG, /api/providers returned it, and the settings page still could not show it. The page renders its picker from a PROVIDERS array compiled into the bundle, so every new provider needs a second manual edit here — and two have been missed. codex_cli and openrouter are in the server catalog and are not in that array, so neither can be picked from this page today even though the backend resolves both.

The change

Rebased onto #2155, which is what makes this small: that PR already moved the active-provider read onto the typed getProviders() client, and the same response already carries the catalog. So this adds no request and no new data-fetching pattern — it destructures providers alongside active and renders the picker from it.

Three bugs the tests found, all mine

I wrote these tests expecting to confirm the change. Each time they showed it was wrong instead. Stating them because the third is the interesting one.

1. A stale local default beat the server's real one. MODEL_PLACEHOLDERS is a hardcoded table of each provider's default model and my first version left it ahead of the catalog — exactly the drift this PR exists to close, one level down. The catalog wins now; the local table is the cold-load stand-in behind it.

2. The catalog is not a superset of what can be selected. mock is registerable and keyless (_BUILTIN_PROVIDERS, KEYLESS_PROVIDERS) but is deliberately absent from PROVIDER_CATALOG. Rendering the catalog verbatim dropped it, so a user with mock saved got a <Select value="mock"> with no matching item — a blank trigger with nothing to recover with. Confirmed in jsdom before fixing: the provider trigger rendered "" while the embedder beside it rendered "mock".

3. My first fix for #2 was still wrong. I unioned in the selected provider, which repairs the blank trigger but not the real loss: a user sitting on gemini could no longer switch to mock at all. Nothing surfaces that — the trigger still reads gemini and looks entirely correct. The flag-only set is now unioned in alongside the selection:

const providerOptions = [...new Set([...providers, ...FLAG_ONLY_PROVIDERS, provider])];

If you would rather mock simply join PROVIDER_CATALOG server-side, that is arguably more consistent with this PR's thesis and I am happy to switch — I kept it client-side because the omission looks deliberate and the server list feeds more than this page.

Also

codex_cli and openrouter had no PROVIDER_ENV_VARS entry, so the two providers this PR newly makes selectable would have rendered with no key guidance and no install hint. Both added. Neither Select had an accessible name; both triggers now have one.

What it does not close

ProviderEntry is {id, name, models?, default_model?, configured?}no env_keys — so PROVIDER_ENV_VARS stays local and can still drift. Closing that means adding env_keys to the payload, which is a server change and not in this diff.

Merge order with #2194

They touch the same constants block: #2194 appends omp to the PROVIDERS array this PR renames to FALLBACK_PROVIDERS, so whichever lands second conflicts there. Resolving toward #2194's side would silently revert the catalog read. Each PR is correct standalone — #2194 keeps its array entry so it works on today's main — and if this lands first I'll rebase #2194 to drop that entry, since omp then arrives through the catalog for free. Flagging it so the conflict isn't resolved the other way by reflex.

Verification

7 tests in #2155's provider-section.test.tsx, reusing its getProviders mock. I mutation-checked all five new ones individually rather than trusting green — each fails for its own reason:

test fails when
catalog provider with no local entry supplies its placeholder the catalog read is reverted
server default beats a stale local entry the precedence is swapped back
no default_model falls back to the local table the ?? MODEL_PLACEHOLDERS term is dropped
selected provider stays selectable when the catalog omits it the union is removed
flag-only provider stays offered FLAG_ONLY_PROVIDERS is dropped from the union

Full packages/web suite: 20 failures before and after, identical to origin/main measured in a clean worktree; my 5 additions all pass. tsc --noEmit clean, no new lint warnings.

This is my least urgent open PR — entirely reasonable to park it or close it if you would rather the catalog stay duplicated in the bundle.

… copy

The settings picker rendered from a `PROVIDERS` array compiled into the page,
while `/api/providers` served the real catalog. The two had already drifted:
`codex_cli` and `openrouter` are in the server catalog and were never added
here, so neither could be selected from this page even though the backend
would have resolved both.

The array stays as a cold-load fallback for an API that has not answered yet,
but the list now comes from the catalog, and a provider's `default_model`
backfills the model placeholder. A provider added on the Python side shows up
here with no change to this file, which is what stops the next one drifting.
…s-from-api

# Conflicts:
#	packages/web/src/components/settings/provider-section.tsx
Reading the catalog for the picker left MODEL_PLACEHOLDERS ahead of it for
the model hint, so a stale local entry still beat the default the server
would actually use -- the same drift the catalog read exists to close, one
level down. The catalog is authoritative; the local table is the cold-load
stand-in behind it.

Covered by tests that fail when the fix is reverted and when the precedence
is swapped back.
…s it

Rendering the server catalog verbatim dropped any provider it does not
advertise. `mock` is flag-only and is genuinely absent from it, so a user
with mock saved got a Select whose value matched no item -- a blank trigger
with nothing to recover with. The selected value is now always among the
options.

Also gives codex_cli and openrouter the env-var and install-hint entries
they never had, since this is the change that first makes them selectable,
and names both Select triggers so they have an accessible name at all.
The previous union only rescued the *selected* provider, so a user on gemini
silently lost the ability to switch to `mock` at all -- it is registerable
and keyless but deliberately absent from PROVIDER_CATALOG, and the trigger
still looked correct, so nothing surfaced the missing option.

The flag-only set is unioned in alongside the selection.
@RaghavChamadiya

Copy link
Copy Markdown
Member

Thanks for this, and for writing up the three bugs your own tests caught. The third one is the interesting one, and I am glad you chased it rather than stopping at the blank trigger.

Rebasing onto #2155 was the right instinct. That PR moved the active-provider read onto the typed client, and this renders the picker from the same response with no extra request, so the two really are halves of one change. I confirmed the gap you describe: codex_cli and openrouter are both in the server catalog and neither is in the hardcoded array, so today neither can be picked even though the backend resolves both.

Unioning the flag-only set in alongside the selection is the right resolution for mock. Merging this into 0.50.0 so it ships with #2155 rather than landing a release behind it.

@RaghavChamadiya
RaghavChamadiya merged commit 76e3d6a into repowise-dev:main Sep 11, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants