Skip to content

refactor(runtime-host): make the Host the authority for the model catalog - #4411

Merged
Astro-Han merged 44 commits into
apache:mainfrom
Astro-Han:refactor/model-catalog-host-authority
Sep 1, 2026
Merged

refactor(runtime-host): make the Host the authority for the model catalog#4411
Astro-Han merged 44 commits into
apache:mainfrom
Astro-Han:refactor/model-catalog-host-authority

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

A model catalog entry — display name, context window, thinking levels, vision support — was resolved independently by every client, each against the model metadata bundled into its own build. A Desktop and a TUI attached to the same Host could describe the same model differently, and the difference tracked whichever client had been updated more recently rather than anything about the model or the account. That is backwards for a product where the Host holds the connection, runs discovery, and sends the request: it is the only party in a position to say what a model is.

The Host now resolves each connection's catalog and projects it. connection.catalog.query gains a third page item, catalog_entry, one per model, counted by the connection header alongside the model and enabled-id counts. Clients decode what they were sent instead of re-deriving it. resolveConnectionModelCatalog is the single entry point for "what models does this connection have, and what is true about them", so provider rules that shape the list — the Codex subscription's servable set — cannot be applied in one place and forgotten in another. Two client-side resolutions remain, both because no Host-held connection exists to project from, and both documented at the call site and in docs/architecture/runtime-host-architecture.md: the recommended default model shown while adding a connection, and the unsaved editor draft in connection settings.

Making the entry the thing that crosses the wire turns every field into a cost the Host pays to encode and each client pays to decode, so this PR also removes what that cost was buying nothing for. Gone from the catalog and its wire: recommendedRank, capabilitySource, availability, pricing, the model-choice provenance, factOverriddenFields, modelsFetchedAt, and the ConnectionModelInventory taxonomy whose one reader asked a yes/no question. Gone from the provider registry: the PROVIDER_DEFAULTS alias, a protocol field derivable from each entry's Runtime adapter, and three auth actions no admission point ever queried. Storage stops copying the registry's shipped inventory into every new connection, where it froze a second answer at write time next to the one the resolver already computes. Net: 1784 insertions against 2529 deletions across 106 files.

The compatibility epoch moves to 87. An older client would ignore the new items and resolve locally; an older Host sends none, leaving a newer client with an empty catalog. Both are rejected at the handshake rather than left to degrade.

Refs #4398

Verification

Repository-wide build, typecheck and format:check are clean. The desktop renderer architecture ratchet passes against the merge-base commit.

Targeted suites, all passing: core catalog, connections, provider auth, provider catalog contract and connection readiness (62); Runtime Host policy coordinator, catalog reader, bootstrap and connection-effect coordinator (57); storage runtime-policy stores (61); CLI TUI runner, onboarding and task readiness (181); desktop connection catalog fixture, account connection, OAuth and Copilot IPC, add-provider submission and catalog choices (26).

New behavior is covered by tests that fail without it — the catalog_entry page item, its counts, and the catalog reader's reassembly. The removals are guarded by existing assertions updated in the same commits: each names the fact that survives rather than the field that left.

Not run: the repository-wide test suite, Playwright E2E, and Storybook. No user-visible surface changes shape, so no screenshot is attached; the picker and settings copy render the same fields from a different source.

Breaking change

Host and client must agree on compatibility epoch 87. A mismatched pair is refused at the handshake, so an older client cannot silently fall back to resolving the catalog itself.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code (Opus 5) drafted the Host-side projection, the protocol changes, the client decoders, and the removals listed under Summary, and ran the verification above. Every commit with material AI-authored content carries a Generated-by: Claude Code trailer. The human contributor of record reviewed the diff and owns the submission.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

`makeEntry`, `makeMissingDefaultEntry`, and `makeMissingUserChoiceEntry` each carried their own copy of the same twelve-field projection from `ModelMetadata` onto `ModelCatalogEntry`. The two missing-entry builders were identical in 46 of their 48 lines, differing only in how `isDefault` is decided and whether provenance records a user choice. Adding a field to the entry meant editing three copies, and the copies had already begun to drift: only `makeEntry` learned to report `capabilitySource: 'user_override'` for a fact the user overrode.

A missing entry is `makeEntry` over a bare `{ id }` row — with no provider row to merge, every field resolves from the bundled metadata alone, which is exactly what those builders spelled out by hand. What such an entry cannot derive is supplied as explicit overrides: unavailability that belongs to the inventory rather than to the model, a default that is default by construction, and the user-choice provenance flag. The facts shared by every entry in one catalog move into an `EntryContext`, so the builders take what varies instead of repeating seven positional arguments at each of the three call sites.

One behavioral difference: a missing entry now carries `pricing` and `provenance.pricingModelKey` when the pricing table describes its id. The previous builders never consulted that table, so a model the user had selected showed no price while the same id priced normally elsewhere in the same catalog.

Generated-by: Claude Code (claude-opus-5)
…log entry

Rendering one model took three resolutions of the same model's facts: `buildConnectionModelCatalogEntries` for the entry, `thinkingVariantsForConnection` for its reasoning levels, and `resolveModelVisionSupport` for whether it accepts images. Only the last two consulted `relayModelProfiles`, the user's per-model declaration, so on a relay connection the entry's `capabilities.vision` and the choice's `supportsVision` could disagree about one model — the entry said what the catalog knew, the choice said what the user declared.

The declaration is authoritative over every catalog source, so both reads that honour it now resolve where the rest of the model's facts already resolve. `ModelCatalogEntry` gains `thinkingLevels`, its `capabilities.vision` runs through `resolveModelVisionSupport` with the declared value, and `BuildModelCatalogInput` accepts the profiles the connection layer already holds. `buildChatModelChoices` reads both off the entry instead of calling out twice more.

`resolveModelVisionSupport` stays exported: the Host's execution-model composition resolves vision for a model it is about to run, with no catalog entry in hand. Sharing the function keeps one rule with one implementation.

One behavioral difference: an entry's `capabilities.vision` now reports true for a model the provider-and-id heuristic recognizes even when neither the provider row nor the bundled metadata declares vision, matching what the chat model choice already reported for the same model.

