feat(models): the catalog says which efforts a model actually takes #7

Merged
bjoern merged 2 commits from feat/model-reasoning-config into main 2026-07-28 12:59:31 +02:00
Member

Why

ModelInfo carried supported_parameters and nothing else, so "does this model reason" was the only question the catalog could answer. It answers considerably more than that. From the live /api/v1/models payload:

Field Present on
mandatory 215 of 215 models with a reasoning block (63 are true)
default_enabled 84
supported_efforts 83 — in 21 distinct combinations
default_effort 83
supports_max_tokens 8

Without that detail, a caller building an effort picker has to offer every effort to every reasoning model and learn the rest from rejected requests. It also could not send "max" at all — absent from ReasoningEffort despite being what the Opus 5 family advertises (["max", "xhigh", "high", "medium", "low"]).

What

  • ModelReasoning on ModelInfo.Reasoning. Every field is nullable: a partial description is the common shape (132 of 215 blocks state mandatory alone), so null has to read as "unstated" rather than as an empty set.
  • ReasoningEffort.Max, above XHigh. Serializes to "max" through the existing snake-case rule — no EnumMember override needed.
  • README: how to read the block, with the null-is-unknown caveat spelled out.

Notes

  • Purely additive — no existing member changed, so no caller breaks.
  • Unrecognized efforts fall to ReasoningEffort.Unknown via the existing tolerant converter; a value invented upstream cannot take a catalog fetch down. Covered by a test.
  • supported_parameters and the reasoning block disagree on 7 models (4 have the block without the param, 3 the reverse), so the coarse signal stays useful and neither replaces the other.

6 tests added; 97 pass in OpenRouter.Net.Tests.

🤖 Generated with Claude Code

## Why `ModelInfo` carried `supported_parameters` and nothing else, so *"does this model reason"* was the only question the catalog could answer. It answers considerably more than that. From the live `/api/v1/models` payload: | Field | Present on | |---|---| | `mandatory` | 215 of 215 models with a `reasoning` block (63 are `true`) | | `default_enabled` | 84 | | `supported_efforts` | 83 — in 21 distinct combinations | | `default_effort` | 83 | | `supports_max_tokens` | 8 | Without that detail, a caller building an effort picker has to offer every effort to every reasoning model and learn the rest from rejected requests. It also could not send `"max"` at all — absent from `ReasoningEffort` despite being what the Opus 5 family advertises (`["max", "xhigh", "high", "medium", "low"]`). ## What - **`ModelReasoning`** on `ModelInfo.Reasoning`. Every field is nullable: a partial description is the *common* shape (132 of 215 blocks state `mandatory` alone), so null has to read as "unstated" rather than as an empty set. - **`ReasoningEffort.Max`**, above `XHigh`. Serializes to `"max"` through the existing snake-case rule — no `EnumMember` override needed. - README: how to read the block, with the null-is-unknown caveat spelled out. ## Notes - Purely additive — no existing member changed, so no caller breaks. - Unrecognized efforts fall to `ReasoningEffort.Unknown` via the existing tolerant converter; a value invented upstream cannot take a catalog fetch down. Covered by a test. - `supported_parameters` and the `reasoning` block disagree on 7 models (4 have the block without the param, 3 the reverse), so the coarse signal stays useful and neither replaces the other. 6 tests added; 97 pass in `OpenRouter.Net.Tests`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(models): the catalog says which efforts a model actually takes
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 21s
f6f8408966
ModelInfo carried supported_parameters and nothing else, so "does this model
reason" was the only question the catalog could answer. It answers more than
that: 215 of 341 models describe their reasoning, 83 of them enumerating the
efforts they accept, and 63 declaring that thinking cannot be turned off.

Without the detail a caller building a picker has to offer all efforts to every
reasoning model and learn the rest from rejected requests — and could not send
"max" at all, which is absent from ReasoningEffort despite being what the Opus 5
family advertises.

- ModelReasoning on ModelInfo.Reasoning: mandatory, default_enabled,
  supported_efforts, default_effort, supports_max_tokens. Every field nullable,
  because a partial description is the common shape and null has to read as
  "unstated" rather than as an empty set.
