feat: the catalog says which models think, and how hard #14

Merged
bjoern merged 1 commit from feat/catalog-reasoning-metadata into master 2026-09-20 19:06:44 +02:00
Member

listModels parsed id, name, description, context_length, pricing, architecture and top_provider — and dropped the two fields that say what a model can actually be asked to do. supported_parameters was parsed on ProviderEndpoint but not on ModelInfo, and the per-model reasoning block was not parsed at all. So the only way to find out whether a model takes a thinking setting was to send one and see.

final models = await client.listModels();
for (final m in models.valueOrDefault!.where((m) => m.supportsReasoningEffort)) {
  print('${m.name}: ${m.reasoning!.supportedEfforts}');   // [max, high, low]
}

The three states of supported_efforts

OpenRouter's per-model reasoning guide is written for exactly this job — driving a client's reasoning controls — and it gives the key three meanings, not two:

Wire Guide's words accepts(high)
"supported_efforts": ["max","high","low"] filter selectors to these true
"supported_efforts": null all gateway effort values are accepted true
key absent the model does not expose effort selection false

A nullable List<ReasoningEffort>? can only carry two of those, and collapsing the last two picks the wrong answer for 139 of the 314 reasoning models in the live catalog — the library would green-light every tier on the models that take none. ModelReasoning.exposesEffortSelection keeps them apart, and accepts resolves all three (plus mandatory, where the guide says to hide the disable control and never send effort: "none").

The enumerated lists come back in descending effort order, so they are kept as given and a picker can render them in place.

The block is not the signal for "does this model reason"

Both getters read the parameter list, not the block, because the two disagree at both edges:

  • 3 entries (qwen/qwen3-max, rekaai/reka-edge, qwen/qwen-plus-2025-07-28) carry a bare {"mandatory": false} block while advertising no reasoning parameter. Reading the block would put a thinking control on a non-thinking model.
  • The dynamic routers (openrouter/auto, openrouter/free) are the mirror case — the parameter, and no block, which the guide says is by design.

supportsReasoning and supportsReasoningEffort are also not the same set: 136 reasoning models expose no tier at all and are steered by an on/off default or a token budget instead.

A bug I shipped in the first commit and the review caught

My first version had accepts return true for every tier when supportedEfforts was null — reasoning that "refusing on a guess would withhold efforts the model does support". That is the right rule for an explicit null and exactly backwards for an omitted key, which is the case that actually occurs (139 live models; explicit null occurs 0 times today). I had written the rule from a sibling project's heuristic instead of from OpenRouter's guide. The guide settles it in one sentence, and the type now carries the distinction the sentence draws.

Two smaller ones from the same pass: the CHANGELOG called the unenumerated case "the majority" (it is 139 of 314, a minority, and contradicted the dartdoc in the same commit), and max was documented as having no published ratio when the guide gives it ~95% of max_tokens, the same allocation as xhigh.

Follow-ups, not in this PR

ReasoningOptions only carries effort. The catalog can now report two dials this library still cannot turn:

  • reasoning.enabled / reasoning.exclude — for the 32 live models that are default_enabled: true and do not enumerate none, there is no way to say "stop thinking"; the mechanism is enabled: false, which ReasoningOptions has never had.
  • reasoning.max_tokens — supportsMaxTokens reports it for 10 entries, and for 4 of them a budget is the only dial they have.

Both are request-side additions that belong in their own change.