Generated-by: Claude Code
A model's facts — display name, context window, capabilities, reasoning levels, lifecycle — come from the models.dev snapshot each build compiles in. Every client merged that snapshot itself: the Host projected the stored connection rows, and Desktop, the TUI, and Chat each ran `buildConnectionModelCatalogEntries` over them against its own copy. Those copies are not the same copy. Desktop installs and updates independently of `npx maka-agent@latest`, and a remote Host is a third version again, so one user attached to one Host through two clients could see one model as offering thinking effort in one and not the other — the symptom reported for GLM-5.3 on a Z.AI plan.

The catalog is now resolved once, where the metadata that resolves it lives. The connection catalog gains a `catalog_entry` item per model, counted by the connection header and paginated like the rows beside it, carrying the entry the Host built. Clients read `connection.catalogEntries`: the chat model menu, the TUI's model choices, the daily-review picker, and the subagent thinking picker no longer look anything up. `ProjectedLlmConnection` names what a client actually holds — the stored connection plus the Host's catalog — and is what crosses the Desktop bridge.

`resolveConnectionModelCatalog` is the one entry point into that resolution, so the provider rules that shape a connection's real model list cannot be applied in one caller and forgotten in another. It absorbs `normalizeOpenAiCodexConnection`, which decides which models a ChatGPT subscription can serve; that function moves next to the catalog it shapes, and the two client-side re-filters of the same set are gone. `buildConnectionModelCatalogEntries` now recognizes a provider through `providerDefaultsOf` rather than indexing the registry, which is the documented single recognition site and the reason a prototype-polluted `providerType` no longer reaches it.

Two client-side resolutions remain, both where the Host has no state to resolve against, and both are commented as such: the add-provider form recommends a default model for a provider that has no connection yet, and the connection editor renders an unsaved draft — model rows just fetched, ids just ticked — that the Host has not been told about.

`ModelChoice.thinkingLevels` becomes required. The TUI used to fall back to its own metadata when a choice carried none, which is exactly the local resolution this change removes; a model no choice describes now offers no levels rather than a guessed list. The only caller that omitted choices was a test embedding.

Compatibility epoch 83: an older client would ignore the new items and keep resolving locally, and an older Host sends none, leaving a newer client with an empty catalog. Both are rejected at the handshake.

Generated-by: Claude Code
`provider-registry.ts` exports `PROVIDER_REGISTRY`; `llm-connections.ts` re-exported it and then bound a second name, `PROVIDER_DEFAULTS`, to the same object. Ninety-odd call sites used one, seven the other, and which one a file imported said nothing about what it did with it — a reader following either name had to discover the alias to know they were looking at the same table.

The alias is gone and its call sites now name the registry. `ProviderDefaults` stays the name of a single entry's type, which is what it describes: the registry is a record of them.

Generated-by: Claude Code
Now that the catalog entry is what crosses the wire, every field on it is something the Host computes, encodes, and a client decodes. Two had no reader at all.

`recommendedRank` numbered a provider's curated fallback models, and nothing in the product ever ordered, filtered, or displayed by it — the pickers sort by their own rules. Its whole derivation goes with it.

`capabilitySource` reported where a model's capabilities came from. Its only readers were three assertions in the catalog tests, each of which already checks the fact the override actually produced — the context window the user declared — so nothing about the covered behavior changes.

Generated-by: Claude Code
`validateChatDefaultModel` re-derived one fact the catalog already states on
every entry: whether a model can serve as the chat default, and if not, why.
Nothing in the product called it — the pickers and readiness gates read
`canUseAsChatDefault` and `unavailableReason` off the entry directly. Its only
callers were assertions in this package's own catalog tests.

Those assertions cover real catalog behaviour (output modalities, stale
inventories, merged partial facts), so they stay: their `verdict` helper now
reads the entry the build produced instead of calling a production function
that existed for it.

Generated-by: Claude Code
…reads

A catalog entry carried a record of where it came from: which user choice
named the model (`connection_default`, `saved_model`, `session_model`,
`daily_review_model`), and whether the provider inventory or the static
catalog described it. `savedModelIds` accepted a `{ id, source }` object so a
caller could say which of those it was.

No caller ever did. The one production producer is this module's own
connection builder, which passes the connection's `enabledModelIds` — plain
strings, every one of them recorded as `saved_model`. Nothing in the product
reads `provenance.sources` back, and the daily-review picker that was the last
plausible `{ id, source }` producer resolves through the Host's catalog now.