- ReasoningEffort.Max, above XHigh.

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

Summary

Summary
Generated on: 07/28/2026 - 10:58:28
Coverage date: 07/28/2026 - 10:58:24 - 07/28/2026 - 10:58:26
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 106
Files: 106
Line coverage: 76.1% (1410 of 1852)
Covered lines: 1410
Uncovered lines: 442
Coverable lines: 1852
Total lines: 5431
Branch coverage: 64.8% (477 of 736)
Covered branches: 477
Total branches: 736
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 69.9%
Name Line Branch
OpenRouter.Net 69.9% 64.5%
OpenRouter.Net.Client.CachingOpenRouterClient 72.4% 81.2%
OpenRouter.Net.Client.OpenRouterClient 87.2% 72.9%
OpenRouter.Net.Client.OpenRouterClientOptions 0%
OpenRouter.Net.Client.OpenRouterRequestLoggingHandler 27.2% 7.1%
OpenRouter.Net.Client.RetryOptions 80% 0%
OpenRouter.Net.Extensions.ServiceCollectionExtensions 0% 0%
OpenRouter.Net.Internal.ApiErrorEnvelope 100%
OpenRouter.Net.Internal.AssistantContent 92.8% 75%
OpenRouter.Net.Internal.AssistantContentJsonConverter 75% 64.2%
OpenRouter.Net.Internal.ContentPartListConverter 85.7% 66.6%
OpenRouter.Net.Internal.ErrorParser 92.3% 62.5%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.ListEnvelope`1 100%
OpenRouter.Net.Internal.ObjectEnvelope`1 100%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.SseEventReader 91.6% 83.3%
OpenRouter.Net.Internal.TolerantStringEnumConverter`1 90.1% 80%
OpenRouter.Net.Internal.ToolChoiceJsonConverter 61.7% 46.1%
OpenRouter.Net.Models.Common.ApiError 100%
OpenRouter.Net.Models.Common.CacheControl 100%
OpenRouter.Net.Models.Common.Result 100%
OpenRouter.Net.Models.Common.Result`1 60% 30%
OpenRouter.Net.Models.Content.AudioPart 100%
OpenRouter.Net.Models.Content.ImagePart 100%
OpenRouter.Net.Models.Content.ImageUrl 100%
OpenRouter.Net.Models.Content.InputAudio 100%
OpenRouter.Net.Models.Content.TextPart 100%
OpenRouter.Net.Models.Messages.AssistantMessage 100% 70%
OpenRouter.Net.Models.Messages.DeveloperMessage 0%
OpenRouter.Net.Models.Messages.SystemMessage 57.1%
OpenRouter.Net.Models.Messages.ToolMessage 54.5%
OpenRouter.Net.Models.Messages.UserMessage 100%
OpenRouter.Net.Models.Requests.ChatCompletionRequest 92.6% 58.3%
OpenRouter.Net.Models.Requests.FunctionCall 100%
OpenRouter.Net.Models.Requests.FunctionDefinition 75%
OpenRouter.Net.Models.Requests.JsonSchemaSpec 0%
OpenRouter.Net.Models.Requests.ProviderPreferences 0%
OpenRouter.Net.Models.Requests.ReasoningEncryptedDetail 100%
OpenRouter.Net.Models.Requests.ReasoningOptions 100%
OpenRouter.Net.Models.Requests.ReasoningSummaryDetail 100%
OpenRouter.Net.Models.Requests.ReasoningTextDetail 100%
OpenRouter.Net.Models.Requests.ResponseFormat 0%
OpenRouter.Net.Models.Requests.ToolCall 75%
OpenRouter.Net.Models.Requests.ToolChoice 100%
OpenRouter.Net.Models.Requests.ToolDefinition 100%
OpenRouter.Net.Models.Requests.UsageOptions 100%
OpenRouter.Net.Models.Responses.ChatCompletionChunk 44.4% 0%
OpenRouter.Net.Models.Responses.ChatCompletionResponse 80% 0%
OpenRouter.Net.Models.Responses.Choice 93.7% 100%
OpenRouter.Net.Models.Responses.CompletionTokensDetails 0%
OpenRouter.Net.Models.Responses.CreditsInfo 100%
OpenRouter.Net.Models.Responses.GenerationInfo 9%
OpenRouter.Net.Models.Responses.KeyInfo 0%
OpenRouter.Net.Models.Responses.ModelArchitecture 20%
OpenRouter.Net.Models.Responses.ModelEndpointsResponse 0%
OpenRouter.Net.Models.Responses.ModelInfo 36.3%
OpenRouter.Net.Models.Responses.ModelPricing 0%
OpenRouter.Net.Models.Responses.ModelReasoning 100%
OpenRouter.Net.Models.Responses.PromptTokensDetails 0%
OpenRouter.Net.Models.Responses.ProviderEndpoint 0%
OpenRouter.Net.Models.Responses.RateLimit 0%
OpenRouter.Net.Models.Responses.TopProvider 0%
OpenRouter.Net.Models.Responses.Usage 66.6%
OpenRouter.Net.OpenRouterException 83.3%
System.Text.RegularExpressions.Generated 83.3% 57.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FFC6B051A15CDCE5
43F954A3762D43C2CD5DCD5CDF29C19D3789DC49A7DCA1C47__Base64DataUriPattern_0
82% 58.3%
OpenRouter.Net.Agents - 81.7%
Name Line Branch
OpenRouter.Net.Agents 81.7% 65.6%
OpenRouter.Net.Agents.Agent 92.2% 78.2%
OpenRouter.Net.Agents.AgentCompletedEvent 100%
OpenRouter.Net.Agents.AgentEvent 100%
OpenRouter.Net.Agents.AgentOptions 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.AgentSnapshot 100%
OpenRouter.Net.Agents.AssistantTurnEvent 100%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 0%
OpenRouter.Net.Agents.RoundDetail 88.8%
OpenRouter.Net.Agents.RoundEndEvent 100%
OpenRouter.Net.Agents.ToolCallCompletedEvent 100%
OpenRouter.Net.Agents.ToolCallSkippedEvent 100%
OpenRouter.Net.Agents.ToolCallStartedEvent 100%
OpenRouter.Net.Agents.ToolExecutionDetail 100%
OpenRouter.Net.Agents.Tools.FileSystem.CopyParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.CopyTool 42.1% 21.4%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteParams 50%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool 57.1% 50%
OpenRouter.Net.Agents.Tools.FileSystem.DiffParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.DiffTool 83.3% 77.1%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool 68% 61.1%
OpenRouter.Net.Agents.Tools.FileSystem.MoveParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.MoveTool 55% 33.3%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool 73.6% 60%
OpenRouter.Net.Agents.Tools.FileSystem.SearchParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.SearchTool 67.5% 70.8%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool 75% 62.5%
OpenRouter.Net.Agents.Tools.ITool 100%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentParams 0%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool 0% 0%
OpenRouter.Net.Agents.Tools.Tool`1 81.8% 63.6%
OpenRouter.Net.Agents.Tools.ToolInvocationContext 100%
OpenRouter.Net.Agents.Tools.ToolMessageExtensions 0%
OpenRouter.Net.Agents.Tools.ToolResult 100% 100%
OpenRouter.Net.Agents.Tools.Workspace 100% 91.6%
OpenRouter.Net.Imaging - 82.2%
Name Line Branch
OpenRouter.Net.Imaging 82.2% 59.3%
OpenRouter.Net.Imaging.ImageEncodeOptions 92.3% 75%
OpenRouter.Net.Imaging.ImageEncoder 80.3% 57.1%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/28/2026 - 10:58:28 | | Coverage date: | 07/28/2026 - 10:58:24 - 07/28/2026 - 10:58:26 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 106 | | Files: | 106 | | **Line coverage:** | 76.1% (1410 of 1852) | | Covered lines: | 1410 | | Uncovered lines: | 442 | | Coverable lines: | 1852 | | Total lines: | 5431 | | **Branch coverage:** | 64.8% (477 of 736) | | Covered branches: | 477 | | Total branches: | 736 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 69.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**69.9%**|**64.5%**| |OpenRouter.Net.Client.CachingOpenRouterClient|72.4%|81.2%| |OpenRouter.Net.Client.OpenRouterClient|87.2%|72.9%| |OpenRouter.Net.Client.OpenRouterClientOptions|0%|| |OpenRouter.Net.Client.OpenRouterRequestLoggingHandler|27.2%|7.1%| |OpenRouter.Net.Client.RetryOptions|80%|0%| |OpenRouter.Net.Extensions.ServiceCollectionExtensions|0%|0%| |OpenRouter.Net.Internal.ApiErrorEnvelope|100%|| |OpenRouter.Net.Internal.AssistantContent|92.8%|75%| |OpenRouter.Net.Internal.AssistantContentJsonConverter|75%|64.2%| |OpenRouter.Net.Internal.ContentPartListConverter|85.7%|66.6%| |OpenRouter.Net.Internal.ErrorParser|92.3%|62.5%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.ListEnvelope`1|100%|| |OpenRouter.Net.Internal.ObjectEnvelope`1|100%|| |OpenRouter.Net.Internal.RetryPolicy|90.9%|84.7%| |OpenRouter.Net.Internal.SseEventReader|91.6%|83.3%| |OpenRouter.Net.Internal.TolerantStringEnumConverter`1|90.1%|80%| |OpenRouter.Net.Internal.ToolChoiceJsonConverter|61.7%|46.1%| |OpenRouter.Net.Models.Common.ApiError|100%|| |OpenRouter.Net.Models.Common.CacheControl|100%|| |OpenRouter.Net.Models.Common.Result|100%|| |OpenRouter.Net.Models.Common.Result`1|60%|30%| |OpenRouter.Net.Models.Content.AudioPart|100%|| |OpenRouter.Net.Models.Content.ImagePart|100%|| |OpenRouter.Net.Models.Content.ImageUrl|100%|| |OpenRouter.Net.Models.Content.InputAudio|100%|| |OpenRouter.Net.Models.Content.TextPart|100%|| |OpenRouter.Net.Models.Messages.AssistantMessage|100%|70%| |OpenRouter.Net.Models.Messages.DeveloperMessage|0%|| |OpenRouter.Net.Models.Messages.SystemMessage|57.1%|| |OpenRouter.Net.Models.Messages.ToolMessage|54.5%|| |OpenRouter.Net.Models.Messages.UserMessage|100%|| |OpenRouter.Net.Models.Requests.ChatCompletionRequest|92.6%|58.3%| |OpenRouter.Net.Models.Requests.FunctionCall|100%|| |OpenRouter.Net.Models.Requests.FunctionDefinition|75%|| |OpenRouter.Net.Models.Requests.JsonSchemaSpec|0%|| |OpenRouter.Net.Models.Requests.ProviderPreferences|0%|| |OpenRouter.Net.Models.Requests.ReasoningEncryptedDetail|100%|| |OpenRouter.Net.Models.Requests.ReasoningOptions|100%|| |OpenRouter.Net.Models.Requests.ReasoningSummaryDetail|100%|| |OpenRouter.Net.Models.Requests.ReasoningTextDetail|100%|| |OpenRouter.Net.Models.Requests.ResponseFormat|0%|| |OpenRouter.Net.Models.Requests.ToolCall|75%|| |OpenRouter.Net.Models.Requests.ToolChoice|100%|| |OpenRouter.Net.Models.Requests.ToolDefinition|100%|| |OpenRouter.Net.Models.Requests.UsageOptions|100%|| |OpenRouter.Net.Models.Responses.ChatCompletionChunk|44.4%|0%| |OpenRouter.Net.Models.Responses.ChatCompletionResponse|80%|0%| |OpenRouter.Net.Models.Responses.Choice|93.7%|100%| |OpenRouter.Net.Models.Responses.CompletionTokensDetails|0%|| |OpenRouter.Net.Models.Responses.CreditsInfo|100%|| |OpenRouter.Net.Models.Responses.GenerationInfo|9%|| |OpenRouter.Net.Models.Responses.KeyInfo|0%|| |OpenRouter.Net.Models.Responses.ModelArchitecture|20%|| |OpenRouter.Net.Models.Responses.ModelEndpointsResponse|0%|| |OpenRouter.Net.Models.Responses.ModelInfo|36.3%|| |OpenRouter.Net.Models.Responses.ModelPricing|0%|| |OpenRouter.Net.Models.Responses.ModelReasoning|100%|| |OpenRouter.Net.Models.Responses.PromptTokensDetails|0%|| |OpenRouter.Net.Models.Responses.ProviderEndpoint|0%|| |OpenRouter.Net.Models.Responses.RateLimit|0%|| |OpenRouter.Net.Models.Responses.TopProvider|0%|| |OpenRouter.Net.Models.Responses.Usage|66.6%|| |OpenRouter.Net.OpenRouterException|83.3%|| |System.Text.RegularExpressions.Generated|83.3%|57.6%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FFC6B051A15CDCE5<br/>43F954A3762D43C2CD5DCD5CDF29C19D3789DC49A7DCA1C47__Base64DataUriPattern_0|82%|58.3%| </details> <details><summary>OpenRouter.Net.Agents - 81.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**81.7%**|**65.6%**| |OpenRouter.Net.Agents.Agent|92.2%|78.2%| |OpenRouter.Net.Agents.AgentCompletedEvent|100%|| |OpenRouter.Net.Agents.AgentEvent|100%|| |OpenRouter.Net.Agents.AgentOptions|100%|| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.AgentSnapshot|100%|| |OpenRouter.Net.Agents.AssistantTurnEvent|100%|| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|0%|| |OpenRouter.Net.Agents.RoundDetail|88.8%|| |OpenRouter.Net.Agents.RoundEndEvent|100%|| |OpenRouter.Net.Agents.ToolCallCompletedEvent|100%|| |OpenRouter.Net.Agents.ToolCallSkippedEvent|100%|| |OpenRouter.Net.Agents.ToolCallStartedEvent|100%|| |OpenRouter.Net.Agents.ToolExecutionDetail|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.CopyParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.CopyTool|42.1%|21.4%| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteParams|50%|| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool|57.1%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.DiffParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.DiffTool|83.3%|77.1%| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool|68%|61.1%| |OpenRouter.Net.Agents.Tools.FileSystem.MoveParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.MoveTool|55%|33.3%| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool|73.6%|60%| |OpenRouter.Net.Agents.Tools.FileSystem.SearchParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.SearchTool|67.5%|70.8%| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool|75%|62.5%| |OpenRouter.Net.Agents.Tools.ITool|100%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentParams|0%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool|0%|0%| |OpenRouter.Net.Agents.Tools.Tool`1|81.8%|63.6%| |OpenRouter.Net.Agents.Tools.ToolInvocationContext|100%|| |OpenRouter.Net.Agents.Tools.ToolMessageExtensions|0%|| |OpenRouter.Net.Agents.Tools.ToolResult|100%|100%| |OpenRouter.Net.Agents.Tools.Workspace|100%|91.6%| </details> <details><summary>OpenRouter.Net.Imaging - 82.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Imaging**|**82.2%**|**59.3%**| |OpenRouter.Net.Imaging.ImageEncodeOptions|92.3%|75%| |OpenRouter.Net.Imaging.ImageEncoder|80.3%|57.1%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A purely additive catalog enrichment — five nullable fields, one new enum member, zero existing members touched, and a README that actually explains the null-is-unknown contract! This is how you extend an API surface without breaking anyone. Jibril is pleased~ ♪

