v0.1.25 — provider/catalog/task-routing three-layer split - #66
Merged
Conversation
A provider used to carry a required `defaultModel`, which made a connection and a model choice the same thing. That single field was a second, hidden routing authority: automatic setup preferred it, `resolveModel` fell back to it, and when it was empty the call fell further back to a hardcoded per-kind table — reaching a model the user never configured. It is also why a DashScope connection had to be named "Qwen Image 3": the label was the only place left to say what the connection was. Worse, Settings offered six task slots while the runtime consumed a two-slot `chat`/`image` projection. A binding for `webdev` was dead whenever `text` was set; `image-edit` was dead whenever `image-generation` was set. The UI promised per-task routing the runtime never honoured. The configuration is now three layers that cannot be collapsed: 1. Connection `ProviderConfig` endpoint + protocol + credential ref 2. Catalog `ProviderConfig.catalog` the models that connection advertises 3. Routing `CapabilityBindings` which connection + model per task - `defaultModel` is optional and deprecated: parsed so pre-v0.1.25 `providers.json` still loads and so the one-time binding migration can seed a `text` route from it, and consulted by nothing else. `resolveModel` has no fallback table — an unbound call fails with `NO_MODEL_BOUND`. - The probed catalog moves from a localStorage verification receipt onto the connection in `providers.json`, so a browser-storage wipe no longer strips a provider of its models. Verifying a provider and refreshing its catalog are one action (`verifyProviderCatalog`) with one writer. - `task-routing.ts` resolves every generation call site by task, with an explicit inheritance chain. `taskFallbackChain` is shared with the coverage summary so the two cannot drift. - `useEndpointModels`/`listEndpointModels` are removed: gated on `provider.baseUrl`, they were permanently disabled for every direct vendor connection, which is why the model pickers were always empty. - Settings reads as two steps — connect providers, then assign models to tasks. A task row shows `provider · model`, never a bare slug, and says which task an unassigned route inherits from. Found and fixed while auditing: `materialize()` rebuilt `ProviderConfig` field by field and dropped `catalog`, so any edit — even a rename — erased the model list it had just probed. Rust gains `iso_timestamp_from_epoch_secs` (no date crate in the tree) so the native layer can stamp a catalog atomically on import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Split provider, catalog and task routing into three layers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump the display, Cargo, Tauri, capability manifest and Codex plugin versions to 0.1.25 and add the reviewed release-note catalog entry for it in all five shipped locales. Two version assertions that a bump cannot reach are made self-deriving instead of re-pinned: - `commerce-operator.test.ts` matched the Cargo version through an escaped regex (`0\.1\.24`), which a search for the literal version never finds — it would have kept passing against the previous release. It now derives the pattern from `package.json`. - `updater/release-notes.test.ts` used the *next* version as its "not in the catalog" case, so it broke the moment that version shipped. It now uses a synthetic version far above anything shippable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Prune obsolete message-catalog entries with `lingui extract --clean`. The ids removed with the old Settings copy stayed behind as commented-out entries, and `i18n:ci` compares against a cleaned extract. - Compare the Cargo version as an exact line instead of a constructed regex. Escaping only `.` left backslashes unescaped, which CodeQL flags as incomplete sanitization; the assertion never needed a regex. - Point the release-notes browser contract at the v0.1.25 entry. Only the version string moved with the bump, so it still asserted v0.1.24 content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The exact-line comparison split on `\n` alone, so a CRLF checkout left a carriage return on every line and the assertion could never match. Splitting on `\r?\n` matches under both line endings; verified against an LF and a CRLF copy of the same file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Why
A provider carried a required
defaultModel, which made a connection and a model choice the same thing. That single field was a second, hidden routing authority: automatic setup preferred it,resolveModelfell back to it, and when it was empty the call fell further back to a hardcoded per-kind table — reaching a model the user never configured. It is also why a DashScope connection had to be named "Qwen Image 3": the label was the only place left to say what the connection was.Separately, Settings offered six task slots while the runtime consumed a two-slot
chat/imageprojection. A binding forwebdevwas dead whenevertextwas set;image-editwas dead wheneverimage-generationwas set. The UI promised per-task routing the runtime never honoured.What
Configuration is now three layers that cannot be collapsed:
ProviderConfig)providers.jsonProviderConfig.catalog)providers.jsonCapabilityBindings)settings.jsondefaultModelis optional and deprecated: parsed so pre-v0.1.25providers.jsonstill loads and so the one-time binding migration can seed atextroute from it, and consulted by nothing else.resolveModelhas no fallback table — an unbound call fails withNO_MODEL_BOUND.verifyProviderCatalog) with one writer.task-routing.tsresolves every generation call site by task, with an explicit inheritance chain.taskFallbackChainis shared with the coverage summary so the two cannot drift.useEndpointModels/listEndpointModelsare removed: gated onprovider.baseUrl, they were permanently disabled for every direct vendor connection, which is why the model pickers were always empty.provider · model, never a bare slug, and says which task an unassigned route inherits from.Found while auditing
materialize()rebuiltProviderConfigfield by field and droppedcatalog, so any edit — even a rename — erased the model list it had just probed. Fixed, with a regression test.commerce-operator.test.tsmatched the Cargo version through an escaped regex (0\.1\.24) that a version-bump search never finds; it would have kept passing against the previous release. Now derived frompackage.json.updater/release-notes.test.tsused the next version as its "not in the catalog" case, so it broke the moment that version shipped. Now a synthetic version.Release
Prepares v0.1.25: version bumped across display, Cargo, Tauri, capability manifest and Codex plugin, with a reviewed release-note catalog entry in all five shipped locales.
Verification
pnpm lint·tsc --noEmit -p tsconfig.app.json· 463 frontend suites · 322 Rust tests ·pnpm build·release-notes:validate --require-all-locales·validate-release-version --expected 0.1.25— all pass.Not yet done: the app has not been launched manually. This changes the persisted provider shape and every generation call site, so a real run is worth doing before the tag.
🤖 Generated with Claude Code