So the enum, the object form of `SavedModelChoice`, the sources record, and
the `userChoice` marker all go, along with the codec branch that validated
them over the wire. `savedModelIds` is what it always was in practice: the ids
a catalog must list even when no inventory describes them (apache#1584). That
behaviour is unchanged, and its tests still assert it — they just no longer
assert the label the entry wore while doing it.

Generated-by: Claude Code
…ltsOf

`providerDefaultsOf` documents itself as the sole owner of "is this
providerType one we know", because plain indexing cannot answer it: the
registry is an object literal, so `['__proto__']` and `['toString']` resolve
to inherited members and read as registered providers.

Five accessors still indexed the registry directly. Four guarded with `?.`,
which happens to yield the right answer for an inherited member only because
`Function.prototype` has no `authKind`, `modelDiscovery`, or `baseUrl`; the
fifth, `defaultEnabledModelIdsWhenOmitted`, had no guard at all. All five now
ask `providerDefaultsOf`, so the recognition rule lives in one place instead
of being re-derived, correctly or by luck, at each call site.

Generated-by: Claude Code
…ason

`availability` was `unavailableReason` folded into three buckets, and nothing
but its own codec read it back: `none` became `available`, `stale` and
`not_in_live_list` became `warning`, everything else `blocked`. Two ways of
saying one thing, both now crossing the wire, both needing to stay in step.

The reason itself is the richer of the two and the one the product reads, so
the fold goes and the reason stays. `availabilityOf` carried the explanation
of why a stale or unlisted model still sends (apache#1584); that belongs on
`canUseUnavailableReasonAsDefault`, which is the function actually deciding
it, and has moved there.

Generated-by: Claude Code
@github-actions github-actions Bot added the effort/XL Under 2500 readable lines label Sep 1, 2026
Two conflicts with the base, both resolved by keeping each side's intent.

The compatibility epoch: main claimed 84 for Host-bound directory references
(apache#4097) while this branch had claimed it for the catalog projection. Both are
real wire changes and both must be rejectable at the handshake, so main's 84
stands and this branch's moves to 85.

The renderer architecture ratchet: `ProjectedLlmConnection` and
`HostResolvedConnectionCatalog` were defined in `model-catalog.ts`, which made
six renderer files depend on `@maka/core/model-catalog` where they had not
before — new external-package debt the ratchet refuses. The types describe a
connection, not a catalog computation: a connection is what holds a catalog.
They move to `llm-connections.ts` beside `IdentifiedLlmConnection`, reached
through a type-only import that erases at compile time, and every renderer
file now names them through the `@maka/core/llm-connections` import it already
had. The ledger is regenerated and the check passes against origin/main.

Generated-by: Claude Code
…get pages

The connection header now states how many resolved entries its page carries,
and the reader treats a header that does not say as an invalid projection.
Two hosted-execution suites still built pages without the field, so every
catalog read in them failed before reaching what they actually assert.

Both are about which Host or endpoint a target resolves to rather than about
what the models are, so the honest count for their pages is zero: they send
no entries and now say so.

Generated-by: Claude Code

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one blocking protocol-boundary issue on this exact head. The Host-authoritative catalog migration is otherwise coherent across Core, Runtime Host, CLI, and Desktop.

Validation included a clean install, build:test, full typecheck, Core 777/777, CLI 686/686, focused Host catalog/protocol 13/13, focused CLI/Desktop catalog paths 193/193, lint, format, ASF headers, git diff --check, and a clean merge tree against current main. The full Runtime Host suite retained one unchanged managed-sandbox failure on this Linux container; Desktop retained eight unchanged MCP OAuth timer cancellations. The hosted Windows package job failed because the pinned v0.1.9 release artifact does not exist, while windows_recovery passed and hosted test is still running.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

Comment thread packages/core/src/runtime-policy/connection-catalog-codec.ts

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding approval at head 455a1214c61a32d5863b9ff90a3beda7a4d2c408. The code review is positive, but the PR cannot be approved yet: it is currently a draft, and CI is not green at this headpackage is red and test is still running.

Code review (exact head). No P0/P1 found; the refactor is coherent and the critical surfaces are sound. Verified specifically:

  • Protocol epoch is correct. RUNTIME_HOST_COMPATIBILITY_EPOCH = 85, one above the current live main (84), and the handshake rejects mismatches on both sides — client (packages/runtime-host/src/client/connection.ts ~1421) and server (host-kernel.ts ~530) — so an epoch-85 client and a non-85 host are refused before domain commands, matching the PR's claim (a stale-epoch peer "rejected at the handshake rather than left to degrade").
  • Single authority for model facts. resolveConnectionModelCatalog is the sole entry point for "what models does this connection have, and what is true about them"; the Host applies the Codex servable-set filter and capability/vision/thinking resolution once, and clients decode the projected catalog_entry instead of re-deriving from their own bundled metadata. That removes the desktop-vs-tui disagreement the PR describes.
  • Fail-closed wire read. assembleConnectionCatalog is strict: duplicate/nesting/out-of-range catalog_entry indices, count mismatches against the header (catalogEntryCount/modelCount/enabledModelIdCount), or a connection-total mismatch with first.connectionCount all throw invalid_projection rather than degrading.
  • No secret on the wire. catalog_entry carries model facts (display name, context window, thinking, vision, pricing, provenance) only — no API key / OAuth token / provider secret is projected. Connection auth stays in the credential store / connection entity, not the catalog page.
  • Provider-registry hardening is correct. PROVIDER_DEFAULTS (the alias) is removed and direct registry indexing funnels through providerDefaultsOf, which owns the Object.hasOwn(PROVIDER_REGISTRY, providerType) own-property check, closing the ['__proto__']/['toString'] inherited-key reads.
  • Dead-field removal is genuine (recommendedRank, capabilitySource, availability/unavailableReason fold, model-choice provenance) — the PR body names each and the diff matches (fields removed from the wire and their only readers).

Minor (non-blocking, P3): a handful of call sites still do PROVIDER_REGISTRY[...].authKind directly (e.g. llm-connections.ts, provider-auth.ts) rather than through providerDefaultsOf. These are downstream of validated provider types today, but for consistency with the new own-property discipline a providerDefaultsOf(...)?.authKind form would avoid a possible TypeError if an unregistered string ever reaches one. Not a P0/P1; suggest cleaning up.

Why I'm not approving yet (gate, not code):

  1. The PR is in draft (isDraft=true).
  2. package is red at this exact head: the Windows packaging step fails on gh release download v0.1.9 --repo apache/maka --pattern Maka-0.1.9-win-x64.exerelease not found. This is an external baseline issue (the referenced release asset doesn't exist upstream), not a defect in this PR's diff — but the gate is not green either way.
  3. test is still in_progress.

Recommendation: bring the PR out of draft, resolve the missing v0.1.9 Windows upgrade-baseline release (or the download step), and get test green at the head — then this can be re-reviewed for merge. Happy to re-lock and approve as soon as the gate is green.

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the code at head 455a1214c61a32d5863b9ff90a3beda7a4d2c408. No P0/P1 in the change.

My earlier review stands: resolveConnectionModelCatalog is the single Host-side authority for model facts (Codex servable-set, capability/vision/thinking applied once); the client decodes the projected catalog_entry instead of re-deriving from its own bundled metadata; assembleConnectionCatalog is strict and fail-closed on malformed projections; the wire carries model facts only (no API key / token / provider secret); the provider-registry hardening through providerDefaultsOf closes the inherited-key (__proto__/toString) reads; the protocol epoch is one above live main (85 vs 84) and mismatches are rejected at the handshake on both client and server.

Explicit, posted for the record before any merge: this PR is still a draft, and CI is not green at this headpackage is red (root cause is external infra: the Windows upgrade-baseline step runs gh release download v0.1.9 … Maka-0.1.9-win-x64.exerelease not found, i.e. a missing baseline release asset, not a defect in this diff) and test is still running. This approval reflects the code-review conclusion only; it must not be merged until the PR is brought out of draft, the baseline-release/package issue is resolved, and test goes green at the exact head. A reviewer approval does not record CI as green, and the merge branch protection/MEMBER account still enforces the real gate.

The wire bound on catalog entries per connection was the sum of the two
persisted lists an entry can come from, plus one for an unlisted default. That
missed a third source: a provider with no model-list endpoint has its whole
shipped inventory prepended to the connection's own models rather than
substituted for them, so its catalog is larger than what the connection
stores.

The gap is reachable from a catalog the storage decoder accepts. At its own
maxima — 2,048 stored models, 512 enabled ids — a `volcengine-agent-plan`
connection resolves to 2,578 entries against a bound of 2,561. The Host emits
that page, the operation decoder rejects the Host's own projection as an
invalid catalog entry count, and Desktop and the TUI are left with no model
choices at all.

The bound now includes that third term, derived from the registry rather than
written down beside it: `MAX_PREPENDED_FALLBACK_MODELS` asks each provider
that does not discover models how large its shipped inventory is. A provider
added or a curated list grown moves the bound with it, which a hand-written
number would not have done — that is how this one became too small. The
fallback resolution the constant and the builder share is one function now
instead of two copies of the same three lines.

A regression walks every registered provider at both storage maxima and
asserts the resolved catalog fits, then asserts the bound equals the largest
catalog any provider reaches, so a bound that drifts above what is reachable
fails too.

Generated-by: Claude Code

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving exact head ab572cbd23892703da71c33d9dea9ea240e5baa0 with three non-blocking P2 findings inline. The previous catalog-bound P1 is closed by the latest commit: the wire bound now includes the largest prepended fallback inventory and the all-provider maximum regression passes. I found no remaining P0/P1.

I rebuilt Core and reran its focused catalog suite (21/21) after the head changed. Before that focused range recheck, the same code line passed the complete Core suite (777/777), 250 affected catalog/Desktop/CLI tests, 26 Host execution tests, format, renderer architecture, relevant typechecks, protocol epoch guard, and a clean merge tree against current main. I reran deterministic probes for all three remaining comments on the new head.

The hosted jobs restarted for this head and are still running, and the PR remains a draft, so this approval records the code-review result rather than merge readiness.


Automated review notice: This comment was posted by an automated review agent operated by M4n5ter. It is not an independent human review and does not replace one.

Comment thread apps/desktop/src/renderer/settings/use-connection-detail.ts Outdated
Comment thread apps/desktop/src/renderer/model-catalog-choices.ts Outdated
Comment thread packages/cli/src/runtime-host-tui-context.ts
`deriveProviderAuthContract` opens with a guard for a providerType this build
does not register, and its comment says the guard mirrors `isRealConnection`.
It only mirrors it while it asks the same question the same way, and it did
not: it read `PROVIDER_REGISTRY[providerType]` directly, which answers with an
inherited member for `__proto__`, `toString`, `constructor` and `valueOf`. The
guard saw a truthy value, fell through, and the rest of the function treated
`Object.prototype` as a provider definition — reporting `setupMode: 'api_key'`
for a connection whose provider does not exist.

It asks `providerDefaultsOf` now, the same function `isRealConnection` asks.

Making that change surfaced a second thing the old form had been hiding.
`PROVIDER_REGISTRY[providerType]?.authKind` looks defensive, but the registry
is a `Record`, so indexing it never widens to `undefined` and the optional
chain was inert to the type checker. With a lookup that returns the honest
type, `setupModeForAuthKind` no longer type-checks against an unregistered
provider, and now says what it means: no registration, no setup to offer.

The existing inherited-member regression covers the contract too.

Generated-by: Claude Code
…alog

The status line took its denominator from the connection's stored model
rows. A provider without a model-list endpoint stores none — its models
exist only in the Host-resolved catalog — so the very first status line
and every diagnostic computed from it opened with no context window at
all, until some later transition happened to refresh the value.

Read it from `modelChoices` instead, which is where every later read of
it already comes from.

Generated-by: Claude Code
`normalizeOpenAiCodexConnection` filtered the model inventory but left
`enabledModelIds` alone. A model this subscription cannot serve was
picker-visible once, so a connection saved back then still lists it
there — and the catalog lists an enabled id back even when no inventory
describes it. The id came out the other side selectable, and failed at
the provider once a scheduled run finally sent to it.

Filter the stored selection by the same rule as the inventory, and keep
the identity return when nothing changed so callers still see an
untouched connection.

Generated-by: Claude Code
…unedited

The connection detail sheet rebuilt its catalog from the connection's
stored fields on every render, including before the user has touched
anything. That is the version disagreement the projection exists to end:
a Desktop older or newer than the Host replaced the Host's display names
and eligibility decisions with its own bundled guesses — and could offer
a model the Host had ruled out.

The client-side resolution an editor legitimately needs is narrower: once
the draft diverges — model rows just fetched, ids just ticked — it
describes a connection the Host has never been told about and so cannot
have resolved. `resolveDraftConnectionModelCatalog` makes that the only
branch that resolves, and lives in core beside the resolver it guards.

Divergence compares every stored field an entry is built from, not ids
alone: a re-fetched row can carry different facts under the same id, and
comparing ids would keep showing the Host's stale entry for it.

Generated-by: Claude Code

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-confirming approval at the current head 69094af8d65812df38f947ac5829aa693a615b8f (my earlier approve was on 455a1214, which this head superseded). No P0/P1 — the two commits added since are correctness fixes I reviewed and find sound:

  • ab572cbd — admits the prepended-fallback rows a resolved catalog actually carries. Without it, a provider with no model-list endpoint (e.g. volcengine-agent-plan) prepends its shipped inventory to the connection's stored models, and at the storage maxima (2,048 models / 512 enabled ids) resolves 2,578 entries against a wire bound of 2,561 — the Host emits that page, the decoder rejects its own projection as an invalid count, and clients end up with no choices. The bound is now derived from the registry (MAX_PREPENDED_FALLBACK_MODELS) and shared with the builder, with a regression that walks every provider at both maxima.
  • 69094af8 — routes deriveProviderAuthContract through providerDefaultsOf. It previously read PROVIDER_REGISTRY[providerType] directly, so __proto__/toString/constructor/valueOf resolved to inherited members, the unknown-provider guard saw truthy and fell through, and Object.prototype was treated as a provider (reporting setupMode: 'api_key' for a provider that does not exist). Now it asks the same owner isRealConnection asks, and setupModeForAuthKind honestly yields "no registration, no setup." Inherited-member regression extended to the auth contract.

The rest of my prior review holds (single Host authority for model facts, strict fail-closed catalog assembly, no secret on the wire, epoch 85 = live main+1 with both-sides handshake rejection).

For the record before any merge: the PR is still draft; package is red at this head (external baseline cause: gh release download v0.1.9 … → release not found); test was cancelled (not green), and mergeable is CONFLICTING. This approval is the code-review conclusion only and must not be merged until the PR is un-drafted, the baseline package issue is fixed, test is green at the exact head, and the conflict is resolved. Branch-protection / the MEMBER account still enforce the real merge gate.

…g-host-authority

# Conflicts:
#	packages/runtime-host/src/protocol/index.ts

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-confirming approval at the current head 1edbf85f41 (supersedes my approvals at 455a1214/69094af8; this head rebased onto current main and added three more fixes). No P0/P1 — the three new PR commits (not the main-side merges) are sound:

  • 39a7a511 (cli): the TUI's opening status line/context-window denominator now reads from the resolved catalog (modelChoices) instead of the stored model rows, which are empty for a provider without a model-list endpoint.
  • 696149b2 (core): normalizeOpenAiCodexConnection now also drops unservable Codex ids from the stored enabledModelIds (the inventory was filtered but the selection wasn't, so an unsupported id stayed selectable and failed at the provider); identity-return preserved when nothing changes.
  • 53fc9390 (desktop): the connection detail sheet no longer re-derives the catalog from stored fields on every render; client-side resolution is confined to resolveDraftConnectionModelCatalog, used only once the draft actually diverges — closing the same "desktop-versus-host disagree on a model the Host ruled out" gap the projection exists to prevent.

My overall review stands from the prior heads (single Host authority for model facts; strict fail-closed catalog assembly; epoch 85 vs main 84 with both-sides handshake rejection; no secret on the wire; providerDefaultsOf/__proto__ hardening; the ab572cbd wire-bound fix and 69094af8 auth-contract fix verified).

For the record before any merge: the PR is still draft; package is red at this head (external baseline cause: gh release download v0.1.9 … → release not found); test is still running. This approval is the code-review conclusion only and must not be merged until the PR is un-drafted, the baseline package issue is fixed, and test is green at the exact head. Branch-protection / the MEMBER account still enforce the real merge gate.

The resolved entry crosses the wire and then the desktop IPC boundary, so
every field is paid for on each catalog read by each attached client.
Thirteen of its twenty-two had no reader anywhere: `providerType` and
`connectionSlug` (the connection that owns the entry already holds both),
`source`, `unavailableReason`, `lifecycle`, `docsUrl`, `inputLimit`,
`maxOutputTokens`, `structuredOutput`, `lastUpdated`, `modalities`,
`provenance`, and every capability but vision. They are not needed today;
when a surface asks for one, it comes back with the reader that wants it.

`makeEntry` still consults all of those facts — they just stop being
shipped. `capabilities` becomes the one boolean the only consumer
projected out of it anyway.

Dropping `unavailableReason` collapses what fed it. `canUseAsChatDefault`
was true for `none`, `stale` and `not_in_live_list` alike, so staleness
and live-list absence never changed an answer: `isStale`, its seven-day
window, and the `now`/`staleAfterMs` inputs that only tests supplied are
gone with them. What is left is the two real vetoes — provider retirement
and an explicit "cannot chat" — and `providerRetired` names the one
producer `providerAvailable` ever had. `authOk` had no producer at all,
so its `'auth'` reason was unreachable.

The connection-level input loses the six options nothing passed:
`savedModelIds`, `fallbackModels`, `now`, `staleAfterMs`,
`providerAvailable`, `authOk`. `pricing` and `pricingSource` stay: the
entry's `pricing` seam is documented as the one field kept without a
producer, and cost accounting does not depend on it — a call is priced
from `pricingModelKey` when it is recorded.

Behavior change: an enabled id no inventory describes now runs the same
chat guard as a listed row, so one the bundled metadata knows to be
image-only stops being default-capable. It previously skipped that check
by construction and was offered. apache#1584 is unaffected — absence from a
live list is still not a veto, and a bare id with no metadata is still
selectable.

The draft comparison narrows to the stored fields an entry is actually
built from, and gains `description`, `knowledgeCutoff` and `modalities`
which it should have compared all along; `inputLimit`, `maxOutputTokens`
and `parallelToolCalls` no longer reach an entry, so a change to them no
longer throws the Host's entries away.

Generated-by: Claude Code
A model catalog needs two facts: what a provider ships when Maka is
offline, and what the provider itself lists when it is not. The first had
three writers.

`CURATED_CATALOG_FALLBACK_MODELS` was not a curated variant of
`fallbackModels` — it was the same fact written twice, and the copy that
got updated. For the eight providers it covered, three were byte-identical
and five had moved on, so the registry still shipped `gpt-4o`,
`gemini-2.5-flash`, `deepseek-chat` and `glm-4.6` while the catalog quietly
served `gpt-5.5`, `gemini-3.5-flash`, `deepseek-v4-flash` and `glm-5.2`.
Everything that read the registry directly — CLI onboarding, the
connection-test probe, the storage seed, the transient connection the Host
builds for an unsaved connection — got the stale list; only
`buildConnectionModelCatalogEntries` got the fresh one. The newer content
moves into `fallbackModels` and the second table is gone.

`opencode-free.defaultEnabledModelIds` was the third copy: the same array
variable as its `fallbackModels`, listed again. The fact it carried is
real — the provider is free and keyless, so a new connection should have
every model on rather than none — and it survives as
`enableShippedModelsByDefault`, a flag the derivation reads. The list is
now derived, so seed and hand-added connection cannot disagree about what
"all of them" means, and the desktop add-form stops naming opencode-free.

`providerFallbackModelIds` moves into the registry, becomes exported, and
is the only reader of `fallbackModels` outside tests. Its
`brokenModelIds` subtraction is why it exists: a quarantined id is one a
stored connection may still carry, so it is filtered on read rather than
pruned at the source.

Behavior change: five providers now offer their current models offline
instead of a list up to a year stale, and every surface offers the same
one. The count in the Claude thinking census moves 13 → 16 because
Anthropic's baseline gained Sonnet 4.6, Opus 4.8 and Haiku 4.5.

Generated-by: Claude Code
`providerDefaultsOf` documents itself as the sole owner of "is this
`providerType` one this build registers", and explains why plain indexing
cannot answer it: `PROVIDER_REGISTRY` is an object literal, so a lookup by
an inherited member's name resolves to that member. It lived in
`llm-connections.ts`, which depends on `provider-registry.ts` — so the
registry could not use its own recognition helper, and three other sites
answered the question themselves.

It moves into `provider-registry.ts` (re-exported unchanged) and the
copies go:

- `connections-ipc-validation.ts` gated on `providerType in
  PROVIDER_REGISTRY`. `in` traverses the prototype chain, so `__proto__`,
  `toString`, `constructor` and `hasOwnProperty` passed validation and
  were persisted as connections whose provider the build cannot resolve.
  The renderer reaches this boundary. Covered by a test.
- `modelFactKey` repeated the `Object.hasOwn` check inline.
- `isRetiredProvider` re-implemented `PROVIDER_REGISTRY[t]?.retired`.

Five reads wrote `PROVIDER_REGISTRY[x]?.field`, which reads as a guard and
compiles as none: `Record<ProviderType, ProviderDefaults>` never widens to
`undefined`, so the `?.` is inert and an unregistered name yields an
inherited member instead. They now ask `providerDefaultsOf`. Direct
indexing by an already-`ProviderType` value is untouched — it claims no
guard.

`isKnownProvider` was a private one-line delegate to `providerDefaultsOf`
under `isRealConnection`, which is itself that same question with a name;
readiness now calls `isRealConnection` directly.

`resolveModelPdfSupport` had no reference anywhere, and
`resolveModelInputModalities` existed to serve it — its remaining
assertions duplicate the `metadata.modalities` deepEqual in the same
suite. Both are gone; `ModelInfo.modalities` still decides the image-only
chat veto through `makeEntry`.

Generated-by: Claude Code
@Astro-Han
Astro-Han marked this pull request as ready for review September 1, 2026 05:36
`ModelChoice.connectionId` was optional, and the one thing that consumes
a choice — the cross-connection `/model` rebind — cannot work without it,
so it threw on the absent case. The only producer,
`projectRuntimeHostModelChoices`, reads it straight off the Host's
catalog connection and has always set it.

Make the field required. The rebind's throw goes, and the label
disambiguator stops carrying a slug fallback for an id it now always has.

Generated-by: Claude Code
`modelsFetchedAt` — when the Host last ran model discovery — was encoded
on every connection header, decoded with a paired-presence rule against
`modelSource`, and projected by both clients into `LlmConnection`. No
surface reads it: nothing in the renderer, the TUI, or `packages/ui`
shows a discovery timestamp. The Host keeps it in storage, where
`hasModelDiscoveryChanged` compares it to decide whether a run changed
anything; that reader is unaffected.

Drop it from the header (and with it the paired-presence check and the
client-side `LlmConnection` field), and drop the `fetchedAt` the desktop
`connections:fetchModels` reply echoed for the same reason — its caller
reads `models` and `source` only.

`lastTestModelFactsFingerprint` joins the same `Omit`. The projection has
always destructured it away as durable invalidation metadata, so the type
claimed a field the wire never carried.

Generated-by: Claude Code
Every catalog page model carried `factOverriddenFields` — which fields a
user's `model-facts.json` had overridden — and the protocol grew a whole
second decoder for it: a duplicate copy of the model field list, a
separate list of overridable fields, and membership and duplicate checks,
all to admit a marker into a shape `decodeConnectionModel` otherwise
rejects.

No client reads it. The one reader of that provenance is the Host's own
context-budget policy, which asks whether a context window was hand-set
and reads it off the execution connection — a path this page is not on.

The overrides themselves still reach clients: the page carries the merged
values, which is what a picker renders. Its test now asserts that
directly instead of asserting the marker travelled.

Generated-by: Claude Code
`PROVIDER_AUTH_ACTIONS` listed six operations; the storage coordinator
gates three — `test_credentials`, `fetch_models` and `start_oauth`. The
other three arrived with `af724fbaea PR-AUTH-0: account auth contract
mock UI` in May, as the contract half of a settings surface that never
landed: no PR-AUTH-1 followed, the connection panel offers no revoke or
refresh (deleting the connection is what clears a credential), and the
`copy` field those actions were shaped for went in the previous trim
for want of a reader.

With `save_secret` and `revoke_auth` gone, `none` and `optional_api_key`
decide the same two answers and collapse into the branch for every
provider reachable without a key.

State on the constant what earns an entry, so the next operation that
sounds like it belongs has to name its admission point first.

Generated-by: Claude Code
…ies it

`ConnectionCatalogPageItem` is the type every projection of the model
catalog crosses, and where both duplicate authorities this branch removed
were introduced: `modelsFetchedAt` and `factOverriddenFields` were each
added as a field here, shipped, and never read.

Say on the type what the Host owns and what a client may not re-derive,
and give a field the rule it has to pass — name a client-side reader, or
leave it off. A separate architecture document would say the same thing
where nobody adding a field is looking.

The preceding commit carried a longer first draft of this comment without
describing it in its message.

Generated-by: Claude Code
The comment on ConnectionCatalogPageItem explained itself at length. Two
sentences carry the rule: the Host owns the catalog, and a field only goes
on this type if a client shows it.

Generated-by: Claude Code
ASF source files carry the license header and leave authorship to git
history; no other file in this repository names an author in a comment.

Generated-by: Claude Code
The add-provider form computed `defaultEnabledModelIdsWhenOmitted` and
sent the result as `enabledModelIds`. The `connections:create` handler
never reads that field: it calls the same registry helper itself and
builds the list from `defaultModel` plus the provider's shipped baseline.

The registry answered the same question twice for one create, once on
each side of the IPC boundary. Only the main-process answer was ever
used, so the renderer's copy goes.

Generated-by: Claude Code

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewing at the current head 3d524e9aec (head has converged further since my prior approvals; requested re-review). No P0/P1 — approving.

Since my last review the PR added more single-authority convergence (plus three doc/comment commits that state the catalog authority on the seams that carry it):

  • fcb8e58e — the auth contract keeps only the actions something actually admits (none/optional_api_key collapse onto the no-key branch; dead save_secret/revoke_auth gone), with the admission rule documented in the runtime-policy protocol.
  • 3d524e9a — the add-provider form stops computing and sending enabledModelIds that the connections:create handler never reads (it recomputes the list from defaultModel + shipped baseline on the main-process side); removes the redundant IPC-side copy.
  • The ongoing reachability/convergence holds: removed symbols relocated or dead, wire kept down to what a reader consumes, no residual orphan imports, no secret on the wire, epoch/handshake discipline unchanged.

CI is re-running at this head (package in_progress, windows_recovery green) — the PR is out of draft, MERGEABLE, review decision APPROVED. This approval is the code-review conclusion at this exact head. It should merge once CI is green at the exact head (the package baseline gate was fixed in-repo via the v0.2.0-dev.11.20260831 repin).

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed this at 3d524e9aeced1dcb46992583b0339a91e05cf9b0. No P0, P1, P2, or P3.

The Host now resolves each connection's catalog and projects a catalog_entry item per model. Clients decode those entries and stop merging stored rows against their own bundled metadata. The wire decoder is exact on the item and on the entry; a missing, duplicate, or short page fails closed. Compatibility epoch is 87 against current main 86, which is the right bump: an older client would ignore the new items and keep resolving locally, and an older Host would send none.

The two remaining client-side resolutions are the ones that have no Host-held connection: the recommended default while adding a provider, and an editor draft that has actually diverged from the committed connection. Unedited detail still reads connection.catalogEntries. Codex now drops unservable ids from enabledModelIds as well as from the inventory. The catalog-entry bound includes the registry-derived prepended fallback list, so a stored catalog at its own maxima still encodes. The latest commit stops the add-provider form sending an enabledModelIds list the create handler never read; the main process already builds that list from the registry.

Hosted package was still in progress on this head when I posted. This is a refactor; I am not merging it.

简体中文

我按 3d524e9aeced1dcb46992583b0339a91e05cf9b0 重审。没有 P0/P1/P2/P3。

Host 解析每个连接的目录,并按模型投影 catalog_entry。客户端解码这些条目,不再用自己打包的元数据去拼。条目和页都是严格解码,缺、重、少都会失败。兼容 epoch 是 87,当前 main 是 86,加一档是对的。

客户端还自己解析的两处都没有 Host 连接:添加供应商时的推荐默认模型,以及已经和已提交连接分叉的编辑草稿。未改的详情页仍读 Host 的 catalogEntries。Codex 会从 enabledModelIds 里拿掉不能服务的 id。条目上限含了 registry 推导的 prepended fallback。最新提交不再让添加表单发送 create 根本不读的 enabledModelIds

发这条时 hosted package 还在跑。这是重构,我不合入。


Automated review notice: This comment was posted by an automated review agent operated by WAWQAQ. It is not an independent human review and does not replace one.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed exact head 3d524e9aeced1dcb46992583b0339a91e05cf9b0 and found no P0-P3 issues.

The previous catalog-bound finding is fixed: the protocol limit now includes the largest registry-derived prepended fallback inventory, and the maximum-boundary regression exercises every provider. The completed refactor keeps resolved model facts Host-owned, strictly validates paginated catalog_entry projections, preserves client-side resolution only for genuinely unsaved drafts, filters unsupported Codex selections, and removes the renderer's unused enabledModelIds create payload. Compatibility epoch 87 correctly follows current main's epoch 86.

Validation on Node 22.22.1 included a clean install, build:test, full typecheck, Core 748/748, CLI 691/691, focused current-main synthetic-merge Host 33/33 and CLI 217/217 tests, lint, format, ASF headers, renderer architecture, git diff --check, and a clean merge tree. Full Desktop completed 1826 tests with eight unchanged MCP OAuth timer cancellations; full Runtime Host completed 1541 tests with one unchanged live-sandbox environment failure and 12 skips. The same live-sandbox failure reproduced on the current-main synthetic merge.

At publication, windows_recovery is green, while hosted package is still running and plan is queued. I did not run real provider requests or platform-specific macOS/Windows packaging locally, so this code-review result does not replace those exact-head gates or an independent human merge decision.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed exact head 3d524e9aeced1dcb46992583b0339a91e05cf9b0 and found no P0-P3 issues.

The three previously reported paths are closed. An unchanged Desktop connection editor preserves the Host's exact catalogEntries; unsupported legacy Codex ids are removed from enabled selections before Daily Review and readiness; and the TUI's opening context window comes from the exact Host-resolved model choice. The later cleanup continues the same design: committed catalog facts have one Host authority, pagination remains strict and fail-closed, the two remaining client-side resolutions cover only pre-connection creation and genuinely diverged drafts, and epoch 87 fences the incompatible wire change from main's epoch 86.

Validation included Core 771/771, Storage 1,073 pass / 10 platform skips, Runtime Host 1,542 pass / 12 platform skips, 337/337 affected tests, the exact Desktop component regression 4/4, and renderer architecture 60/60. Core, Storage, Runtime, Runtime Host, and CLI builds passed, as did changed-source formatting and git diff --check. All four review threads are resolved, and the current-main merge tree is clean. Hosted package and windows_recovery are green; the ordinary CI plan job is still queued, so this approval records the current-head code-review result and does not assert merge readiness.


Automated review notice: This comment was posted by an automated review agent operated by M4n5ter. It is not an independent human review and does not replace one.

@Astro-Han
Astro-Han merged commit 49858c4 into apache:main Sep 1, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the refactor/model-catalog-host-authority branch September 1, 2026 09:36
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
…e feature branch

Resolve the settings-surface.tsx conflict: keep the feature rework's
SettingsPageBody shape (runtimeHostEpoch prop; usageStats prop dropped since the
Usage feature now loads its own stats) and adopt apache#4411's connection type rename
IdentifiedLlmConnection[] -> ProjectedLlmConnection[] (dropping the now-unused
IdentifiedLlmConnection import). Regenerate renderer-architecture.json and the
astryx surface inventory.
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 1, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 90 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…alog (apache#4411)

Every client resolved a model catalog entry — display name, context window, thinking levels, vision support — on its own, against the model metadata bundled into its build. A Desktop and a TUI attached to the same Host could describe the same model differently, and the difference tracked whichever client had been updated more recently rather than anything about the model or the account.

The Host now resolves each connection's catalog and projects it. `connection.catalog.query` gains a `catalog_entry` page item, one per model, counted by the connection header alongside the model and enabled-id counts; clients decode what they were sent. `resolveConnectionModelCatalog` is the single entry point for what a connection's models are and what is true about them, so provider rules that shape the list — the Codex subscription's servable set — cannot be applied in one place and forgotten in another. Two client-side resolutions remain, both because no Host-held connection exists to project from: the recommended default model shown while adding a connection, and the unsaved editor draft in connection settings.

Making the entry the thing that crosses the wire turns every field into a cost the Host pays to encode and each client pays to decode, so this change also removes what that cost bought nothing for. Gone from the catalog and its wire: `recommendedRank`, `capabilitySource`, `availability`, `pricing`, the model-choice provenance, `factOverriddenFields`, `modelsFetchedAt`, and the `ConnectionModelInventory` taxonomy whose one reader asked a yes/no question. Gone from the provider registry: the `PROVIDER_DEFAULTS` alias, a `protocol` field derivable from each entry's Runtime adapter, and three auth actions no admission point queried. Storage stops copying the registry's shipped inventory into every new connection, where it froze a second answer at write time beside the one the resolver already computes. Net: 1784 insertions against 2529 deletions across 106 files.

Compatibility impact: the epoch moves to 87. An older client would ignore the new page items and resolve locally; an older Host sends none, leaving a newer client with an empty catalog. Both pairings are refused at the handshake rather than left to degrade. No stored document changes shape, so no data migration is required.

Refs apache#4398

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 93 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 93 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 94 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 94 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

Editing a connection keeps that guarantee. When the draft diverges from
storage — rows just fetched, ids just ticked — `resolveDraftConnectionModelCatalog`
must resolve locally, so it preserves the Host's `describedByMetadata` for every
id the Host already described and consults the bundled table only for an id the
Host never saw. Otherwise the local rebuild would flip a refreshed model back to
uncovered and bring the row this field removes straight back.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 94 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

Editing a connection keeps that guarantee. When the draft diverges from
storage — rows just fetched, ids just ticked — `resolveDraftConnectionModelCatalog`
must resolve locally, so it preserves the Host's `describedByMetadata` for every
id the Host already described and consults the bundled table only for an id the
Host never saw. Otherwise the local rebuild would flip a refreshed model back to
uncovered and bring the row this field removes straight back.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 95 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

Editing a connection keeps that guarantee. When the draft diverges from
storage — rows just fetched, ids just ticked — `resolveDraftConnectionModelCatalog`
must resolve locally, so it preserves the Host's `describedByMetadata` for every
id the Host already described and consults the bundled table only for an id the
Host never saw. Otherwise the local rebuild would flip a refreshed model back to
uncovered and bring the row this field removes straight back.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 95 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

Editing a connection keeps that guarantee. When the draft diverges from
storage — rows just fetched, ids just ticked — `resolveDraftConnectionModelCatalog`
must resolve locally, so it preserves the Host's `describedByMetadata` for every
id the Host already described and consults the bundled table only for an id the
Host never saw. Otherwise the local rebuild would flip a refreshed model back to
uncovered and bring the row this field removes straight back.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
liuxiaocs7 added a commit to liuxiaocs7/maka that referenced this pull request Sep 2, 2026
apache#4496)

The connection detail asked `hasModelMetadata()` — a question about the
renderer's own bundled models.dev snapshot — to decide whether an enabled
model needs a hand-written capability declaration. Since apache#4411 clients read
Host-resolved `catalogEntries` rather than resolve a catalog themselves, and
since apache#4467 the Host refreshes that catalog at startup; so a model the Host
learned about after this build was cut is described everywhere except this one
renderer, which still showed it a spurious capability-declaration row.

The Host already owns the answer. `ModelCatalogEntry` now carries
`describedByMetadata`, set by `makeEntry` from the same metadata lookup
`hasModelMetadata` reads, and the renderer asks the entry it already has
instead of its stale table. Inferring coverage from whether some optional
field happens to be present would put a second, weaker copy of the rule in the
renderer — the split this line of work exists to close — so the entry states
it directly.

That makes it a wire field: it is required on `ModelCatalogEntry`, decoded by
`model-catalog-entry-codec`, and the compatibility epoch moves to 95 so a Host
and client that disagree about its presence are refused at the handshake. The
`apache#1584` case (a user-typed id no inventory describes) still reports false, so
that model keeps its declaration row.

Editing a connection keeps that guarantee. When the draft diverges from
storage — rows just fetched, ids just ticked — `resolveDraftConnectionModelCatalog`
must resolve locally, so it preserves the Host's `describedByMetadata` for every
id the Host already described and consults the bundled table only for an id the
Host never saw. Otherwise the local rebuild would flip a refreshed model back to
uncovered and bring the row this field removes straight back.

`provider-endpoint-presentation.ts` also reads the bundled table, but only to
look up build-time `generatedModelProviderOverrides` a Host refresh never
installs, so it stays consistent and is left alone.

Fixes apache#4496

Generated-by: Claude Code
Astro-Han pushed a commit that referenced this pull request Sep 2, 2026
#4500)

Settings showed a spurious "declare this model's capabilities by hand" row for any model the Runtime Host already describes. The renderer answered "does this model have metadata" from its own bundled models.dev snapshot, which never refreshes, while since #4411 and #4467 the Host resolves and refreshes the catalog itself, so the two disagreed for every model listed after the build was cut.

The Host is the authority, so `ModelCatalogEntry` now carries `describedByMetadata`, set by `makeEntry` from the same lookup `hasModelMetadata` reads, and `provider-connection-detail.tsx` reads that field off the `modelChoices` it already holds instead of consulting the bundled table. A user-typed id no inventory describes still reports false and keeps its declaration row. No protocol epoch change: the field is additive on a Host-produced entry.

Fixes #4496

Generated-by: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Under 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants