Skip to content

feat(core,storage): add user-overridable model facts - #3129

Open
Nyvo-io wants to merge 6 commits into
apache:mainfrom
Nyvo-io:feat/2330-model-facts
Open

feat(core,storage): add user-overridable model facts#3129
Nyvo-io wants to merge 6 commits into
apache:mainfrom
Nyvo-io:feat/2330-model-facts

Conversation

@Nyvo-io

@Nyvo-io Nyvo-io commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a versioned, schema-validated model-facts.json authority keyed by provider:model. User overrides are projected over provider inventory and generated metadata for catalog, connection-test, and execution paths; enabled override-only models remain selectable without exposing unrelated models. Replacing facts atomically persists validated data and invalidates prior connection verification.

Fixes #2330

Verification

  • npm --workspace @maka/core run build
  • npm --workspace @maka/storage run build
  • npm --workspace @maka/core run typecheck
  • npm --workspace @maka/storage run typecheck
  • Core full suite: 540 passed
  • Focused model-facts suites: 68 passed
  • Storage full suite: 781 passed, 14 skipped; one unrelated environment failure because this worktree has no node_modules/dugite/git/bin/git for bundled-git-workspace-smoke.test
  • biome check on all 15 changed files
  • git diff --check

AI use

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

Tool(s) and scope: Codex implemented the change and tests, and performed local static and test review. The final commit contains Generated-by: Codex.

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

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary

This PR adds a versioned, schema-validated model-facts.json authority keyed by provider:model.

It solves these problems:

  • Users can override context windows, token limits, capabilities, modalities, and pricing.
  • Users can select custom models that provider catalogs do not list.
  • User fields take precedence over provider and generated facts.
  • Effective model facts apply to catalogs, context budgeting, connection tests, and execution.
  • Invalid, malformed, oversized, or unsafe documents fail closed.
  • Valid replacements persist atomically and invalidate prior connection verification.
  • Override-only models become selectable without exposing unrelated models.

Source of truth

This PR extends the existing model-fact sources. It does not replace provider inventories or generated built-in facts.

The effective order is:

  1. User overrides.
  2. Provider inventory.
  3. Generated built-in facts as an offline fallback.

The provider:model key prevents collisions between providers that use the same model ID.

Complexity delta

The PR adds:

  • A persisted model-facts document with schema and size limits.
  • Validation, normalization, projection, diagnostics, fingerprints, and generation tracking.
  • Projection branches for catalogs, connections, execution, onboarding, model fetching, and connection tests.
  • Public ./model-facts and runtime-policy store APIs.
  • Tests for projection, persistence, recovery, validation, and race handling.

It removes no existing authority. Generated facts remain read-only, and provider inventories remain available.

The added complexity supports custom models, field-level precedence, atomic replacement, restart recovery, unsafe-input rejection, and connection-test invalidation. No safe deletion or simplification is apparent without weakening behavior or regression coverage.

Total maintenance complexity increases, but the increase is justified by the required persisted override behavior. Optional follow-up concerns include surfaced diagnostics, external-edit generation handling, dead catalog-pipeline code, precedence documentation, repeated projections, diagnostic classification, late byte-budget validation, and enabled-ID trimming.

Risks and validation

Effective model facts now affect model visibility, context budgeting, connection tests, execution, and behavior across restarts.

Persisted replacements and external file changes can affect verification invalidation and in-flight test results.

The new ./model-facts export and runtime-policy store methods expand public contracts.

The tests cover provider-specific keys, field merging, capability preservation, custom models, validation, bounded persistence, malformed files, prototype-key rejection, restart recovery, cleanup, rollback, external edits, and pending-ticket supersession.

The repository defines build, typecheck, lint, formatting, and Core/Storage test commands. The supplied context reports these checks as passing, but the shell output provides no direct execution results. Final required-check status therefore remains unverified.

Review-relevant risks

The diff has apparent user-visible behavior and public-contract effects through model selection, catalog projection, connection testing, execution, the ./model-facts export, and runtime-policy store APIs. Material changes in these areas require independent human review under repository policy.

The diff has an apparent persistence and operational-integrity effect through atomic model-facts.json replacement, bounded reads, recovery cleanup, fingerprints, generation tracking, and verification invalidation. Material changes in these areas require independent human review under repository policy.

The diff has an apparent security effect through schema validation and prototype-key rejection. Material changes in this area require independent human review under repository policy.

No licensing, release, or governance effect was identified in the current diff.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

This change adds validated model-fact overrides for metadata and capabilities. It persists overrides in model-facts.json, exposes them through runtime-policy stores, applies them to catalogs and execution connections, and invalidates connection tests when facts change.

Changes

Model facts override flow

