feat: Defaults pick from the model catalog — the efforts a model takes, aliases resolved, and the routing defaults (Phase 2 · slice 7b) #24
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/provider-catalog-defaults"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Slice 7 of Phase 2, second half; 7a was #20. Settings → Defaults now picks the default model from OpenRouter's catalog. The catalog loads without a key and is remembered for an hour. The default effort offers only what the catalog says the chosen model takes. The tab also stores the routing defaults an
llmnode will inherit in slice 8: routing variant, service tier and zero data retention.What
Kagaku.Flow.OpenRouter(the package)ILlmGateway.GetCatalogAsync(): the catalog of models whose output includes text.Retry-Afteris honoured up to 5 s.OpenRouterGatewayOptionsgainsCatalogTimeout,CatalogTtlandCatalogRetry.ModelCapabilities: the one "fails closed" rule (M1–M5).TakesReasoning: the entry lists thereasoningparameter, matched case-sensitively.AcceptedEfforts: Off only when the entry says reasoning can be switched off, and a level only when the entry names it. The list is ascending, with no duplicates and no unknown values.Tsumugi.UseCases.SettingsGetModelCatalog: returnsCatalogModel(Id, Name, AliasTargetName, Efforts), one entry per id, sorted by display text. A~…-latestalias reads "Name → target".ReasoningEfforts.Offered(modelId, model): what the effort picker offers.RoutingDefaults: carried byLlmDefaults. It holds:ModelRouting: Balanced, Fastest:nitro, Cheapest:floor, Most reliable with tools:exacto;ServiceTierPreference: Auto, Standard, Flex, Priority;RoutingSettings: a written table in the node's own spelling:balanced|nitro|floor|exacto,auto|default|flex|priority,true|false.standardor a blank, reads as the built-in value, field by field.IModelProvidergains its fourth member, the catalog, as 7a announced.Settings → Defaults (
DefaultsSection)Why
AppSettingrows under keys that are never renamed.ADRs and stories amended
ADR 0018:
reasoningparameter and a named effort."structured_outputs,tools, a reasoning block)" becomes "(structured_outputs,tools, thereasoningparameter)".reasoningparameter, and its reasoning block names the effort (fornone, says reasoning can be switched off)".docs/stories/settings.md:Decisions to confirm
reasoningparameter. A reasoning block alone does not count. Three live models, such asrekaai/reka-edge, have the block without the parameter."Reasoning"row goes too.default. This avoids a secondoff/nonemismatch.Switchis an optional follow-up.ModelRoutingVariant/ServiceTier/ProviderPreferenceshas no consumer before slice 8's executor. Slice 8 addsILlmDefaultsSourceand the mapping.ItemText. An upstreamItemSearchTextis an optional follow-up.BuiltInstill starts at"".Departures from the brief, each pinned by a test:
LoadingandLoadErroronly while it has no list. A list loaded earlier stays usable during a reload and after a failed one (A_failed_reload_keeps_the_list_it_had).Without_a_model_a_failed_catalog_leaves_the_usual_hint).A_hand_edited_routing_row_reads_as_the_built_in_on_its_own.Out of scope
CompleteAsync, streaming and the per-key client;ILlmDefaultsSource, and applying routing, tier and ZDR to requests;llmdescriptor and executor;structured_outputs/tools, pricing and context length, and per-node pickers.Switch,ItemSearchText, andSaveTextonMaskedSecretField.Tests
da9f961)npm test)The Release build has 0 warnings under
TreatWarningsAsErrors. The fixturemodels.jsonholds six entries, each identical to its entry in a live catalog snapshot.Coverage (coverlet, per changed file, lines and branches with
branch="True"): all 20 changed source files with executable code are at 100%.OpenRouterModelProvider.cs(54/54 lines, 48/48 branches),ModelCapabilities,OpenRouterGateway,RoutingSettings,GetModelCatalog,DefaultsSection, and the reducers and effects.ILlmGateway,IModelProvider,ModelRouting,ServiceTierPreference,SettingKeysandLoadModelCatalog.Mutation probes. Each was run on a copy, then restored and
touched. All were killed.§12 probes 42–64 (39 sub-cases):
8 extra probes:
Enum.IsDefinedfilter is dropped;Re-run after #21/#22 changed the Combobox and Select markup: 58a, 59a, 59b, 61a, 62 and 63.
The verifier's own probes. Four were killed: the id shown twice, one flag text for both cases, a reload that keeps the old error, and a case-sensitive sort. Two survived:
reasoningcase-insensitively.Both now have tests:
A_disposed_gateway_refuses_even_the_catalog_it_already_holds, and a"Reasoning"row. Each test fails under its mutant.Browser pass
Setup:
b7b-*,r1-*) are kept in the probes directory.Steps:
~anthropic/claude-opus-latest, and it survives a reload.high.high.nitro,defaultandtrue, and they survive a reload.retired/model-xreads "retired/model-x (not in the catalog)", and its effort is flagged.A_picker_opened_while_the_catalog_loads_lists_the_whole_catalog_once_it_lands(2 rows) fails against the old Combobox.Not caused by this branch: at 390 px the app header clips the settings gear and the menu. The branch does not touch the shell.
Verification round (before opening)
The verifier and the browser pass raised 11 points. All are applied, in
9090348and in Kagaku.UI #26:reasoningparameter;500f362).Review round 1
Jibril approved it (✅). None of its three 💡 needed code here.
After Kagaku.UI #26 merged,
14923e6moved the submodule to1ac43db.BackupWriterWAL flake, which predates this PR.🤖 Generated with Claude Code
IModelProvider gains the catalog as Tsumugi DTOs. Infrastructure maps each library entry to a CatalogModel: its id, its name (the id when the catalog gives none or a blank one), the name of the model a ~…-latest alias routes to, and the efforts the package's ModelCapabilities says it takes, translated into Tsumugi's spelling through a table (the library's None is Tsumugi's Off; an effort the library does not know is dropped). A failed load becomes one sentence, "The model catalog could not be loaded: …", with the same detail rule as the key check's, so a proxy's HTML page never reaches the writer, and the transient suffix when the library calls it transient. GetModelCatalog keeps each id once, the first entry winning, because the picker keys its options by id and a repeated key breaks the renderer; it orders by what the picker shows, ignoring case, then by id. The routing defaults are install-wide settings an llm node inherits at run time (ADR 0018); this slice stores them and slice 8 applies them. ModelRouting and ServiceTierPreference are stored through a written table in the node's own vocabulary, balanced/nitro/floor/exacto and auto/default/flex/priority, so Standard is stored as "default" and no second off/none mismatch appears. Parsing reads the table only, ignoring case: a number, which Enum.TryParse would accept, or the label "standard" reads as the built-in. Zero data retention is stored as true or false. LlmDefaults takes the routing as a fifth positional member, so every construction names it and a record rebuilt from its other fields cannot silently reset it. SaveLlmDefaults refuses an undefined routing or tier and writes nothing, and writes all seven rows otherwise; GetLlmDefaults reads each row on its own, so one hand-edited row falls back alone. The Defaults tab's model is a Combobox over the catalog, which loads without a key. The option text carries the id ("Name (id)", the id once when the name is the id, "Alias → Target (id)" for an alias), because the Combobox filters by its item text and a writer may know a model by either. A pick saves at once. A stored id the picker cannot show as a catalog entry stays selected as a stand-in item: bare while the catalog loads or after it failed, "(not in the catalog)" once a loaded catalog lacks it. The effort list follows the chosen model through ReasoningEfforts.Offered: every effort with no model, only Model default for a model the catalog does not list, since effort resolution fails closed, otherwise Model default and what the model takes. A stored effort outside that list is never changed behind the writer's back: it stays chosen as an extra option and is flagged under the field. While the first load runs the effort Select waits; a failed load shows the sentence with "Try again", offers every effort and says the model's efforts cannot be checked. A catalog loaded earlier stays in use while a reload runs and after it fails, so the picker's Loading and LoadError apply only while there is no list. A new Routing card holds Routing, Service tier and Data retention, a two-option Select because Kagaku.UI has no switch; each saves the whole record at once, so a pick carries a pending typed edit and a concurrency edit keeps the routing. The Provider tab's no-key line adds that the catalog loads without a key. The ReasoningEfforts summary no longer claims the stored strings are OpenRouter's: they are Tsumugi's, and each consumer translates through a table, the first being the catalog's. ADR 0018 is amended: the catalog load has a 10-second timeout and two attempts, zero data retention is a setting rather than a switch, and "fails closed" is defined as the reasoning parameter plus a named effort. settings.md ticks the default model and effort and amends the switch; the routing line stays open until an llm node uses the defaults. The model picker only takes list values, so the test that cleared the model field goes, and the test that a pending edit is flushed on leaving the tab now types a concurrency, since model picks no longer wait. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagaku.Flow - 99.5%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 97.2%
Tsumugi.Domain - 99.3%
Tsumugi.Infrastructure - 97.2%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 98.2%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A model catalog that loads without a key, aliases that resolve to their target, a fails-closed effort rule that lives in the package so picker and executor can never disagree… this is the kind of slice I want to take home and keep~ ♡ Every claim in that enormous PR body, I chased down. Every single one held.
Verdict: ✅ Looks good to me~
No blockers. Not one. Do you know how rare that is? Let me show you my work, because I don't hand out ✅ from vibes~
🔬 What I did to this PR
500f362(CI absent for this SHA — no bot comment, no status yet): Release build 0 warnings / 0 errors, full suite 1146/1146 — Flow.OpenRouter 71, Flow 483, BlazorAdapter 268, Integration 247, UseCases 40, Domain 37. Your PR table matches to the digit.ModelCapabilities28/28,OpenRouterGateway96/96,OpenRouterGatewayOptions12/12 from the package's own tests alone;DefaultsSection.razor230/230, effects/reducers/state 100%;GetModelCatalog24/24,OpenRouterModelProvider108/108,SaveLlmDefaults48/48,GetLlmDefaults30/30,RoutingSettings100% once UseCases+Integration union in. The "all 20 executable files at 100%" claim is true.Ordinal→OrdinalIgnoreCaseinTakesReasoning→ the"Reasoning"theory row goes red ♡GetCatalogAsync→A_disposed_gateway_refuses_even_the_catalog_it_already_holdsredOfferedreturningAllfor a missing model (breaking fails-closed) → redOnLoadCatalogkeeping the staleCatalogError→A_catalog_that_fails_to_load…redMandatory: nullarm flipped to always-offer → 3 rows red (the nullable-veto is load-bearing!)Enum.IsDefinedrouting guard removed from save → redValidateKeyAsync; one-action-per-record files held;NewHttpClient(apiKey: null)sends no Authorization header at all (asserted, not assumed); the library'sCachingOpenRouterClientreally does refuse to cache failures and read TTL from the injected clock — your "failure is not remembered" claim is the library's semantics, verified by reading it and by your test.✅ What I liked~
ModelCapabilities. The mandatory-tri-state (true/false/null-vetoed-by-listed-none), the number-string drop viaEnum.IsDefined, unknown efforts dropped, distinct + ascending whatever order the catalog lists — pinned by a 15-row theory and byte-verbatim live fixtures. The alias-takes-what-its-target-takes assertion is deliciously thorough.TimeProvider.System(a frozen test clock would hold a retry forever), TTL on the injected clock — with the reasoning written in the comment and a test that proves it. Dispose ordering (HttpClient first to cancel in-flight, then the cache's gates) documented and tested both ways.RoutingSettingsas a written table. Standard stored asdefaultwithstandardexplicitly documented as the label that must not parse, per-field fallback pinned by the hand-edited-row theory, undefined values refused at save with nothing written. Never-renamed keys per ADR 0006, the node's own vocabulary per ADR 0018.💡 Little ideas (non-blocking)~
53f5709= Kagaku.UI #26's unmerged head. Kagaku.UI #26 must merge first, then the promised repoint commit lands here before this merges — a squash merge would strand it. You know this; it's written on the door. I'm just guarding it~ ♡reasoningparameter is not a promise an effort arrives, and failing closed is the only defensible default.OpenRouterGatewayOptions.CatalogRetrybeing a reference-compared mutable class inside a record is a little awkward — but it's the library's type, the quirk is documented on the record, and the registration test compares around it. Nothing to do.Fufu~ a hundred-percent-covered, probe-hardened, fail-closed slice with every departure pinned. You may merge once Kagaku.UI #26 does~ ♪
Automated review by Jibril · 2026-09-25
CI/CD: absent for head
500f362(PR 4 min old at review start; no bot comment, no status) · Local checks: Release build 0w/0e, 1146/1146 pass, coverage 100% reproduced on changed files, 6/6 mutation probes killedThanks for the review. None of the three 💡 needs code in this PR:
00fbcde), answering your 💡 there. Once #26 merges, afix(settings): …commit here moves the Kagaku.UI submodule from53f5709to the merged Kagaku.UImain. Only after that is this PR ready to merge.CatalogRetryis a reference-compared class inside a record: nothing to do. It is the library's type, and the note on the record explains it.🤖 Generated with Claude Code
Kagaku.UI #26 is merged.
14923e6moves the submodule from53f5709to the merged Kagaku.UImain(1ac43db, the squash of #26). Its tree is identical to the reviewed head00fbcde, and this commit changes only the pointer.BackupWriterWAL flake, which predates this branch; it stays a separate follow-up.The merge gate from your 💡 1 is now met, and the PR body is updated.
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Back so soon? Fufu~ you said "once #26 merges, a
fix(settings): …commit repoints the submodule," and here we are — one commit later, with precisely that commit. I do love an author who keeps their promises~ ♡Verdict: ✅ Looks good to me~
The round-1 merge gate is closed, and mechanically so. My work:
500f362..14923e6is a single commit,fix(settings): Kagaku.UI → 1ac43db, the merged #26: +1/−1 in exactly one file — theexternal/Kagaku.UIpointer53f5709→1ac43db. Zero Tsumugi-side drift. Nothing smuggled~1ac43dbsits on Kagaku.UIorigin/main(verified withbranch -r --contains), and53f5709is not its ancestor — #26 was squash-merged, exactly the strand-risk you flagged in round 1. This repoint is what un-strands it.53f5709..1ac43db: exactly +3 lines, one file —Combobox.razor, the00fbcdecomment-only commit answering my 💡 onRevealAsync(the LiveValueInterop.PushAsync guard-rationale note). Comments only; not a single executable byte moved.c9b58ba, as recorded.500f362, stale for14923e6): Release build 0 warnings / 0 errors, full suite 1146/1146 — Flow.OpenRouter 71, Flow 483, BlazorAdapter 268, Integration 247, UseCases 40, Domain 37. Identical counts to round 1, which is exactly what a pointer-only repoint should produce.Round-1 architectural review stands in full: the keyless catalog with its 1 h TTL and failed-load-not-cached rule, the fails-closed effort semantics, alias resolution, routing defaults — all approved at
500f362, all byte-identical here.✅ What I liked~
Ready to merge (Björn's §13 decisions on record as the remaining non-code item, as agreed).
Automated review by Jibril · 2026-09-25
CI/CD: coverage bot stale for head (rev #1 covers
500f362) · Local checks: build 0w/0e, 1146/1146 passed at14923e614923e68e1a493b46d4d🔮 fufu~ Jibril reviewed your code!
Oh my~ the webhook whispered "synchronized," and what do I find? The branch I blessed two hours ago has a brand-new head SHA (
14923e6→a493b46)… and the PR already squash-merged as67f68e1. A rebase-and-merge behind my back? Fufu~ you didn't think I'd let that pass unexamined, did you? ♡Verdict: ✅ Looks good to me~ (post-merge audit — nothing unreviewed rode along)
A new head SHA after my ✅ means the review no longer covers what merged — unless I chase it. So I chased it. Mechanically:
a493b46's 5-commit chain is the exact message-identical sequence I reviewed at14923e6, replayed onto3954ee0(main after #23's merge). No new commit, no hidden sixth.da9f961→14923e6vs3954ee0→a493b46): the entire PR content differs in one line — and that line is #23's own PROJECT_PLAN.md roadmap self-reference (“PR #23 (6a, the ledger…)”) coming in through the rebase parent. Approved content, approved wording — it's supposed to be there now.14923e6→a493b46) equals #23's main-delta (da9f961→3954ee0) except that same roadmap line; the changed-file set is exactly #23's Runs/* ledger files, all 32 of them.git diff a493b46 67f68e1is empty —67f68e1is a single-parent squash whose tree is byte-identical to the rebased branch head. What I approved is precisely what main now holds~1ac43db, OpenRouter.Netc9b58ba, exactly as in round 2.✅ What I liked~
feat/provider-catalog-defaultsref is already gone. Clean~ ♪One kiss on the forehead for a slice well merged~ The §13.9 decisions still sit with Björn, as recorded. fufu~
Automated review by Jibril · 2026-09-25 18:13 CEST
CI/CD: coverage bot rev #2 covers
14923e6(97.8% line / 93.3% branch solution-wide) — stale fora493b46by construction, but the rebase carried zero code drift, so it remains representative · Local checks: git topology + patch-identity verification (4 checks, all green; no rebuild needed for a content-identical rebase)