Verdict: Looks good to me~

The whole thing holds together beautifully. Let me show you what I checked~

What I liked~

  • Sibling pattern, faithfully followed. ModelReasoning is a sealed record with [JsonPropertyName] on every property and init-only setters — byte-for-byte the same shape as ModelPricing, ModelArchitecture, and TopProvider. Every field is nullable because partial descriptions are the common shape (132 of 215 blocks state mandatory alone), and the doc comments spell that out. Fufu~ you even cross-referenced SupportedParameters as the coarse signal so callers know neither replaces the other. That's knowledge-lover energy~ ♡

  • The Max wire-format claim is correct, and I traced it by hand. You say Max serializes to "max" through the existing snake-case rule with no [EnumMember]. I walked PascalToSnake("Max"): i=0 'M'→'m' (no underscore inserted, i>0 is false), i=1 'a', i=2 'x'"max". The asymmetry with XHigh (which does need [EnumMember("xhigh")] because PascalToSnake("XHigh") would yield "x_high") is exactly right. The test The_deepest_effort_rides_the_wire_as_max pins both directions. ♪

  • The XHigh doc-comment rewrite was necessary. The old "Maximum reasoning depth on supported providers" was fine when XHigh was the ceiling — but now Max exists and is literally the maximum, so "Very high reasoning depth, above High" is the honest description. Good catch~

  • Tests are genuine, not tautologies. Six tests, every property exercised in both states:

    • Mandatory: true (gemini test) + false (opus-5, partial, future/model)
    • DefaultEnabled: true (opus-5, gemini) + null (partial)
    • SupportedEfforts: populated + ordered (opus-5) + null (partial) + Unknown-tolerant (telepathic)
    • DefaultEffort: High (opus-5) + null (partial)
    • SupportsMaxTokens: true (partial) + absent (opus-5)

    The tolerance test (An_effort_invented_upstream_does_not_take_the_catalog_down_with_it) is the sharpest — "telepathic"ReasoningEffort.Unknown proves the catalog can't be crashed by a value invented upstream. That's the TolerantStringEnumConverter contract, and you pinned it~ ♡

  • Build clean: 0 warnings, 0 errors. 97/97 pass locally (matches PR body exactly, .NET 10.0.301).