Layer / File(s) Summary
Model facts contracts and normalization
packages/core/src/model-facts.ts, packages/core/src/runtime-policy/connection-catalog-codec.ts, packages/core/src/__tests__/model-facts.test.ts, packages/core/src/__tests__/runtime-policy-codec.test.ts, packages/core/package.json
Adds model-fact schemas, key construction, strict validation, field merging, modality handling, and the public package export.
Catalog override projection
packages/core/src/model-catalog.ts, packages/core/src/__tests__/model-catalog.test.ts
Applies overrides to existing and missing catalog entries, merges capabilities, preserves webSearch, and reports user_override provenance.
Model facts persistence and store facades
packages/storage/src/model-facts-store.ts, packages/storage/src/runtime-policy-stores.ts, packages/storage/src/index.ts, packages/storage/src/runtime-policy/document-io.ts, packages/storage/src/__tests__/model-facts-store.test.ts
Adds bounded model-facts reads and writes, validation errors, store facades, public exports, and temporary-file cleanup.
Runtime policy integration and lifecycle validation
packages/storage/src/runtime-policy/coordinator.ts, packages/storage/src/__tests__/runtime-policy-model-facts.test.ts, packages/storage/src/__tests__/runtime-policy-stores.test.ts
Projects runtime-policy results through overrides, clears connection-test results after replacements, validates projected ticket state, and verifies restart persistence and protocol selection.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to c5c2f

Pricing overrides are currently rejected, so the advertised user-defined pricing behavior will not work. Merge should wait until the supported pricing fields are accepted and their merge behavior is verified.

Sequence Diagram(s)

sequenceDiagram
  participant RuntimePolicyStoresWriter
  participant RuntimePolicyCoordinator
  participant ModelFactsDocumentOwner
  participant CatalogSnapshot
  RuntimePolicyStoresWriter->>RuntimePolicyCoordinator: replaceModelFacts(overrides)
  RuntimePolicyCoordinator->>ModelFactsDocumentOwner: replace(root, overrides)
  ModelFactsDocumentOwner-->>RuntimePolicyCoordinator: validated ModelFactsDocument
  RuntimePolicyCoordinator->>CatalogSnapshot: apply model-fact overrides
  CatalogSnapshot-->>RuntimePolicyStoresWriter: projected runtime-policy result
Loading

Suggested reviewers: m4n5ter

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: adding user-overridable model facts across core and storage.
Description check ✅ Passed The description includes the required summary, issue reference, verification results, AI disclosure, checklist, and behavior-change declaration.
Linked Issues check ✅ Passed The changes implement the linked issue objectives for keyed, validated, field-level model-fact overrides, custom models, precedence, persistence, and catalog integration.
Out of Scope Changes check ✅ Passed The changed implementation and tests are directly related to model-fact overrides, persistence, runtime integration, validation, and verification invalidation.
Ai Use Disclosure ✅ Passed The PR selects generative tooling, names Codex and its implementation scope, and all 3 PR commits contain standalone Generated-by: Codex trailers.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 21de45db-4119-444a-a30e-50161c8d0fb3

📥 Commits

Reviewing files that changed from the base of the PR and between 93c65c7 and 3e8b439.

📒 Files selected for processing (15)
  • packages/core/package.json
  • packages/core/src/__tests__/model-catalog.test.ts
  • packages/core/src/__tests__/model-facts.test.ts
  • packages/core/src/__tests__/runtime-policy-codec.test.ts
  • packages/core/src/model-catalog.ts
  • packages/core/src/model-facts.ts
  • packages/core/src/runtime-policy/connection-catalog-codec.ts
  • packages/storage/src/__tests__/model-facts-store.test.ts
  • packages/storage/src/__tests__/runtime-policy-model-facts.test.ts
  • packages/storage/src/__tests__/runtime-policy-stores.test.ts
  • packages/storage/src/index.ts
  • packages/storage/src/model-facts-store.ts
  • packages/storage/src/runtime-policy-stores.ts
  • packages/storage/src/runtime-policy/coordinator.ts
  • packages/storage/src/runtime-policy/document-io.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.

Comment thread packages/storage/src/model-facts-store.ts Outdated
Comment thread packages/storage/src/runtime-policy/coordinator.ts Outdated
@Nyvo-io

Nyvo-io commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Final revision is ready for maintainer review.

Addressed both CodeRabbit findings in one follow-up commit: prototype-key rejection now fails closed, and model-fact replacement now invalidates verification before persistence while superseding in-flight connection-test tickets.

Verification is complete: all GitHub checks pass, CodeRabbit reports no actionable comments, and the PR is mergeable. Local verification also passed Core 540/540 and the focused model-facts suites; Storage passed 783 tests with 14 platform skips, with the one unrelated bundled-Git smoke failure documented in the PR description.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for the rework — I verified the architecture is right: read-time projection (overrides never enter the persisted catalog, so session history stays clean — sessions store slug+modelId and re-resolve at execution), user > provider > metadata precedence with field-level capability merge, protected id, schema-validated fail-closed file, atomic replaceModelFacts write, and — critically — the execution path really is wired (resolveExecutionConnection projects, then resolveSelectedModelContextWindow reads the override, so the headline scenario "override contextWindow instead of falling back to 16K" works). The second commit's fail-closed clear-then-write race fix and prototype-pollution defense are sound, and the priority order matches the issue's cross-tool research consensus. CI 14/14 green.

Conclusion: PASS with two P2s that need handling or an explicit deferral note.

