Collapse the model-refresh vendor dispatch into one table and surface it as canRefreshModels - #3624
Merged
Conversation
…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
force-pushed
the
claim/issue-3620
branch
from
August 7, 2026 23:09
930a52b to
1150df2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adding one vendor to the model-refresh feature took four coordinated hand edits — the TUI
else ifchain inrefreshProviderModels, the CLIifchain in_refreshCLIProviderModels, the client'ssupportsModelRefreshmirror, 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
geminiprovider configured by absolute path was in the second bucket (#3616).Server: one table. New
server/lib/aiToolkit/internal/modelFetchers.jsholds 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:cliMatchollamaBacked)cliNameMatchtuiMatchfetchis 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.
canRefreshModelsis exported as a pure predicate and decorated onto every provider-shaped response;supportsModelRefreshshrinks to(p) => p?.canRefreshModels === true. The ~40-line mirror and theserverWouldRefreshtranscription 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 whatsaveProviderswrites back, so computing it there would land the field inproviders.jsonand let it go stale against the table the first time a user repointed a command. A PUT that echoes the field back is stripped byproviderSchema.partial(). PortOS's own sanitized GET handlers inserver/routes/providers.jsshadow the toolkit's, so they decorate too — and they derive before redaction, because the ollama row keys partly onenvVars.ANTHROPIC_BASE_URL, whichsanitizeProviderrewrites 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
gemininow 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 hypotheticalacmerow 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 toproviders.json(not on create, not on a PUT that echoes it back); a POSTedcanRefreshModels: trueneither 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 secretANTHROPIC_BASE_URLstill reportstrue).Changed:
client/src/utils/providers.test.js—serverWouldRefreshdeleted; 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; oneQuotaBurnflake that passes in isolation.cd client && npm run lint→ clean.Closes #3620
Closes #3616