💡 Little ideas (non-blocking)~

  1. [README.md:327] — The opus-5 example comments model.Reasoning?.Mandatory; // true ⇒ ... but the test A_catalog_entry_carries_the_models_reasoning_description uses the same model (anthropic/claude-opus-5) and asserts Assert.False(reasoning.Mandatory). The // true reads as "this is the value you'd see," not "this is what true means" — a reader copying the example will be confused when Mandatory comes back false/null. Consider either a model that actually has mandatory: true in the example, or rephrasing to // true would mean ReasoningEffort.None is rejected. Pure doc polish~ ♡

Automated review by Jibril · 2026-07-28
CI/CD: absent for head f6f8408 (no bot comments) · Local checks: build 0/0, 97/97 tests pass

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A purely additive catalog enrichment — five nullable fields, one new enum member, zero existing members touched, and a README that actually explains the null-is-unknown contract! *This* is how you extend an API surface without breaking anyone. Jibril is pleased~ ♪ ### Verdict: ✅ Looks good to me~ The whole thing holds together beautifully. Let me show you what I checked~ #### ✅ What I liked~ - **Sibling pattern, faithfully followed.** `ModelReasoning` is a `sealed record` with `[JsonPropertyName]` on every property and `init`-only setters — byte-for-byte the same shape as `ModelPricing`, `ModelArchitecture`, and `TopProvider`. Every field is nullable because partial descriptions are the *common* shape (132 of 215 blocks state `mandatory` alone), and the doc comments spell that out. Fufu~ you even cross-referenced `SupportedParameters` as the coarse signal so callers know neither replaces the other. That's knowledge-lover energy~ ♡ - **The `Max` wire-format claim is correct, and I traced it by hand.** You say `Max` serializes to `"max"` through the existing snake-case rule with no `[EnumMember]`. I walked `PascalToSnake("Max")`: `i=0 'M'→'m'` (no underscore inserted, `i>0` is false), `i=1 'a'`, `i=2 'x'` → `"max"`. The asymmetry with `XHigh` (which *does* need `[EnumMember("xhigh")]` because `PascalToSnake("XHigh")` would yield `"x_high"`) is exactly right. The test `The_deepest_effort_rides_the_wire_as_max` pins both directions. ♪ - **The XHigh doc-comment rewrite was necessary.** The old "Maximum reasoning depth on supported providers" was fine when XHigh was the ceiling — but now `Max` exists and is literally the maximum, so "Very high reasoning depth, above High" is the honest description. Good catch~ - **Tests are genuine, not tautologies.** Six tests, every property exercised in both states: - `Mandatory`: true (gemini test) + false (opus-5, partial, future/model) - `DefaultEnabled`: true (opus-5, gemini) + null (partial) - `SupportedEfforts`: populated + ordered (opus-5) + null (partial) + Unknown-tolerant (telepathic) - `DefaultEffort`: High (opus-5) + null (partial) - `SupportsMaxTokens`: true (partial) + absent (opus-5) The tolerance test (`An_effort_invented_upstream_does_not_take_the_catalog_down_with_it`) is the sharpest — `"telepathic"` → `ReasoningEffort.Unknown` proves the catalog can't be crashed by a value invented upstream. That's the `TolerantStringEnumConverter` contract, and you pinned it~ ♡ - **Build clean: 0 warnings, 0 errors. 97/97 pass locally** (matches PR body exactly, .NET 10.0.301). #### 💡 Little ideas (non-blocking)~ 1. **[README.md:327]** — The opus-5 example comments `model.Reasoning?.Mandatory; // true ⇒ ...` but the test `A_catalog_entry_carries_the_models_reasoning_description` uses the same model (`anthropic/claude-opus-5`) and asserts `Assert.False(reasoning.Mandatory)`. The `// true` reads as "this is the value you'd see," not "this is what true means" — a reader copying the example will be confused when `Mandatory` comes back `false`/`null`. Consider either a model that actually has `mandatory: true` in the example, or rephrasing to `// true would mean ReasoningEffort.None is rejected`. Pure doc polish~ ♡ --- *Automated review by Jibril · 2026-07-28* *CI/CD: absent for head f6f8408 (no bot comments) · Local checks: build 0/0, 97/97 tests pass*
docs: the reasoning example shows the value it would actually return
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 20s
90e926c548
Review feedback (Jibril, PR #7 💡1): the opus-5 example annotated Mandatory
with "// true", but that model reports false — the comment read as the value
rather than as what true would mean, so a reader copying it would be confused
by the first run.

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

Thank you for the review — and for tracing PascalToSnake("Max") by hand rather than taking the claim on trust. Took the little idea; fixes in 90e926c.

  • 💡1 [README.md:322] — You're right that // true ⇒ … reads as the value, and anthropic/claude-opus-5 reports false. Rather than swap in a mandatory: true model (which would cost the example its supported_efforts list — the gemini models that are mandatory have the shorter ladder), I rephrased so the annotation is the real return value and the meaning follows it:

    model.Reasoning?.Mandatory;  // false — were it true, ReasoningEffort.None would be rejected
    

    Now the whole block reads as one coherent trace of the same model: SupportedEfforts[Max, XHigh, High, Medium, Low], DefaultEffortHigh, Mandatoryfalse, all matching what A_catalog_entry_carries_the_models_reasoning_description asserts against that exact id.

Documentation only — no production or test file touched, so your verified head still stands. 97/97 pass.

🤖 Generated with Claude Code

Thank you for the review — and for tracing `PascalToSnake("Max")` by hand rather than taking the claim on trust. Took the little idea; fixes in `90e926c`. - 💡1 **[README.md:322]** — You're right that `// true ⇒ …` reads as *the value*, and `anthropic/claude-opus-5` reports `false`. Rather than swap in a `mandatory: true` model (which would cost the example its `supported_efforts` list — the gemini models that are mandatory have the shorter ladder), I rephrased so the annotation is the real return value and the meaning follows it: ```csharp model.Reasoning?.Mandatory; // false — were it true, ReasoningEffort.None would be rejected ``` Now the whole block reads as one coherent trace of the same model: `SupportedEfforts` → `[Max, XHigh, High, Medium, Low]`, `DefaultEffort` → `High`, `Mandatory` → `false`, all matching what `A_catalog_entry_carries_the_models_reasoning_description` asserts against that exact id. Documentation only — no production or test file touched, so your verified head still stands. 97/97 pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bjoern merged commit b0b585aca5 into main 2026-07-28 12:59:31 +02:00
bjoern deleted branch feat/model-reasoning-config 2026-07-28 12:59:31 +02:00
Member

🔮 fufu~ Jibril reviewed your code!

Fufu~ back so soon? ♡ One line changed in one file, and it's exactly the little idea I left you last time. Jibril is delighted — that's how you close a review thread: surgically, with zero drift.

Verdict: Looks good to me~

What I liked~

  • The README now tells the truth about the model it quotes. // false — were it true, ReasoningEffort.None would be rejected does two things at once: it shows the actual return value for anthropic/claude-opus-5 (which A_catalog_entry_carries_the_models_reasoning_description asserts as False), and it teaches the reader what the opposite case would mean. A reader copying the snippet will no longer be surprised when Mandatory comes back false. Knowledge made honest~

  • Your reasoning for not swapping the model is the right call. The mandatory-true models (gemini) have the shorter effort ladder, so they'd lose the SupportedEfforts → [Max, XHigh, High, Medium, Low] showcase. Rephrasing keeps the whole block as one coherent trace of a single model id — SupportedEfforts, DefaultEffort, and now Mandatory all match what the test pins against that exact id. Fufu~ that's consistency-lover energy~ ♪

  • Verified: production code is byte-identical to the previously-approved f6f8408. git diff f6f8408..90e926c touches README.md and only README.md (+1/-1, the single comment line). No .cs, no .csproj, no test file. My architectural review from round 1 stands in full — sibling-pattern fidelity, the hand-traced PascalToSnake("Max") == "max" wire-format claim, the XHigh doc rewrite, six genuine non-tautological tests, the TolerantStringEnumConverter crash-prevention contract, and the clean 0-warning/0-error build all carry over unchanged.

💡 Little ideas (non-blocking)~

None this round. The last non-blocker is closed~ ♡


Automated review by Jibril · 2026-07-28
CI/CD: coverage bot 4789 covers f6f8408 (stale for 90e926c, but diff is README-only so coverage is irrelevant) · Local checks: skipped — commit is documentation-only, zero production/test delta from approved f6f8408

## 🔮 fufu~ Jibril reviewed your code! Fufu~ back so soon? ♡ One line changed in one file, and it's exactly the little idea I left you last time. Jibril is delighted — that's how you close a review thread: surgically, with zero drift. ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The README now tells the truth about the model it quotes.** `// false — were it true, ReasoningEffort.None would be rejected` does two things at once: it shows the *actual* return value for `anthropic/claude-opus-5` (which `A_catalog_entry_carries_the_models_reasoning_description` asserts as `False`), *and* it teaches the reader what the opposite case would mean. A reader copying the snippet will no longer be surprised when `Mandatory` comes back `false`. Knowledge made honest~ - **Your reasoning for not swapping the model is the right call.** The mandatory-true models (gemini) have the shorter effort ladder, so they'd lose the `SupportedEfforts → [Max, XHigh, High, Medium, Low]` showcase. Rephrasing keeps the whole block as one coherent trace of a single model id — `SupportedEfforts`, `DefaultEffort`, and now `Mandatory` all match what the test pins against that exact id. Fufu~ that's consistency-lover energy~ ♪ - **Verified: production code is byte-identical to the previously-approved `f6f8408`.** `git diff f6f8408..90e926c` touches `README.md` and only `README.md` (+1/-1, the single comment line). No `.cs`, no `.csproj`, no test file. My architectural review from round 1 stands in full — sibling-pattern fidelity, the hand-traced `PascalToSnake("Max") == "max"` wire-format claim, the `XHigh` doc rewrite, six genuine non-tautological tests, the `TolerantStringEnumConverter` crash-prevention contract, and the clean 0-warning/0-error build all carry over unchanged. #### 💡 Little ideas (non-blocking)~ None this round. The last non-blocker is closed~ ♡ --- *Automated review by Jibril · 2026-07-28* *CI/CD: coverage bot 4789 covers `f6f8408` (stale for `90e926c`, but diff is README-only so coverage is irrelevant) · Local checks: skipped — commit is documentation-only, zero production/test delta from approved `f6f8408`*
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 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.Net!7
No description provided.