P2-1 — the only production entrypoint is hand-editing the JSON file, and a bad file silently disables all overrides with zero diagnostics. readWithDiagnostics returns a diagnostic, but every consumer takes only .document.overrides (coordinator.ts:1441-1443, :639-642, :1041-1046, :1244-1246), and grep confirms nothing in runtime-host/cli/ui references modelFacts/replaceModelFacts — the API has no production callers. A user who typo's model-facts.json (the only live path) loses every override silently, and the "bad models.json → error surfaced, built-ins still work" requirement from #2330 is only half met. At minimum surface the diagnostic (a warn log on the projection read, and an integration test that a bad file through the coordinator disables overrides — today only the store layer tests this).

P2-2 — the invalidation semantics only hold for the (uncalled) API path, not for the file-edit path that is the real entrypoint. modelFactsGeneration is only bumped by replaceModelFacts; an external edit of the file doesn't move it, and sameConnectionTestModelBasis compares only id+apiProtocol — so editing only contextWindow keeps a stale lastTest: verified badge and doesn't cancel an in-flight connection test (whose result then commits under the new facts). Either derive the generation from the file's mtime/stat on the read path, or explicitly document "file edits are advisory; invalidation only happens via replaceModelFacts". Also: replaceModelFacts clearing lastTest for all connections (including unrelated providers) is over-invalidation — consider narrowing to the affected provider/model.

P3 (optional): the modelFactOverrides pipeline in model-catalog.ts is dead in production (no caller passes it; the desktop model menu reads the storage projection) — ~240 lines of code+tests serving only itself; delete if the upcoming UI PR won't consume it; the webSearch capability passthrough is a behavior change for unoverridden provider models too (previously discarded, now surfaced in the catalog) — not declared; dual authority with relayModelProfiles (per-connection, relay-specific): on relay connections the relay profile silently wins over the global file for overlapping fields (contextWindow/vision) — the precedence is undocumented; every catalog snapshot / connection resolution / test-start re-reads and re-freezes the full projection even with no overrides — a stat/mtime cache would help; the error.message.includes('exceeds') diagnostic classification is fragile to wording changes in document-io.ts; 512 overrides × 2048-char strings can exceed the 256KB byte cap after prepareReplacement clears lastTests → commit_outcome_unknown with verification discarded but overrides not written — byte-budget check belongs in prepare; untrimmed enabledModelIds are pushed verbatim as override-entry ids while lookup trims — a whitespace id creates a dirty entry.


AI-assisted review disclosure: this review was produced with AI assistance (pi review subagent on opencode-go/deepseek-v4-flash), which traced the projection/priority/invalidation paths and grepped for production callers. P2-1 and P2-2 are consequences of the caller graph (both surfaced as unverified predictions). Please weigh these findings with your own judgment.

中文摘要(AI 辅助审查)

结论:PASS(2 个 P2 需处理或显式延后)。架构正确:读时投影(覆盖不进持久化 catalog、Session 历史干净——session 只存 slug+modelId 执行时重解析)、优先级 user>provider>metadata+字段级 capability merge、id 受保护、schema 校验 fail-closed、replaceModelFacts 原子写,执行路径真打通(resolveExecutionConnection 投影后 resolveSelectedModelContextWindow 取覆盖值,headline 场景"覆盖 window 而非 16K 回退"成立);第二 commit 的 fail-closed clear-then-write 竞态修复与原型污染防御正确;优先级顺序符合 issue 跨工具调研共识。CI 14/14 绿。P2-1:唯一生产入口是手改 JSON,坏文件静默失效全部覆盖零诊断——readWithDiagnostics 返回 diagnostic 但所有消费者只取 .document.overrides,且 grep 确认 runtime-host/cli/ui 零引用 modelFacts/replaceModelFacts(API 无生产调用者);打错一个字符所有覆盖静默消失,#2330 的"坏文件→error surfaced、内置照常"只完成一半。至少:投影读路径 warn 日志 + "坏文件经 coordinator 投影后覆盖禁用"集成测试(现只测到 store 层)。P2-2:作废语义只对(无人调用的)API 路径成立——modelFactsGeneration 仅 replaceModelFacts 递增,文件外部编辑不改变;sameConnectionTestModelBasis 只比较 id+apiProtocol,仅改 contextWindow 的编辑保留过期 lastTest: verified 且不拦截飞行中 connection test(后者以旧事实测试结果提交到新事实下)。建议:读路径用文件 mtime/stat 派生 generation,或显式声明"文件编辑为 advisory,作废仅经 replaceModelFacts";另 replaceModelFacts 对所有连接(含无关 provider)清 lastTest 属过度作废,建议按受影响 provider/model 收窄。P3(可选):model-catalog.ts 的 modelFactOverrides 管线在生产是死代码(无生产调用者,桌面模型菜单走 storage 投影)——若后续 UI PR 不消费应删除(~240 行);webSearch 能力透传对未覆盖的 provider 模型也是行为变更(此前被丢弃现在出现在 catalog)未声明;与 relayModelProfiles 双权威(relay 连接上 relay profile 静默压过全局文件的重叠字段)优先级未文档化;每次 catalog 快照/resolve/test-start 都重读+refreeze 全量投影(可加 stat/mtime 缓存);错误分类依赖 error.message.includes('exceeds') 脆;512 覆盖×2048 字符可能超 256KB 字节上限且发生在 prepareReplacement 清 lastTests 之后→commit_outcome_unknown(验证已丢覆盖未写入),字节预算应在 prepare 阶段;未 trim 的 enabledModelIds 原样 push 而 lookup trim——空白 id 产生脏条目。

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

