Skip to content

Collapse the model-refresh vendor dispatch into one table and surface it as canRefreshModels - #3624

Merged
atomantic merged 2 commits into
mainfrom
claim/issue-3620
Aug 7, 2026
Merged

Collapse the model-refresh vendor dispatch into one table and surface it as canRefreshModels#3624
atomantic merged 2 commits into
mainfrom
claim/issue-3620

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Adding one vendor to the model-refresh feature took four coordinated hand edits — the TUI else if chain in refreshProviderModels, the CLI if chain in _refreshCLIProviderModels, the client's supportsModelRefresh mirror, and a second transcription of the server dispatch inside the client's parity test. That last one is the tell: the parity test re-implemented the dispatch rather than importing it, so it proved the client mirror matched the test's copy, not the server.

The drift was silent in both directions — a client clause that was too generous showed a Refresh button that 404'd; too stingy hid the feature with no error at all. A gemini provider configured by absolute path was in the second bucket (#3616).

Server: one table. New server/lib/aiToolkit/internal/modelFetchers.js holds one row per vendor, and both dispatch arms resolve from it. The three keying conventions that had accumulated as prose above each branch are columns now:

column signal
cliMatch strong: the launch command (path/exe-tolerant where the vendor needs it) or a structural marker (ollamaBacked)
cliNameMatch weak: a display-name substring, consulted only after no row claimed the provider by command
tuiMatch never consults the display name — an exact shipped id is the only non-command signal it admits

fetch is the provider-service method name, so the table stays a pure data module (a test pins that every name resolves to a real method).

Client: read the answer. canRefreshModels is exported as a pure predicate and decorated onto every provider-shaped response; supportsModelRefresh shrinks to (p) => p?.canRefreshModels === true. The ~40-line mirror and the serverWouldRefresh transcription are deleted, along with the two client-side command predicates that existed only to feed them.

Where it's decorated, and why that matters. At the route, never in getAllProviders() — the service's object is what saveProviders writes back, so computing it there would land the field in providers.json and let it go stale against the table the first time a user repointed a command. A PUT that echoes the field back is stripped by providerSchema.partial(). PortOS's own sanitized GET handlers in server/routes/providers.js shadow the toolkit's, so they decorate too — and they derive before redaction, because the ollama row keys partly on envVars.ANTHROPIC_BASE_URL, which sanitizeProvider rewrites to *** when the user marked it secret.

Behavior. No shipped provider's Refresh-button visibility changes (frozen catalog walks on both sides pin this). The one behavior that does change: a provider whose command is literally gemini now refreshes as gemini even under a "Claude …" display name — the generalization of the "an exact command beats a name substring" rule the chain had already grown case-by-case for cursor and antigravity.

Test plan

New:

  • server/lib/aiToolkit/internal/modelFetchers.test.js — shipped-catalog walk against the frozen pre-refactor visibility; the ordering the old chains encoded in prose (ollama-before-claude, command-beats-name both ways, "Cursor Notes" never claims cursor); the TUI arm's name-blindness; and an "adding a vendor is exactly one row" test that injects a hypothetical acme row and shows it lighting up the CLI arm, the TUI arm, and the payload flag at once while leaving every shipped provider's answer untouched.
  • server/lib/aiToolkit/routes/providers.refreshCapability.test.js — real service over a temp data dir: GET / carries the flag; it is never written to providers.json (not on create, not on a PUT that echoes it back); a POSTed canRefreshModels: true neither persists nor fakes the answer.
  • server/routes/providers.refreshCapability.test.js — the sanitized PortOS handlers carry it too, and derive it before redaction (a Claude-Ollama provider with a secret ANTHROPIC_BASE_URL still reports true).

Changed:

  • client/src/utils/providers.test.jsserverWouldRefresh deleted; replaced with payload-shape tests (absent field ⇒ no button, so an older server can't produce a 404ing one) plus the shipped-catalog walk retargeted through the server's own decorator, asserting the same frozen id list.

Runs:

  • cd server && NODE_ENV=test npx vitest run → 25603 passed. Three files failed under full-suite load (imageGen.watermark, creativeDirector/recovery, sharing/peerSync) and all pass in isolation, on this branch and with the diff stashed — pre-existing timeout flakes, unrelated to this change.
  • cd client && npx vitest run → 6582 passed; one QuotaBurn flake that passes in isolation.
  • cd client && npm run lint → clean.

Closes #3620

Closes #3616

…to one table and ship it as canRefreshModels

Adding a vendor to model refresh took four coordinated hand edits: the TUI
`else if` chain in `refreshProviderModels`, the CLI `if` chain in
`_refreshCLIProviderModels`, the client's `supportsModelRefresh` mirror, and a
SECOND transcription of the server dispatch inside the client parity test — so
that test only ever proved the mirror matched its own copy.

Both server arms now resolve from `internal/modelFetchers.js`, one row per
vendor. The three keying conventions that had accumulated as prose above each
branch become columns: `cliMatch` (strong — command/structural), `cliNameMatch`
(weak — display-name substring, consulted only after no command claims the
provider), `tuiMatch` (never consults the name). The route decorates every
provider-shaped response with the derived `canRefreshModels`, so the client
shrinks to `p?.canRefreshModels === true` and the hand-written mirror is gone.

Derived on read and decorated at the route, never in `getAllProviders()`, so it
cannot reach `saveProviders` and go stale against the table; a PUT that echoes
it back is stripped by `providerSchema.partial()`. PortOS's own sanitized GET
handlers shadow the toolkit's, so they decorate too — before redaction, since
the ollama row keys partly on `envVars.ANTHROPIC_BASE_URL`.

No shipped provider's button visibility changes (pinned by a frozen catalog
walk on both sides). The one behavior that does: a provider whose command is
literally `gemini` now refreshes as gemini even under a "Claude …" display
name — the same "exact command beats a name substring" rule the chain had
already grown for cursor and antigravity.

Closes #3620
Closes #3616
…nd re-export canRefreshModels from the service shim

The toolkit's `GET /samples` was the one provider-shaped response left
undecorated, while PortOS's shadowing handler already decorated it — so the
two disagreed about the same sample. And `server/services/providers.js`
re-exported `isOllamaBackedProvider` but not its new sibling, leaving hosts
that classify providers through the shim without the capability predicate.
@atomantic
atomantic merged commit b4ed654 into main Aug 7, 2026
6 checks passed
@atomantic
atomantic deleted the claim/issue-3620 branch August 7, 2026 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant