fix(desktop): read model metadata coverage from the Host catalog entry - #4500
Conversation
f6a2669 to
006f248
Compare
jackwener
left a comment
There was a problem hiding this comment.
[P2] Preserve the Host's metadata-coverage decision when the connection draft diverges (packages/core/src/model-catalog.ts:351-358).
The new field fixes the initially rendered, committed connection, but resolveDraftConnectionModelCatalog returns the Host entries only while the draft exactly matches storage. Ticking any model id or accepting a fetched list rebuilds every entry in the renderer process, whose bundled metadata is the stale authority this PR is removing. I reproduced this on the exact head by installing metadata for a synthetic future model, building the Host entry (describedByMetadata: true), resetting to the bundled table to represent the renderer, and then resolving the same connection after adding another enabled id. The unchanged draft retained true; the edited draft changed that same model to false, so the filter at provider-connection-detail.tsx:219-223 shows the spurious capability row again as soon as the user edits the model list. Please preserve the Host-projected value for ids already present in connection.catalogEntries, and use local resolution only for ids the Host has never described; add a refreshed-only model plus divergent-draft regression.
[P2] Update the Compatibility section to the epoch transition this head actually makes. Current main is epoch 93 and this branch is epoch 94, with both ledger entries present; the PR body still says 89 → 90. The required-field compatibility explanation is otherwise correct.
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.
8442d6e to
72ac2dc
Compare
|
Thanks for the careful review — both points are addressed in the current head ( [P2] Preserve the Host's metadata-coverage decision when the draft diverges. Fixed. Added a regression test in [P2] Compatibility section. Fixed in the PR body to state the transition this head actually makes, with both ledger entries present. It now reads |
Astro-Han
left a comment
There was a problem hiding this comment.
Approving 72ac2dcb; one small behaviour fix below to take before you press merge. The diagnosis holds and the shape is right. installRefreshedModelMetadata only swaps a module-level table in the Host process, the renderer's activeMetadata() is the build-time snapshot forever, so "described by metadata" has to be settled on the Host and travel with the entry. The epoch is genuinely needed (exactRecord closes the key set, so even an optional field breaks old peers), the ledger line is right, protocol-epoch-check passes against main, and the architecture snapshot loses a dependency edge rather than gaining an exemption. jackwener's two points are both closed on this head. Core 768/768, desktop typecheck, lint and the renderer check all green locally.
The fix to take before merge. The old condition asked hasModelMetadata(providerType, modelId) by id; the new one at provider-connection-detail.tsx:223 asks entryById.get(modelId)?.describedByMetadata !== true, so a missing entry now reads as "no metadata". The catalog deliberately omits quarantined ids (OPENCODE_FREE_BROKEN_MODEL_IDS and the Codex normaliser), while enabledModelIds comes from connectionEnabledModelIds unfiltered and the enabled-model manager still lists such an id so the user can untick it. Reproduced on dist with opencode-free + x-preview-f-free: no entry, hasModelMetadata true, so main shows no declaration row and this branch shows one. Same class of bug the PR fixes, reachable from persisted state the catalog code itself documents. Treat absence and "described: false" differently:
const entry = entryById.get(modelId);
... || (entry !== undefined && !entry.describedByMetadata)Hand-typed ids always have an entry via savedModelIds, so #1584 is unaffected.
Smaller, same round if you like: after this change hasModelMetadata has no production caller left, and makeEntry at model-catalog.ts:502 re-implements its sentinel by hand; either call it from makeEntry or delete it and let the tests assert the entry. The draft-divergence branch at :356-372 now has a second producer for the field patched by hostCoverage; fine as the minimal fix for jackwener's point, but a one-line comment that this is the sync point would help the next field. And the expression that actually changed has no test of its own (no story or E2E on this page), which is how the case above slips through CI.
Merge-order note, not a defect: five open PRs claim 95 (#4386, #4308, #4439, #4508 and this one); whichever lands second rebases to 96 and reorders the ledger.
Evidence boundary: static read of 72ac2dcb against main 92fa5281 and 72eb982d; core test:dist, desktop typecheck, Biome, renderer architecture check and the epoch guard run locally; quarantine case reproduced on the built dist; no live settings page exercised.
AI-assisted review: drafted with Maka; I verified the quarantine reproduction, the hasModelMetadata residue and the epoch result myself.
简体中文
批准合并,合并前顺手修一处。诊断成立、形状对:元数据刷新只换 Host 进程的表,renderer 永远是构建期快照,所以「是否被元数据描述」必须在 Host 结算随 entry 过线;epoch 确实需要,ledger 和 guard 都对;jackwener 两条已关。合并前一处行为改动:旧条件按 id 问 hasModelMetadata,新条件把 entry 缺失当成「没有元数据」,而目录会主动剔除隔离 id、enabledModelIds 又不过滤,dist 上复现了一个被隔离但仍 enabled 的 id 在本分支长出声明行。把缺失和 described: false 分开即可。顺手:hasModelMetadata 已无生产调用者且 makeEntry 手抄了它的哨兵,二选一。epoch 95 有五个 PR 抢号,后合的改 96。
72ac2dc to
da1d8f9
Compare
|
Thanks — the pre-merge fix is in ( Absence vs. const entry = entryById.get(modelId);
return entry !== undefined && !entry.describedByMetadata;A missing entry — a quarantined id the provider registry drops from the catalog ( Sync point. Added a comment at the On the two smaller items — happy to fold either in this round, tell me which you'd prefer:
Epoch / merge order. Noted — this head is 95 now (main reached 94 since your review). Whichever 95-claimant lands first keeps it; I'll re-resolve to |
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
da1d8f9 to
f06147c
Compare
Summary
Settings → a provider connection showed a spurious "declare this model's
capabilities by hand" row for any model the Runtime Host already describes —
one models.dev started listing since this Maka build was cut.
The renderer decided whether a model needs a hand-written declaration by asking
hasModelMetadata(), a question about its own bundled models.dev snapshot.Since #4411 clients read Host-resolved
catalogEntriesinstead of resolving acatalog themselves, and since #4467 the Host refreshes that catalog at startup.
So the Host can describe a newly-listed model while this one renderer — a
different process, often on a different machine, whose snapshot never refreshes —
cannot, and it fell back to asking the user.
The Host owns the answer, so the entry now states it directly:
ModelCatalogEntrycarriesdescribedByMetadata, set bymakeEntryfrom thesame metadata lookup
hasModelMetadatareads.provider-connection-detail.tsxreads that field off themodelChoicesitalready has, instead of consulting its bundled table.
Inferring coverage from whether some optional field (e.g.
contextWindow)happens to be present would have put a second, weaker copy of the rule in the
renderer — exactly the split this line of work exists to close — so the entry
answers directly rather than being re-derived.
The
#1584case (a user-typed id no inventory describes) still reportsfalse,so such a model keeps its declaration row.
provider-endpoint-presentation.tsreads the bundled table too, but only for build-time
generatedModelProviderOverridesa Host refresh never installs, so it staysconsistent and is left untouched (as the issue asks).
Fixes #4496
Verification
packages/core— new test inmodel-catalog.test.ts: a known model reportsdescribedByMetadata: true, a bare id reportsfalse(both matchinghasModelMetadata), the field survives a wire round-trip, and an entrylacking it is rejected by the decoder.
node --test: 20/20.runtime-policy-codec13/13,runtime-policy-coordinator17/17.
npm run buildacross all packages (incl. the desktop renderer) ✓.biome format/biome linton the changed files ✓.apps/desktop/renderer-architecture.json(--write); the "Checkrenderer architecture" snapshot passes.
Cosmetic and recoverable behavior: the fix removes one spurious form row on a
normal user path; no persisted state was affected.
Compatibility
describedByMetadatais required on the wire and decoded inmodel-catalog-entry-codec, soRUNTIME_HOST_COMPATIBILITY_EPOCHmoves94 → 95. A Host and client that disagree about the field's presence arerefused at the handshake rather than dropping it silently. No stored document
changes shape — no data migration.
AI use
Select exactly one:
Tool(s) and scope: Claude Code authored the change (core wire field, epoch bump,
renderer edit, and tests) under human direction. The commit retains a
Generated-by: Claude Codetrailer.Checklist
Does this PR entail a change in behavior?