The field-level precedence (user > provider > metadata), immutable persisted authority, atomic replacement, generation tickets, and execution projection are coherent. Existing public threads already cover malformed-file diagnostics and external-edit invalidation, so I am not duplicating them.

One additional transaction-ordering issue remains below. The first-principles rule is that the complete candidate document—including serialized byte budget—must be validated before any dependent state is invalidated. More broadly, a content fingerprint can be the single authority for both external-edit generation and test invalidation, instead of maintaining parallel API-only generation semantics.

Review performed with three Codex reviewer agents and DeepSeek V4 Flash as advisory tools; I reproduced the finding on the latest head. The branch also needs rebasing because GitHub currently reports conflicts with main.

中文评论

字段级 precedence(user > provider > metadata)、不可变持久权威、原子替换、generation ticket 和执行投影整体闭环。现有公开线程已覆盖 malformed-file diagnostic 与外部编辑失效,我不重复。

以下仍有一个额外的事务顺序问题。第一性原理是:完整候选文档(包括序列化字节预算)必须在任何依赖状态失效之前完成验证。更进一步,可用内容 fingerprint 作为外部编辑 generation 与测试失效的单一权威,避免维护只对 API 写入生效的并行 generation 语义。

本次审查使用了三位 Codex reviewer agents 与 DeepSeek V4 Flash 作为辅助工具;我已在最新 head 上复现该问题。当前 GitHub 还显示分支与 main 冲突,需要 rebase。

Comment thread packages/storage/src/runtime-policy/coordinator.ts Outdated
@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Uncertain replacement keeps tickets valid ✓ Resolved 🐞 Bug ☼ Reliability
Description
Fix-now: modelFactsGeneration advances only after writeReplacement() returns, but the writer can
publish the renamed facts file and then throw commit_outcome_unknown when directory sync fails. In
that state the new facts are active while an in-flight connection-test ticket retains the old
generation and may commit a fresh verified result, especially for fact changes outside the narrow
API-protocol model basis.
Code

packages/storage/src/runtime-policy/coordinator.ts[R238-240]

+        const persisted = await this.modelFacts.writeReplacement(root, document);
+        this.modelFactsGeneration += 1;
+        return deepFreeze(persisted);
Relevance

●●● Strong

Recent reliability precedents accept fixes for crash windows and stale persisted state after partial
commits.

PR-#1742

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The document writer renames before syncing the directory and explicitly reports an unknown outcome
if a later step fails; the coordinator increments generation only after that call resolves.
Completion relies on generation mismatch, while the fallback semantic model basis contains only
enabled IDs, source, IDs, and API protocol, so other newly published fact changes do not supersede
the old ticket.

packages/storage/src/runtime-policy/coordinator.ts[222-249]
packages/storage/src/runtime-policy/document-io.ts[142-182]
packages/storage/src/runtime-policy/coordinator.ts[1234-1263]
packages/storage/src/runtime-policy/connection-catalog-document.ts[634-660]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A post-rename durability failure can leave new facts published without advancing the generation used to supersede in-flight connection tests.

## Issue Context
Reuse the existing generation invalidation seam: when replacement reports an unknown commit outcome, conservatively advance the generation before propagating the error. Add a fault-injection test covering failure after rename/publication.

