Skip to content

fix(desktop): read model metadata coverage from the Host catalog entry - #4500

Merged
Astro-Han merged 1 commit into
apache:mainfrom
liuxiaocs7:liuxiaocs7/desktop-connection-detail-asks-for-capability-de
Sep 2, 2026
Merged

fix(desktop): read model metadata coverage from the Host catalog entry#4500
Astro-Han merged 1 commit into
apache:mainfrom
liuxiaocs7:liuxiaocs7/desktop-connection-detail-asks-for-capability-de

Conversation

@liuxiaocs7

@liuxiaocs7 liuxiaocs7 commented Sep 1, 2026

Copy link
Copy Markdown
Member

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 catalogEntries instead of resolving a
catalog 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:

  • ModelCatalogEntry carries describedByMetadata, set by makeEntry from the
    same metadata lookup hasModelMetadata reads.
  • provider-connection-detail.tsx reads that field off the modelChoices it
    already 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 #1584 case (a user-typed id no inventory describes) still reports false,
so such a model keeps its declaration row. provider-endpoint-presentation.ts
reads the bundled table too, but only for build-time
generatedModelProviderOverrides a Host refresh never installs, so it stays
consistent and is left untouched (as the issue asks).

Fixes #4496

Verification

  • packages/core — new test in model-catalog.test.ts: a known model reports
    describedByMetadata: true, a bare id reports false (both matching
    hasModelMetadata), the field survives a wire round-trip, and an entry
    lacking it is rejected by the decoder. node --test: 20/20.
  • Wire/codec suites: runtime-policy-codec 13/13, runtime-policy-coordinator
    17/17.
  • npm run build across all packages (incl. the desktop renderer) ✓.
  • biome format/biome lint on the changed files ✓.
  • Regenerated apps/desktop/renderer-architecture.json (--write); the "Check
    renderer 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

describedByMetadata is required on the wire and decoded in
model-catalog-entry-codec, so RUNTIME_HOST_COMPATIBILITY_EPOCH moves
94 → 95. A Host and client that disagree about the field's presence are
refused at the handshake rather than dropping it silently. No stored document
changes shape — no data migration.

AI use

Select exactly one:

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

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 Code trailer.

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

@github-actions github-actions Bot added the effort/S Under 100 readable lines label Sep 1, 2026
@liuxiaocs7
liuxiaocs7 force-pushed the liuxiaocs7/desktop-connection-detail-asks-for-capability-de branch 4 times, most recently from f6a2669 to 006f248 Compare September 2, 2026 05:12

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

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

@liuxiaocs7
liuxiaocs7 force-pushed the liuxiaocs7/desktop-connection-detail-asks-for-capability-de branch 3 times, most recently from 8442d6e to 72ac2dc Compare September 2, 2026 08:28
@liuxiaocs7

Copy link
Copy Markdown
Member Author

Thanks for the careful review — both points are addressed in the current head (72ac2dcb).

[P2] Preserve the Host's metadata-coverage decision when the draft diverges. Fixed. resolveDraftConnectionModelCatalog still resolves locally once the draft diverges from storage, but it no longer lets the renderer's bundled table overturn the Host's answer: it now keeps the Host-projected describedByMetadata for every id already present in connection.catalogEntries, and falls back to local resolution only for an id the Host has never described. So ticking a model id or accepting a fetched list can no longer flip a refreshed-only model back to false and bring the spurious capability-declaration row back.

Added a regression test in packages/core/src/__tests__/model-catalog.test.ts"a divergent draft keeps the Host metadata-coverage decision for ids it already described" — that reproduces your scenario: a synthetic future model marked describedByMetadata: true on the Host entry but absent from the bundled table (asserted via hasModelMetadata(...) === false). The unedited draft retains true; after adding another enabled id, the edited draft still reports true for that id, while a brand-new id the Host never described takes the local answer (false).

[P2] Compatibility section. Fixed in the PR body to state the transition this head actually makes, with both ledger entries present. It now reads 94 → 95 rather than 93 → 94: main has kept advancing and further epoch bumps have landed since the review, so the branch re-numbers to main's epoch + 1 on each rebase.

@liuxiaocs7
liuxiaocs7 requested a review from jackwener September 2, 2026 09:41

@Astro-Han Astro-Han 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 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。

Comment thread apps/desktop/src/renderer/settings/provider-connection-detail.tsx Outdated
@liuxiaocs7
liuxiaocs7 force-pushed the liuxiaocs7/desktop-connection-detail-asks-for-capability-de branch from 72ac2dc to da1d8f9 Compare September 2, 2026 14:05
@liuxiaocs7

Copy link
Copy Markdown
Member Author

Thanks — the pre-merge fix is in (da1d8f9d).

Absence vs. described: false. Fixed in provider-connection-detail.tsx. The capability-row filter now reads:

const entry = entryById.get(modelId);
return entry !== undefined && !entry.describedByMetadata;

A missing entry — a quarantined id the provider registry drops from the catalog (OPENCODE_FREE_BROKEN_MODEL_IDS, the Codex normaliser) but enabledModelIds still carries so the user can untick it — no longer counts as "no metadata," so it stops growing a declaration row main never showed. A present-but-uncovered entry still gets its row, and since savedModelIds always yields an entry for a hand-typed id, #1584 is unaffected. Good catch — same class of bug the PR set out to close; I reproduced your opencode-free case before and after.

Sync point. Added a comment at the resolveDraftConnectionModelCatalog overlay noting describedByMetadata now has two producers (makeEntry and the hostCoverage patch), so a future Host-decided field must be patched back there too.

On the two smaller items — happy to fold either in this round, tell me which you'd prefer:

  • hasModelMetadata now has no production caller (only the codec/entry tests assert against it). Its body is exactly Object.keys(lookupModelMetadata(...)).length > 0, which makeEntry re-implements by hand, so I can route makeEntry through it or delete it and let the tests assert the entry — I left it as-is to keep the pre-merge diff to the behaviour fix.
  • A dedicated test for the changed expression: the page has no test harness today, so a clean seam means extracting the predicate, which adds a renderer dependency edge and churns the architecture snapshot. I kept it out of the pre-merge change for that reason, but can add it as a small extraction if you'd like the coverage.

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 main + 1 when that happens.

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
liuxiaocs7 force-pushed the liuxiaocs7/desktop-connection-detail-asks-for-capability-de branch from da1d8f9 to f06147c Compare September 2, 2026 14:21
@Astro-Han
Astro-Han merged commit 61224f5 into apache:main Sep 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/S Under 100 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop connection detail asks for capability declarations the Host can already provide

3 participants