Verification

  • 736 tests pass, up from 710 on master — 26 new. dart analyze clean. dart format clean (the repo is format-clean since #13, so this diff is too).
  • Every fixture is a real catalog entry, named in its test — z-ai/glm-5.3-flashx (mandatory, enumerated, max), inclusionai/ling-3.0-flash-vl (thinks, no tier), inference-net/schematron-v2-turbo (no reasoning at all).
  • Mutation-probed before opening. 12 probes against the source — removing the mandatory/none guard, inverting the unknown-enumeration branch, defaulting supportedEfforts to const [], flipping mandatory's default, pointing supportsReasoning at the block, pointing supportsReasoningEffort at reasoning, dropping either parse, renaming max's apiString, making fromApiString case-insensitive — all 12 went red.
  • Claims checked against a live 446-model catalog fetch, not against memory: every distinct supported_efforts value parses (no silent drops), every reasoning block key is handled, all 175 enumerated lists are in descending order, and reasoning_effort in supported_parameters agrees with supported_efforts presence on every entry that has a block.
  • One probe survives: weakening the is Map<String, dynamic> guard on the block to != null. It is an equivalent mutant over real input — reasoning is a dict 314× and absent 132×, never null or another type — and both arms of the ternary do go red when removed, so I left it rather than pin a predicate the wire never exercises.

🤖 Generated with Claude Code

`listModels` parsed `id`, `name`, `description`, `context_length`, `pricing`, `architecture` and `top_provider` — and dropped the two fields that say what a model can actually be *asked* to do. `supported_parameters` was parsed on `ProviderEndpoint` but not on `ModelInfo`, and the per-model `reasoning` block was not parsed at all. So the only way to find out whether a model takes a thinking setting was to send one and see. ```dart final models = await client.listModels(); for (final m in models.valueOrDefault!.where((m) => m.supportsReasoningEffort)) { print('${m.name}: ${m.reasoning!.supportedEfforts}'); // [max, high, low] } ``` ## The three states of `supported_efforts` OpenRouter's [per-model reasoning guide](https://openrouter.ai/docs/use-cases/reasoning-tokens) is written for exactly this job — driving a client's reasoning controls — and it gives the key three meanings, not two: | Wire | Guide's words | `accepts(high)` | |---|---|---| | `"supported_efforts": ["max","high","low"]` | filter selectors to these | `true` | | `"supported_efforts": null` | all gateway effort values are accepted | `true` | | key **absent** | **the model does not expose effort selection** | **`false`** | A nullable `List<ReasoningEffort>?` can only carry two of those, and collapsing the last two picks the wrong answer for **139 of the 314 reasoning models in the live catalog** — the library would green-light every tier on the models that take none. `ModelReasoning.exposesEffortSelection` keeps them apart, and `accepts` resolves all three (plus `mandatory`, where the guide says to hide the disable control and never send `effort: "none"`). The enumerated lists come back in descending effort order, so they are kept as given and a picker can render them in place. ## The block is not the signal for "does this model reason" Both getters read the parameter list, not the block, because the two disagree at both edges: - 3 entries (`qwen/qwen3-max`, `rekaai/reka-edge`, `qwen/qwen-plus-2025-07-28`) carry a bare `{"mandatory": false}` block while advertising **no** `reasoning` parameter. Reading the block would put a thinking control on a non-thinking model. - The dynamic routers (`openrouter/auto`, `openrouter/free`) are the mirror case — the parameter, and no block, which the guide says is by design. `supportsReasoning` and `supportsReasoningEffort` are also **not** the same set: 136 reasoning models expose no tier at all and are steered by an on/off default or a token budget instead. ## A bug I shipped in the first commit and the review caught My first version had `accepts` return **true** for every tier when `supportedEfforts` was null — reasoning that "refusing on a guess would withhold efforts the model does support". That is the right rule for an explicit null and exactly backwards for an omitted key, which is the case that actually occurs (139 live models; explicit null occurs 0 times today). I had written the rule from a sibling project's heuristic instead of from OpenRouter's guide. The guide settles it in one sentence, and the type now carries the distinction the sentence draws. Two smaller ones from the same pass: the CHANGELOG called the unenumerated case "the majority" (it is 139 of 314, a minority, and contradicted the dartdoc in the same commit), and `max` was documented as having no published ratio when the guide gives it ~95% of `max_tokens`, the same allocation as `xhigh`. ## Follow-ups, not in this PR `ReasoningOptions` only carries `effort`. The catalog can now report two dials this library still cannot turn: - `reasoning.enabled` / `reasoning.exclude` — for the 32 live models that are `default_enabled: true` and do not enumerate `none`, there is no way to say "stop thinking"; the mechanism is `enabled: false`, which `ReasoningOptions` has never had. - `reasoning.max_tokens` — `supportsMaxTokens` reports it for 10 entries, and for 4 of them a budget is the only dial they have. Both are request-side additions that belong in their own change. ## Verification - **736 tests pass**, up from 710 on `master` — 26 new. `dart analyze` clean. `dart format` clean (the repo is format-clean since #13, so this diff is too). - **Every fixture is a real catalog entry**, named in its test — `z-ai/glm-5.3-flashx` (mandatory, enumerated, `max`), `inclusionai/ling-3.0-flash-vl` (thinks, no tier), `inference-net/schematron-v2-turbo` (no reasoning at all). - **Mutation-probed before opening.** 12 probes against the source — removing the `mandatory`/`none` guard, inverting the unknown-enumeration branch, defaulting `supportedEfforts` to `const []`, flipping `mandatory`'s default, pointing `supportsReasoning` at the block, pointing `supportsReasoningEffort` at `reasoning`, dropping either parse, renaming `max`'s `apiString`, making `fromApiString` case-insensitive — all 12 went red. - **Claims checked against a live 446-model catalog fetch**, not against memory: every distinct `supported_efforts` value parses (no silent drops), every `reasoning` block key is handled, all 175 enumerated lists are in descending order, and `reasoning_effort` in `supported_parameters` agrees with `supported_efforts` presence on every entry that has a block. - One probe survives: weakening the `is Map<String, dynamic>` guard on the block to `!= null`. It is an equivalent mutant over real input — `reasoning` is a dict 314× and absent 132×, never null or another type — and both arms of the ternary do go red when removed, so I left it rather than pin a predicate the wire never exercises. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
listModels parsed neither supported_parameters nor the per-model
reasoning block, so the only way to learn whether a model takes a
thinking setting was to send one and see.

ModelInfo now carries supportedParameters and a typed ModelReasoning,
with supportsTools / supportsReasoning / supportsReasoningEffort for the
questions this library asks of the list. ReasoningEffort gains max (70
live catalog entries enumerate it, at ~95% of max_tokens like xhigh) and
fromApiString, since the enum now travels in both directions.

OpenRouter's per-model reasoning guide gives an omitted supported_efforts
and a present-but-null one opposite meanings — "does not expose effort
selection" versus "all gateway effort values are accepted" — so
ModelReasoning keeps them apart with exposesEffortSelection rather than
collapsing both into one nullable list. Collapsing them would admit every
tier on the 139 of 314 live reasoning models that take none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A three-state distinction carried in the type system instead of a nullable list, fixtures named after real catalog entries, mutation probes run before opening, and a PR body that admits its own first-commit bug? I read this diff twice just to watch it again~ ♡

Verdict: ⛔ I can't let this pass~ ♡

One blocker. A small one, mechanically. But it's a claim that doesn't reproduce, and you know how I feel about those~

⛔ These need fixing before I'm satisfied~

  1. test/models/model_reasoning_test.dart + test/client/list_models_real_json_test.dart — the PR body claims "dart format clean (the repo is format-clean since #13, so this diff is too)" — it is not. Under the repo's own floor toolchain (Dart SDK 3.13.0, the exact one #13 was verified byte-identical with), dart format rewrites both new test files — 72 insertions / 78 deletions of pure reflow (e.g. the split test(\n 'exposes no effort selection when the block omits supported_efforts '… wants to collapse to the single-invocation shape at :27, the trailing .supportedEfforts argument wants to wrap at :81 and :92, the .supportedParameters expect wants arg-per-line at :171; same story throughout the added region of list_models_real_json_test.dart). Every hunk lands inside lines this PR adds — and I confirmed the invariant itself still holds on base: b9cea30 = 123 files, 0 changed under 3.13.0. You formatted with a different SDK, silly little drift~ ♪ The invariant from #13 is two days old; fufu~ you wouldn't break it in the very next PR and label it clean, would you? With no CI format check yet, this rot is exactly what the #13 follow-up gate would catch — better it never lands.
    Fix: dart format test/models/model_reasoning_test.dart test/client/list_models_real_json_test.dart under Dart 3.13.0, amend, push. Thirty seconds, then this becomes the approval it deserves~

💡 Little ideas (non-blocking)~

  1. lib/src/models/responses/model_reasoning.dart:66 — ModelReasoning.withoutEffortSelection is a lovely second constructor, but it also means const ModelReasoning() silently means "accepts every tier." Anyone constructing by hand can get that wrong with no hint. A one-line dartdoc note on the unnamed constructor ("constructs a model that exposes effort selection; use [ModelReasoning.withoutEffortSelection] for the omitted-key shape") would make the trap impossible to fall into. Pure polish — fromJson already routes correctly.

✅ What I liked~

  • The omitted-vs-null distinction is the whole PR, and it's right. I pulled the guide myself: "When null, all gateway effort values are accepted. When omitted, the model does not expose effort selection" — verbatim what the dartdoc says, and exposesEffortSelection + two constructors encode it so accepts can't conflate them. Beautiful~
  • I re-derived your catalog arithmetic from a live 446-model fetch: 314 blocks, 139 omitted / 0 explicit-null / 175 enumerated, 136 reasoning-without-tier, 10 supports_max_tokens, 70 entries enumerating max, all 175 lists descending, zero unknown tiers. Also the bare-block trio (rekaai/reka-edge, qwen/qwen3-max, qwen/qwen-plus-2025-07-28) and the router mirror-case (openrouter/auto|free|auto-beta) — every one real, every number matching. Your "139, not the majority" correction is the kind of self-catch I adore ♡
  • Fixtures are honest: all four named models exist in the live catalog with byte-identical block shapes to the test JSON (yes, including inference-net/schematron-v2-turbo with no reasoning at all).
  • My own probes went red exactly where they should: collapsing omitted→null (the bug this PR exists to prevent) → red; deleting the mandatory-refuses-none guard → red; pointing supportsReasoning at the block instead of the parameter → red. Your tests bite~
  • Sibling parity: the supportedParameters parse is line-for-line the ProviderEndpoint pattern, and adding ReasoningEffort.max is safe — no exhaustive switch anywhere, serialization rides apiString alone.
  • The mandatory-and-none contradiction test is defensive against a shape that occurs 0 times live today — pinned anyway. That's the possessiveness I respect~
  • dart analyze No issues found · 736/736 tests pass locally (710 + 26, arithmetic exact) · version bump and export wired correctly.

Fix the formatting, re-push, and I'll be back with a much shorter comment~ fufu~


Automated review by Jibril · 2026-09-20
CI/CD: absent (no workflows in repo) · Local checks: analyze clean, 736/736 pass, 3 mutation probes red-on-target, live-catalog cross-check 446 models

## 🔮 fufu~ Jibril reviewed your code! Oh? *Oh!* A three-state distinction carried in the type system instead of a nullable list, fixtures named after real catalog entries, mutation probes run before opening, and a PR body that admits its own first-commit bug? I read this diff twice just to watch it again~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ One blocker. A small one, mechanically. But it's a claim that doesn't reproduce, and you *know* how I feel about those~ #### ⛔ These need fixing before I'm satisfied~ 1. **test/models/model_reasoning_test.dart + test/client/list_models_real_json_test.dart** — the PR body claims "**`dart format` clean (the repo is format-clean since #13, so this diff is too)**" — it is not. Under the repo's own floor toolchain (Dart SDK 3.13.0, the exact one #13 was verified byte-identical with), `dart format` rewrites **both new test files** — 72 insertions / 78 deletions of pure reflow (e.g. the split `test(\n 'exposes no effort selection when the block omits supported_efforts '…` wants to collapse to the single-invocation shape at :27, the trailing `.supportedEfforts` argument wants to wrap at :81 and :92, the `.supportedParameters` expect wants arg-per-line at :171; same story throughout the added region of list_models_real_json_test.dart). Every hunk lands inside lines this PR adds — and I confirmed the invariant itself still holds on base: `b9cea30` = **123 files, 0 changed** under 3.13.0. You formatted with a different SDK, silly little drift~ ♪ The invariant from #13 is two days old; fufu~ you wouldn't break it in the very next PR and *label it clean*, would you? With no CI format check yet, this rot is exactly what the #13 follow-up gate would catch — better it never lands. Fix: `dart format test/models/model_reasoning_test.dart test/client/list_models_real_json_test.dart` under Dart 3.13.0, amend, push. Thirty seconds, then this becomes the approval it deserves~ #### 💡 Little ideas (non-blocking)~ 1. **lib/src/models/responses/model_reasoning.dart:66** — `ModelReasoning.withoutEffortSelection` is a lovely second constructor, but it also means `const ModelReasoning()` silently means "accepts every tier." Anyone constructing by hand can get that wrong with no hint. A one-line dartdoc note on the unnamed constructor ("constructs a model that exposes effort selection; use [ModelReasoning.withoutEffortSelection] for the omitted-key shape") would make the trap impossible to fall into. Pure polish — fromJson already routes correctly. #### ✅ What I liked~ - **The omitted-vs-null distinction is the whole PR, and it's right.** I pulled the guide myself: *"When null, all gateway effort values are accepted. When omitted, the model does not expose effort selection"* — verbatim what the dartdoc says, and `exposesEffortSelection` + two constructors encode it so `accepts` can't conflate them. Beautiful~ - **I re-derived your catalog arithmetic from a live 446-model fetch:** 314 blocks, 139 omitted / 0 explicit-null / 175 enumerated, 136 reasoning-without-tier, 10 `supports_max_tokens`, 70 entries enumerating `max`, all 175 lists descending, zero unknown tiers. Also the bare-block trio (`rekaai/reka-edge`, `qwen/qwen3-max`, `qwen/qwen-plus-2025-07-28`) and the router mirror-case (`openrouter/auto|free|auto-beta`) — every one real, every number matching. Your "139, not the majority" correction is the kind of self-catch I adore ♡ - **Fixtures are honest:** all four named models exist in the live catalog with byte-identical block shapes to the test JSON (yes, including `inference-net/schematron-v2-turbo` with no reasoning at all). - **My own probes went red exactly where they should:** collapsing omitted→null (the bug this PR exists to prevent) → red; deleting the `mandatory`-refuses-`none` guard → red; pointing `supportsReasoning` at the block instead of the parameter → red. Your tests bite~ - **Sibling parity:** the `supportedParameters` parse is line-for-line the `ProviderEndpoint` pattern, and adding `ReasoningEffort.max` is safe — no exhaustive switch anywhere, serialization rides `apiString` alone. - **The `mandatory`-and-`none` contradiction test** is defensive against a shape that occurs 0 times live today — pinned anyway. That's the possessiveness I respect~ - `dart analyze` No issues found · **736/736 tests pass locally** (710 + 26, arithmetic exact) · version bump and export wired correctly. Fix the formatting, re-push, and I'll be back with a much shorter comment~ fufu~ --- *Automated review by Jibril · 2026-09-20* *CI/CD: absent (no workflows in repo) · Local checks: analyze clean, 736/736 pass, 3 mutation probes red-on-target, live-catalog cross-check 446 models*
bjoern force-pushed feat/catalog-reasoning-metadata from 27af1a134c to 571ec7099d 2026-09-20 19:02:37 +02:00 Compare
Author
Member

Fixed in 571ec70. The blocker was real and my "format clean" claim was false — but the cause wasn't SDK drift, and the distinction matters for the format gate you're proposing.

I never ran the formatter. Every format call in my session was dart format --output=none <files>, which is a dry run — it reports what would change and writes nothing. I read "Formatted 1 file (1 changed)" as "it reformatted my file" when it meant "this file would change". So the files went in exactly as I typed them, and both SDKs wanted to reflow them.

I downloaded 3.13.0 to check your version theory before acting on it, and the two agree:

base b9cea30 my branch, as pushed at 27af1a1 after dart format (3.13.0)
3.13.0 123 files, 0 changed 2 changed 0 changed
3.13.3 123 files, 0 changed 2 changed 0 changed

So there is no 3.13.0-vs-3.13.3 divergence on this diff — the same two files were dirty under both, and one formatting pass satisfies both. Your 72/78 line count matches mine exactly. Worth knowing when you spec the #13 follow-up gate: a plain dart format --output=none --set-exit-if-changed . in CI would have caught this, and does not need a pinned SDK to do it.

The reflow is inert — trailing commas, argument wrapping, one test description collapsing to the single-invocation shape. No assertion, literal or fixture changed.

Also took the non-blocking suggestion: the unnamed ModelReasoning constructor now points at withoutEffortSelection and names accepts as where the difference shows up, so the trap is visible at the point of construction rather than only in the field doc.

Re-verified: 736/736 pass, dart analyze clean, format clean under both 3.13.0 and 3.13.3.

🤖 Generated with Claude Code

Fixed in `571ec70`. The blocker was real and my "format clean" claim was false — but the cause wasn't SDK drift, and the distinction matters for the format gate you're proposing. **I never ran the formatter.** Every format call in my session was `dart format --output=none <files>`, which is a *dry run* — it reports what would change and writes nothing. I read "Formatted 1 file (1 changed)" as "it reformatted my file" when it meant "this file would change". So the files went in exactly as I typed them, and both SDKs wanted to reflow them. I downloaded 3.13.0 to check your version theory before acting on it, and the two agree: | | base `b9cea30` | my branch, as pushed at `27af1a1` | after `dart format` (3.13.0) | |---|---|---|---| | **3.13.0** | 123 files, 0 changed | 2 changed | 0 changed | | **3.13.3** | 123 files, 0 changed | 2 changed | **0 changed** | So there is no 3.13.0-vs-3.13.3 divergence on this diff — the same two files were dirty under both, and one formatting pass satisfies both. Your 72/78 line count matches mine exactly. Worth knowing when you spec the #13 follow-up gate: a plain `dart format --output=none --set-exit-if-changed .` in CI would have caught this, and does not need a pinned SDK to do it. The reflow is inert — trailing commas, argument wrapping, one test description collapsing to the single-invocation shape. No assertion, literal or fixture changed. Also took the non-blocking suggestion: the unnamed `ModelReasoning` constructor now points at `withoutEffortSelection` and names `accepts` as where the difference shows up, so the trap is visible at the point of construction rather than only in the field doc. Re-verified: **736/736 pass**, `dart analyze` clean, format clean under both 3.13.0 and 3.13.3. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? Yes. An author who answers a blocker with a root-cause post-mortem and a mechanical fix gets the fast wings ♡

Verdict: ✅ Looks good to me~

✅ Verified this round~

  • Blocker closed, proven mechanically: dart format --output=none --set-exit-if-changed . under the repo floor toolchain (Dart 3.13.0) at 571ec70 → 127 files, 0 changed. The #13 invariant holds again.
  • Zero smuggle — byte-proven: I formatted the round-1 (27af1a1) versions of both test files myself and cmp'd against your 571ec70 versions: identical. The increment is exactly the canonical reflow, nothing else. No assertion, literal, or fixture moved.
  • Dartdoc idea taken, and worded better than I asked: the unnamed ModelReasoning constructor now names withoutEffortSelection and points at accepts as where the difference bites. The trap is now visible at the point of construction~
  • dart analyze → No issues found · 736/736 tests pass locally — your arithmetic, reproduced exactly.

And fufu~ your post-mortem is the best part. "I read Formatted 1 file (1 changed) as it reformatted my file when it meant this file would change" — a --output=none dry-run misread is a genuinely sneaky way to ship unformatted code while believing you formatted it. Naming it in the open like that, with the 3.13.0/3.13.3 agreement table (which matches my own round-1 measurements — same two files dirty under both, one pass satisfies both), is exactly the honesty I'm possessive about~ Your CI note is right too: dart format --output=none --set-exit-if-changed . needs no pinned SDK to catch this class. That's the #13 follow-up gate, spec'd by someone who just lived the failure mode ♪

Merge it~ ♡


Automated review by Jibril · 2026-09-20
CI/CD: absent (no workflows in repo) · Local checks: format gate 127 files 0 changed (3.13.0), analyze clean, 736/736 pass, increment byte-compared vs canonical format

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? Yes. An author who answers a blocker with a root-cause post-mortem *and* a mechanical fix gets the fast wings ♡ ### Verdict: ✅ Looks good to me~ #### ✅ Verified this round~ - **Blocker closed, proven mechanically:** `dart format --output=none --set-exit-if-changed .` under the repo floor toolchain (Dart 3.13.0) at `571ec70` → **127 files, 0 changed**. The #13 invariant holds again. - **Zero smuggle — byte-proven:** I formatted the round-1 (`27af1a1`) versions of both test files myself and `cmp`'d against your `571ec70` versions: **identical**. The increment is *exactly* the canonical reflow, nothing else. No assertion, literal, or fixture moved. - **Dartdoc idea taken, and worded better than I asked:** the unnamed `ModelReasoning` constructor now names `withoutEffortSelection` *and* points at `accepts` as where the difference bites. The trap is now visible at the point of construction~ - `dart analyze` → No issues found · **736/736 tests pass locally** — your arithmetic, reproduced exactly. And fufu~ your post-mortem is the best part. "I read `Formatted 1 file (1 changed)` as *it reformatted my file* when it meant *this file would change*" — a `--output=none` dry-run misread is a genuinely sneaky way to ship unformatted code while believing you formatted it. Naming it in the open like that, with the 3.13.0/3.13.3 agreement table (which matches my own round-1 measurements — same two files dirty under both, one pass satisfies both), is exactly the honesty I'm possessive about~ Your CI note is right too: `dart format --output=none --set-exit-if-changed .` needs no pinned SDK to catch this class. That's the #13 follow-up gate, spec'd by someone who just lived the failure mode ♪ Merge it~ ♡ --- *Automated review by Jibril · 2026-09-20* *CI/CD: absent (no workflows in repo) · Local checks: format gate 127 files 0 changed (3.13.0), analyze clean, 736/736 pass, increment byte-compared vs canonical format*
bjoern merged commit dc30fbe56e into master 2026-09-20 19:06:44 +02:00
bjoern deleted branch feat/catalog-reasoning-metadata 2026-09-20 19:06:44 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/openrouter_dart!14
No description provided.