## Fix Focus Areas
- packages/storage/src/runtime-policy/coordinator.ts[237-248]
- packages/storage/src/runtime-policy/document-io.ts[155-182]
- packages/storage/src/__tests__/runtime-policy-model-facts.test.ts[86-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Sparse modalities bypass validation ✓ Resolved 🐞 Bug ≡ Correctness
Description
Fix-now: decodeModelModalities() uses Array.map, which skips holes, so a programmatic input such
as {input: Array(1), output: ['text']} is accepted with an unvalidated sparse modality. The
malformed model can then pass normalizeConnectionModelDiscoveryResult() and enter the
catalog/runtime contract.
Code

packages/core/src/runtime-policy/connection-catalog-codec.ts[R518-519]

+  const input = item.input.map((entry) => decodeModelInputModality(entry));
+  const output = item.output.map((entry) => decodeModelOutputModality(entry));
Relevance

●●● Strong

Exact recent precedent accepted rejecting sparse arrays caused by map holes.

PR-#3079

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new modality decoder maps arrays directly, while the discovery normalizer trusts each decoded
model; JavaScript map preserves and skips sparse slots. The same root-cause pattern was accepted
in PR #3079.

packages/core/src/runtime-policy/connection-catalog-codec.ts[513-520]
packages/core/src/runtime-policy/connection-catalog-codec.ts[566-592]
PR-#3079

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Sparse modality arrays bypass decoding because `Array.map()` skips holes.

## Issue Context
This is the same decoder pattern previously fixed for sparse protocol arrays; use an iteration form that visits every numeric slot so holes are decoded as invalid values.

## Fix Focus Areas
- packages/core/src/runtime-policy/connection-catalog-codec.ts[513-520]
- packages/core/src/__tests__/runtime-policy-codec.test.ts[376-401]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Oversized input misclassified ✓ Resolved 🐞 Bug ≡ Correctness
Description
Non-blocking remediation: an otherwise schema-valid replacement can exceed 256 KiB because up to 512
overrides may each contain 2,048-character fields, but the size is checked only by the persistence
writer and therefore surfaces as invalid_document instead of caller-facing invalid_policy_input.
Runtime-policy consumers classify that as a persistence failure rather than an invalid request.
Code

packages/storage/src/model-facts-store.ts[R77-79]

+  async writeReplacement(root: string, document: ModelFactsDocument): Promise<ModelFactsDocument> {
+    await writeJsonDocument(root, FILE, document, MODEL_FACTS_DOCUMENT_MAX_BYTES);
+    return document;
Relevance

●● Moderate

Clear classification issue, but no close precedent confirms preflight byte-size validation for this
new store.

PR-#3028

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Preparation bounds shape and count but not encoded bytes; writeJsonDocument applies the byte limit
through invalidDocument, while the established policy mutation path checks serialized size during
preparation and raises invalid_policy_input. The runtime host maps those two codes to different
public failures.

packages/storage/src/model-facts-store.ts[49-79]
packages/core/src/model-facts.ts[52-60]
packages/core/src/model-facts.ts[82-87]
packages/storage/src/runtime-policy/document-io.ts[133-140]
packages/storage/src/runtime-policy/policy-document.ts[85-96]
packages/runtime-host/src/server/runtime-policy-coordinator.ts[320-339]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Oversized replacement input is classified as a corrupt/persistence document instead of invalid policy input.

## Issue Context
Reuse `serializeJsonDocument()` during `prepareReplacement()`, as the existing runtime-policy document owner does; no new authority or public surface is needed.

## Fix Focus Areas
- packages/storage/src/model-facts-store.ts[49-79]
- packages/storage/src/__tests__/model-facts-store.test.ts[10-30]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Web pages:
  +2 more
Review mode: 🧠 Deep: This is a bug-dense behavioral change spanning core projection, schema validation, storage persistence, public store APIs, catalog/connection-test paths, and verification invalidation, with many independent logic sites where redundant review can catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/core/src/runtime-policy/connection-catalog-codec.ts Outdated
Comment thread packages/storage/src/runtime-policy/coordinator.ts Outdated
@Nyvo-io
Nyvo-io force-pushed the feat/2330-model-facts branch from 6fc9c1d to c5c2fa1 Compare August 19, 2026 05:46
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ba670128-8656-4efd-a2c1-b5feb82a2d33

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef1c55 and c5c2fa1.

📒 Files selected for processing (15)
  • packages/core/package.json
  • packages/core/src/__tests__/model-catalog.test.ts
  • packages/core/src/__tests__/model-facts.test.ts
  • packages/core/src/__tests__/runtime-policy-codec.test.ts
  • packages/core/src/model-catalog.ts
  • packages/core/src/model-facts.ts
  • packages/core/src/runtime-policy/connection-catalog-codec.ts
  • packages/storage/src/__tests__/model-facts-store.test.ts
  • packages/storage/src/__tests__/runtime-policy-model-facts.test.ts
  • packages/storage/src/__tests__/runtime-policy-stores.test.ts
  • packages/storage/src/index.ts
  • packages/storage/src/model-facts-store.ts
  • packages/storage/src/runtime-policy-stores.ts
  • packages/storage/src/runtime-policy/coordinator.ts
  • packages/storage/src/runtime-policy/document-io.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/storage/src/index.ts
  • packages/storage/src/tests/runtime-policy-stores.test.ts
  • packages/core/src/tests/runtime-policy-codec.test.ts
  • packages/core/package.json
  • packages/core/src/tests/model-facts.test.ts
  • packages/storage/src/model-facts-store.ts
  • packages/storage/src/runtime-policy-stores.ts
  • packages/core/src/tests/model-catalog.test.ts
  • packages/core/src/runtime-policy/connection-catalog-codec.ts
  • packages/core/src/model-catalog.ts
  • packages/storage/src/runtime-policy/coordinator.ts
  • packages/storage/src/tests/model-facts-store.test.ts

Included review availability: Your plan provides up to 3 included reviews per hour; 2 remain after this review.

Comment thread packages/core/src/model-facts.ts

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

The immutable facts document, field-level projection, generation tickets, and execution resolver are coherent on this head; I also verified that the execution resolver applies overrides before returning, so an initially suspected execution bypass was discarded. One normal refresh path still removes the headline override-only model state.

The inline P2 is the smallest remaining product correction: model fetch reconciliation needs to preserve currently enabled IDs backed by model facts without writing them into provider inventory. The current live typecheck is also failing, so this head is not merge-ready independently of the review finding.

AI-assisted review by Codex with three independent reviewer passes and OpenCode Go DeepSeek V4 Flash (high) as an advisory pass; I verified the current head, refresh reconciliation, projection semantics, existing threads, and live CI.

中文

当前 head 的不可变 facts document、字段级 projection、generation ticket 与 execution resolver 整体闭环;我也确认 execution resolver 返回前确实应用 overrides,因此剔除了一个初步误报。仍有一个正常刷新路径会删除核心的 override-only model 状态。

行内 P2 是最小剩余修复:model fetch reconciliation 应保留当前已启用且由 model facts 支撑的 ID,但不要把它写进 provider inventory。当前实时 typecheck 也失败,因此即使不考虑 finding,这个 head 也尚不可合并。

本次由 Codex、三个独立 reviewer 与 OpenCode Go DeepSeek V4 Flash high 辅助;已核对当前 head、刷新 reconciliation、projection semantics、已有线程和实时 CI。

return deepFreeze({ kind: 'committed' as const, snapshot });
return deepFreeze({
kind: 'committed' as const,
snapshot: await this.projectCatalogSnapshot(root),

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.

[P2] Preserve enabled override-only models across provider refresh

By the time this projected snapshot is returned, writeModelFetchResult has already reconciled enabledModelIds strictly against the fetched inventory. An enabled custom model backed by model-facts.json is therefore removed (and may lose the default) whenever the provider returns only its live inventory; the projection here cannot add it back because it is no longer enabled. Please pass the current facts-backed IDs into model-fetch reconciliation, preserve only those already enabled/defaulted, and add a refresh regression proving the custom model remains selectable and executable.

中文

返回这个 projected snapshot 时,writeModelFetchResult 已经只按 provider inventory 重算 enabledModelIds。由 model-facts.json 支撑的已启用 custom model 会在刷新时被删除,默认项也可能被切换;此时 projection 因该 ID 已不再 enabled,无法把它加回来。请把当前 facts-backed IDs 传入 reconciliation,只保留已经 enabled/default 的部分,并补刷新后仍可选择与执行 custom model 的回归。

@Nyvo-io
Nyvo-io force-pushed the feat/2330-model-facts branch from c5c2fa1 to 8c592fd Compare August 19, 2026 17:04

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

The current head is semantically equivalent to the previously reviewed implementation plus rebase/formatting. I verified that the oversized-input validation, sparse-modality validation, and post-publication generation invalidation are now covered; I also resolved the pricing thread because #2330 explicitly keeps pricing in the existing pricing editor rather than duplicating it in model-facts.json.

The existing provider-refresh P2 remains current and unresolved: refresh reconciliation still removes an enabled/default override-only model before the facts projection runs. No duplicate inline is needed. This also changes user-visible model selection by making override-only models selectable, but the PR has no visual evidence; please add an actual TUI or Desktop model-selector screenshot showing a facts-backed custom model. Exact-head CI has not run yet because the Apache Actions lanes are unavailable/queued.

AI-assisted review by OpenAI Codex. I verified the exact-head range-diff, current implementation and tests, issue #2330’s ownership decision, review-thread state, screenshot gate, provenance, and live checks.

中文

当前 head 与上一轮相比主要是 rebase/format;oversized input、稀疏 modalities 和发布后 generation 失效均已有修复。pricing 线程不成立,因为 #2330 明确要求 pricing 继续由现有 pricing editor 负责,不应重复进入 model-facts.json

已有的 provider refresh P2 仍然有效:刷新会在 facts projection 前删除已启用/默认的 override-only model,因此不重复发 inline。该改动会让自定义模型出现在用户选择器中,也需要补一张真实 TUI 或 Desktop 模型选择器截图。当前 exact-head CI 尚未实际运行。

本次由 OpenAI Codex 辅助,已核对 exact-head range-diff、实现与测试、#2330 权威边界、线程状态、截图门禁、来源披露和实时检查。

@Nyvo-io
Nyvo-io force-pushed the feat/2330-model-facts branch from 8c592fd to f321119 Compare August 19, 2026 17:38
@Nyvo-io

Nyvo-io commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Implemented and reviewed the provider-refresh correction in one follow-up commit: f321119.\n\n- Refresh reconciliation now preserves only facts-backed model IDs that are already enabled or are the connection's current default.\n- Raw provider inventories remain limited to live discovery results; custom facts are projected read-time.\n- The default target remains valid and the execution resolver still resolves the selected custom model.\n- Added Core and Storage regressions covering refresh, default retention, execution, inventory purity, and exclusion of unselected facts-backed models.\n- Rebased onto current main.\n\nVerification on the rebased head:\n- Core typecheck passed.\n- Storage typecheck passed after rebuilding Core.\n- Biome check passed for all changed files.\n- git diff --check passed.\n- Core reconciliation tests: 9 passed.\n- Storage model-facts tests: 6 passed.\n- Storage runtime-policy-stores tests: 44 passed.\n- Independent adversarial review found no actionable defect.\n\nPricing remains intentionally outside model-facts.json, per issue #2330's authority decision; the existing pricing editor remains the pricing source of truth.

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

Thanks — the storage half of this is genuinely careful, and I want to name that before the findings. Reviewed exact head f321119a7f2ebf3576e1a9bc9decfa2ea8326249.

The store gets the hard mechanics right: atomicity and temp-file recovery, commit_outcome_unknown on both the pre-clear and post-publication failure paths, oversize rejection that preserves verification, external-edit ticket supersession, and __proto__ rejection — with the key grammar handling colon-bearing model ids like ollama-cloud:gpt-oss:120b correctly, because only the provider segment is colon-free. The numeric validator is tighter than the codec's and rejects zero, negatives, non-integers and infinities. Most importantly, no write path reads a projected snapshot and writes it back, so overrides never contaminate connection-catalog.json — that is what keeps a stale pin recoverable, and there is an explicit assertion for it. Pricing is not reachable from this PR at all, since the override allowlist excludes price fields and pricing has its own authority; the wrong-number risk here is truncation, not cost.

The problem is the precedence chain. Context window now has three authorities — relayModelProfiles, this new fact table, and bundled metadata — and resolveSelectedModelContextWindow consults the first one first and returns early, so on an openai-compatible connection a model fact silently loses to a setting the user may have configured months earlier, with no diagnostic. RelayModelProfile is the pre-existing per-model user-declaration seam with a documented gate; the project rule is to extend the closest existing seam rather than run a parallel one beside it, and that is what happened here. Separately, the PR threads a second, entirely dead projection path through the catalog builders, which is why the one provenance signal it adds can never fire.

Two P1s, six P2s and three P3s inline. Not approving while P1/P2 findings are open.

On tests: the storage suite is strong on mechanics and empty on precedence. Nothing asserts a fact override against a competing authority, nothing drives capabilitySource through the call shape production actually uses — which is precisely why the dead path went unnoticed — nothing covers a contextWindow override against a row carrying the inputLimit this same PR starts persisting, and nothing covers a non-1 schemaVersion, a concurrent replace, or an override for a model that no longer exists. Each of those inherits its severity from the finding it belongs to rather than standing on its own.

Review disclosure: this review was prepared with Claude Code, which read the diff at this head, enumerated the reader sites and the precedence chain by search rather than by assumption, and executed several of the pure functions against verbatim copies to check the merge semantics. Evidence grade is stated per finding — three were reproduced by execution, the rest are code reading or labelled inference. The human contributor reviewed this before posting.

Comment thread packages/core/src/model-catalog.ts Outdated
: metadata.capabilities
? 'static_catalog'
: 'unknown',
capabilitySource: lookupModelFactOverride(

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.

[P1] This projection path has no production caller, so capabilitySource: 'user_override' can never fire. The ~94 lines threading modelFactOverrides through the catalog builders are reached only from model-catalog.test.ts; a search over the whole tree at this head finds no other caller. Production instead arrives here with a connection whose models[] was already merged by the storage-side projection in coordinator.ts, so input.modelFactOverrides is always undefined, lookupModelFactOverride returns undefined, and the ternary falls through to 'provider_api'. Concretely: a user writes {"openai:gpt-5":{"contextWindow":32000,"capabilities":{"vision":false}}}, and Settings shows 32 000 attributed to the provider API. The user cannot distinguish their own pin from a provider report — which is exactly the signal they need months later when the provider has corrected the value and the pin is now the stale one. Either delete this path and derive provenance inside the storage projection (project a factOverriddenFields set alongside models[]), or wire the real callers. The regression test has to go through the same call shape production uses; the two existing tests pass modelFactOverrides directly, which is why this was invisible.

);
}

private async readModelFacts(root: string) {

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.

[P1] Make this pure — a read must not perform a durable catalog write. readModelFacts calls clearAllConnectionLastTests on a fingerprint change, which is a real write that bumps every connection's revision, and failures surface as commit_outcome_unknown. It is reached from projectCatalogSnapshot, getModelFacts, and resolveExecutionConnection — that last one being on the send path. Concretely: a user with six verified connections hand-edits model-facts.json to fix one Ollama model's window; the next resolveExecutionConnection for an unrelated Anthropic connection wipes verification on all six, which then render as not verified, and if the catalog write fails the user's send fails with a commit-unknown error rather than degrading to a stale read. Two problems compounded: the write happens on a read, and the invalidation is global when the change is per-model. Move the generation bump and the invalidation into an explicit write-path reconcile, and scope it to connections whose projected model set actually changed. Note also that openInteractiveRuntimePolicyStoresForRead builds this coordinator on a read-access lease and runWithStorageRootLease only checks identity rather than blocking writes — that variant has no production caller today, so the read-lease angle is inference, but it is the kind of assumption worth not leaving lying around.

throw new Error('Invalid apiProtocol');
result.apiProtocol = value.apiProtocol;
}
for (const key of ['contextWindow', 'inputLimit', 'maxOutputTokens'] as const) {

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.

[P2] An override that raises contextWindow is silently discarded by the min against inputLimit. This PR newly persists inputLimit on stored model rows and newly lets a user override contextWindow, but resolveSelectedModelContextWindow takes narrowestPositiveLimit(model.contextWindow, model.inputLimit), and applyModelFactOverride sets only contextWindow and leaves the stored inputLimit alone. Reproduced by execution against verbatim copies: row {contextWindow: 8192, inputLimit: 8192} plus override {contextWindow: 200000} yields an applied row of {contextWindow: 200000, inputLimit: 8192} and a truncation budget of 8192 — while the catalog entry reports 200 000. So the number the user sees and the number that governs truncation disagree, which is the single worst outcome for a feature whose purpose is to correct that number. When an override sets contextWindow, clear or raise the row's inputLimit in applyModelFactOverride. Regression test: exactly the pair above. That a discovery adapter emits inputLimit today is inference — only the generated metadata sets it now — but the codec is asserted to pass it through, so the row shape is reachable.

return deepFreeze({
kind: 'ready' as const,
connection: structuredClone(connection),
connection: applyModelFactOverridesToConnection(

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.

[P2] Pick one authority for a user-declared context window. Merging the fact override into models[] here does not make it win: resolveSelectedModelContextWindow consults relayModelProfile(connection, modelId)?.contextWindow first and returns early, so on an openai-compatible connection the older setting silently beats the newer fact. Concretely: a user sets relayModelProfiles['m'].contextWindow = 8192 in settings, later writes {"openai-compatible:m":{"contextWindow":200000}} in model-facts.json, and truncation stays at 8192 while the model picker shows 200 000 — no warning, no precedence documented anywhere. RelayModelProfile is the pre-existing per-model user-declaration seam and it already has a documented gate, so generalising it was the closest existing seam; running a second override table beside it is the thing the project's own rule asks us not to do. At minimum make resolveSelectedModelContextWindow the single resolver for both and state the order in one place — but the better answer is one table, not two.

.map((key) => key.slice(prefix.length));
}

export function decodeModelFactsDocument(value: unknown): ModelFactsDocument {

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.

[P2] Distinguish an unsupported future version from a malformed file, and refuse to write over one. decodeModelFactsDocument requires schemaVersion === 1 exactly; readWithDiagnostics swallows the throw into diagnostic: 'malformed' with an empty document; and replaceModelFacts then overwrites the whole file with a v1 document. So a user who runs a newer build that writes schemaVersion: 2 and then reverts to this one loses every override silently — the only signal is a process.emitWarning — has all their connection verification cleared on the next read, and has the v2 data destroyed by the first write. Version downgrade is a normal thing for users on a release channel to do, and this is a one-way door. Treat an unknown-but-parseable version as a refusal to write rather than as corruption, and add a regression test that a schemaVersion: 2 document survives a replace.

}
}

async replace(root: string, overrides: ModelFactOverrides): Promise<ModelFactsDocument> {

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.

[P2] Adopt the revision-checked shape the sibling override store in this same package already uses. replace(root, overrides) swaps the whole table with no expected revision, so two windows that each read the table, each add one override, and each call replace silently lose the first one's entry — and getModelFacts returns no revision, so no caller can even detect the loss. packages/storage/src/pricing-store.ts is the existing user-override authority right next door and it does this correctly: upsert(expectedRevision, ...) / delete(expectedRevision, ...) with a PricingRevisionConflictError. Following it removes the lost-update entirely, and per-key writes also make the 256 KiB whole-document byte check unnecessary.

const PROVIDER_MODEL_KEY_PATTERN = /^([^:\s]{1,128}):([^\s]{1,256})$/;
const MAX_FACT_NUMBER = 10_000_000_000;

export function modelFactKey(providerType: ProviderType | string, modelId: string): string {

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.

[P2] Validate the provider segment against the registered providers, or report keys that matched nothing. modelFactKey types the provider as ProviderType | string and checks only that it has no colon, no whitespace and is at most 128 characters — so "anthropic:claude-opus-5" is happily stored for a connection whose providerType is anthropic-api, and the override then never applies anywhere. The document validates, getModelFacts reads it back intact, and the only symptom the user gets is that nothing changed — for a hand-edited JSON file, which is the primary way this feature is used, that is a bad failure mode. Either reject unknown provider segments on write, or have getModelFacts report unmatched keys so the UI can say which overrides are inert.

Comment thread packages/core/src/model-facts.ts Outdated
...(override.capabilities === undefined
? {}
: { capabilities: { ...model.capabilities, ...override.capabilities } }),
...(override.modalities === undefined ? {} : { modalities: override.modalities }),

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.

[P3] Merge modalities per direction, or document that they replace wholesale. capabilities merge field by field but modalities replace the whole object, and nothing in the type says so. Reproduced by execution: a row with {input: ['text','image','pdf'], output: ['text']} plus an override of {input: ['text'], output: ['text']} drops image and pdf — permanently, including any input modality the provider adds later. A user who only wanted to remove pdf has silently also removed image, and since the whole point of the pin is that it survives provider updates, they will not find out from a later correction. Per-direction merge matches how capabilities already behave and is what a reader would expect.

return { schemaVersion: MODEL_FACTS_SCHEMA_VERSION, overrides };
}

export function normalizeModelFactOverride(value: unknown): ModelFactOverride {

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.

[P3] Give the user a way to say "this fact is unknown", not just "this fact is that". normalizeModelFactOverride accepts only present-and-valid values, with no null tombstone, so when a provider reports a wrong contextWindow the user's only option is to substitute another number they would have to guess — they cannot express "ignore the provider here and fall back to bundled metadata", which for a correction feature is the more honest of the two things they might want. Reproduced by execution. A null sentinel in the allowlist would cover it.

Comment thread packages/core/src/model-catalog.ts Outdated
reasoning?: true;
functionCalling?: true;
imageGeneration?: true;
webSearch?: true;

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.

[P3] webSearch here is a separate concern with no production reader. The only thing that reads entry.capabilities.webSearch is model-catalog.test.ts; model-web-search.ts reads ModelInfo.capabilities.webSearch off the model row, not the catalog entry. So this adds a field to a public shape for a consumer that does not exist, inside a PR about user-overridable facts. The repository asks that product changes stay separate unless they must ship together — either wire the reader or move it to its own change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: user-overridable model facts (context window etc.) — models.json-style override layer

2 participants