Skip to content

v0.1.25 — provider/catalog/task-routing three-layer split - #66

Merged
TsekaLuk merged 5 commits into
mainfrom
release/v0.1.25
Aug 20, 2026
Merged

v0.1.25 — provider/catalog/task-routing three-layer split#66
TsekaLuk merged 5 commits into
mainfrom
release/v0.1.25

Conversation

@TsekaLuk

Copy link
Copy Markdown
Contributor

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, 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.

Separately, 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.

What

Configuration is now three layers that cannot be collapsed:

Layer Owns Persisted in
1 Connection (ProviderConfig) endpoint + protocol + credential reference providers.json
2 Catalog (ProviderConfig.catalog) the models that connection advertises providers.json
3 Routing (CapabilityBindings) which connection + model serves each task settings.json
  • 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, so clearing web storage 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 AI 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 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. Fixed, with a regression test.
  • commerce-operator.test.ts matched 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 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. 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

TsekaLuk and others added 3 commits August 20, 2026 19:34
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>
Comment thread scripts/commerce-operator.test.ts Fixed
TsekaLuk and others added 2 commits August 20, 2026 20:30
- 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>
@TsekaLuk
TsekaLuk merged commit f531764 into main Aug 20, 2026
23 of 25 checks passed
@TsekaLuk
TsekaLuk deleted the release/v0.1.25 branch August 20, 2026 14:15
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