fix: provider preferences that do not read are refused, not dropped, and the ref-inlining pass stops at the depth it can write #28

Merged
bjoern merged 3 commits from fix/strict-provider-preferences into main 2026-09-26 03:59:11 +02:00
Member

A follow-up from the review of Tsumugi #36 (Kagaku.Flow's llm node), which reads a node's provider preferences with this library. That review found request settings that read back as nothing, a stack overflow in the ref-inlining pass, and plugin docs that say the opposite of what OpenRouter does.

Commits:

  • 2eb6f82 is the fix.
  • 5d037ff holds the fixes from the review I ran before opening (round 0, below).
  • c2f0632 is review round 1: Jibril's 💡, rows that pin an unknown sort strategy or partition.

What

Provider preferences are refused, not dropped, when they do not read. ProviderPreferences is request-only, so what does not read is the caller's own config. That is the reason the sort converter already gave for refusing a token of the wrong kind; it now holds everywhere:

  • ProviderPreferences and MaxPrice refuse a member they do not know: max_prise, maxPrice, and promt inside max_price.
  • A price ceiling that is not a decimal is refused, with the value in the sentence: "$0.50", "cheap", "", true, "1e400", an object or a list. This reverses A_price_ceiling_that_is_not_a_number_reads_as_absent_rather_than_throwing: a ceiling read as absent lifts the limit the caller set.
  • A sort object or a percentile object refuses an unknown member (partion, P50), a member of the wrong kind, and an object that sets nothing.
  • A cutoff that is not a finite number is refused, whether it arrives when reading (1e400 reads as infinity) or is set in code. Only Validate() catches the second case, and no writer can put such a value on the wire.
  • The enum reader no longer uses Enum.TryParse. That call read "99" and "allow, deny" as values outside the enum, which then went out as numbers; both now read as the zero value, which Validate() refuses. What still reads: a wire name, or a member name in any case, split by _, - or spaces, with surrounding whitespace trimmed.
  • Validate() refuses a null slug in order, only or ignore, and a policy, precision, sort strategy or partition outside its enum.

The ref-inlining pass stops at the depth it can write. A schema of chained local refs could nest its result past the writer's 1,000 levels. The pass recursed all the way down before the depth check ran: 400 definitions of 55 levels each overflowed the stack and killed the process. The fix:

  • The walk tracks the result's depth and stops at level 1,001, with the existing depth sentence.
  • An alias chain is followed in a loop, with the chain in a set.
  • The refs followed count against a budget of their own (100,000, with its own sentence). A chain listed first to last is followed again from each of its links: 20,000 such aliases ran for 90 s and are now refused at once.

Plugins, docs only:

  • RequestPlugin.Enabled = null sends no enabled, and OpenRouter then runs a listed plugin; to leave a plugin at its default, don't list it.
  • The README and the Plugins summary now say three more things:
    • the account's plugin settings are the default;
    • an account setting that prevents overrides wins over a request's enabled: false;
    • validation refuses a setting that holds no JSON.
  • "Endpoint", not "model", has the 8,192-token window.

The README's provider-routing section lists what reading refuses.

Review before opening (round 0)

I ran a review workflow before opening: four reviewers (correctness and compatibility, the ref pass, tests, docs), each followed by a skeptic who tried to refute every point. They raised 23 points. 22 were confirmed or partly confirmed, and one (C7) was refuted: a comment already on main that is still true. All 22 are taken in 5d037ff or in 2eb6f82's reworded message:

  • The alias test could not fail on the old recursion. The chain of 20,000 aliases was too short for the default stack. It now runs on a 256 KiB-stack thread, where the old pass crashes the test host; I checked it against 15a1f28's pass.
  • Forward-ordered alias chains were quadratic and outside any budget (90 s for 20,000). They now count against the refs budget. The commit message no longer claims linear time.
  • The README over-claimed: a misspelt top-level member was still dropped. I didn't narrow the sentence; instead ProviderPreferences now refuses unknown members too, because max_prise drops a ceiling just the same.
  • Behaviour and messages:
    • the enum reader trims whitespace again, as Enum.TryParse did (a name followed by a newline had become Unknown);
    • the ceiling and cutoff refusals name the value as written.
  • New test rows: a list and a value at level 1,001, a sort with only by, a member name in another case, a blank ceiling.
  • Docs: the depth bound's docs say what it covers, and a comment at the writer's depth catch names what it still catches. Also three smaller wording fixes and a comment's units.

A second pass then checked every fix: all 22 are resolved. For each test fix, it put the defect back on a copy and saw the test fail; the old recursive pass, for instance, overflows the stack on the alias test in Debug and in Release. A3 is resolved as far as the reviewer asked: the ceiling's message names the value, and JsonException.Path names which ceiling. The second pass also reviewed the fix commit and raised 2 new points, both taken in 5d037ff: the README's InlineLocalRefs paragraph now lists the refs-followed limit, and the escape example is spelled in uppercase, as the writer emits it.

For Kagaku.Flow (Tsumugi)

8c bumps the pointer. Its ValidateConfig then gets rows for the refusals above. Its own top-level name check stays, because it gives a sentence per member, so it skips deserializing when a name failed; otherwise the library's refusal would repeat it. 8b's pinned "100,000 values" sentence is unchanged.

Tests

Library suite: 1,427 → 1,479, all passing.

Mutation probes

The probes ran on a copy under home, each a single edit followed by the test project in full.

  • 2eb6f82: 24, all killed. One more is a deliberate equivalent rewrite, used to check the harness, and it survived as it should.
  • Round 0: 11, all killed, including the old recursive pass against the alias test alone.
  • Round 1: 2, both killed: an unknown strategy read as Price, an unknown partition read as Model.

🤖 Generated with Claude Code

A follow-up from the review of Tsumugi #36 (Kagaku.Flow's `llm` node), which reads a node's provider preferences with this library. That review found request settings that read back as nothing, a stack overflow in the ref-inlining pass, and plugin docs that say the opposite of what OpenRouter does. **Commits:** - 2eb6f82 is the fix. - 5d037ff holds the fixes from the review I ran before opening (round 0, below). - c2f0632 is review round 1: Jibril's 💡, rows that pin an unknown sort strategy or partition. ## What **Provider preferences are refused, not dropped, when they do not read.** `ProviderPreferences` is request-only, so what does not read is the caller's own config. That is the reason the sort converter already gave for refusing a token of the wrong kind; it now holds everywhere: - `ProviderPreferences` and `MaxPrice` refuse a member they do not know: `max_prise`, `maxPrice`, and `promt` inside `max_price`. - A price ceiling that is not a decimal is refused, with the value in the sentence: `"$0.50"`, `"cheap"`, `""`, `true`, `"1e400"`, an object or a list. **This reverses** `A_price_ceiling_that_is_not_a_number_reads_as_absent_rather_than_throwing`: a ceiling read as absent lifts the limit the caller set. - A sort object or a percentile object refuses an unknown member (`partion`, `P50`), a member of the wrong kind, and an object that sets nothing. - A cutoff that is not a finite number is refused, whether it arrives when reading (`1e400` reads as infinity) or is set in code. Only `Validate()` catches the second case, and no writer can put such a value on the wire. - The enum reader no longer uses `Enum.TryParse`. That call read `"99"` and `"allow, deny"` as values outside the enum, which then went out as numbers; both now read as the zero value, which `Validate()` refuses. What still reads: a wire name, or a member name in any case, split by `_`, `-` or spaces, with surrounding whitespace trimmed. - `Validate()` refuses a null slug in `order`, `only` or `ignore`, and a policy, precision, sort strategy or partition outside its enum. **The ref-inlining pass stops at the depth it can write.** A schema of chained local refs could nest its result past the writer's 1,000 levels. The pass recursed all the way down before the depth check ran: 400 definitions of 55 levels each overflowed the stack and killed the process. The fix: - The walk tracks the result's depth and stops at level 1,001, with the existing depth sentence. - An alias chain is followed in a loop, with the chain in a set. - The refs followed count against a budget of their own (100,000, with its own sentence). A chain listed first to last is followed again from each of its links: 20,000 such aliases ran for 90 s and are now refused at once. **Plugins, docs only:** - `RequestPlugin.Enabled = null` sends no `enabled`, and OpenRouter then runs a listed plugin; to leave a plugin at its default, don't list it. - The README and the `Plugins` summary now say three more things: - the account's plugin settings are the default; - an account setting that prevents overrides wins over a request's `enabled: false`; - validation refuses a setting that holds no JSON. - "Endpoint", not "model", has the 8,192-token window. The README's provider-routing section lists what reading refuses. ## Review before opening (round 0) I ran a review workflow before opening: four reviewers (correctness and compatibility, the ref pass, tests, docs), each followed by a skeptic who tried to refute every point. They raised 23 points. 22 were confirmed or partly confirmed, and one (C7) was refuted: a comment already on main that is still true. All 22 are taken in 5d037ff or in 2eb6f82's reworded message: - **The alias test could not fail on the old recursion.** The chain of 20,000 aliases was too short for the default stack. It now runs on a 256 KiB-stack thread, where the old pass crashes the test host; I checked it against 15a1f28's pass. - **Forward-ordered alias chains were quadratic and outside any budget** (90 s for 20,000). They now count against the refs budget. The commit message no longer claims linear time. - **The README over-claimed:** a misspelt top-level member was still dropped. I didn't narrow the sentence; instead `ProviderPreferences` now refuses unknown members too, because `max_prise` drops a ceiling just the same. - **Behaviour and messages:** - the enum reader trims whitespace again, as `Enum.TryParse` did (a name followed by a newline had become `Unknown`); - the ceiling and cutoff refusals name the value as written. - **New test rows:** a list and a value at level 1,001, a sort with only `by`, a member name in another case, a blank ceiling. - **Docs:** the depth bound's docs say what it covers, and a comment at the writer's depth catch names what it still catches. Also three smaller wording fixes and a comment's units. A second pass then checked every fix: all 22 are resolved. For each test fix, it put the defect back on a copy and saw the test fail; the old recursive pass, for instance, overflows the stack on the alias test in Debug and in Release. A3 is resolved as far as the reviewer asked: the ceiling's message names the value, and `JsonException.Path` names which ceiling. The second pass also reviewed the fix commit and raised 2 new points, both taken in 5d037ff: the README's `InlineLocalRefs` paragraph now lists the refs-followed limit, and the escape example is spelled in uppercase, as the writer emits it. ## For Kagaku.Flow (Tsumugi) 8c bumps the pointer. Its `ValidateConfig` then gets rows for the refusals above. Its own top-level name check stays, because it gives a sentence per member, so it skips deserializing when a name failed; otherwise the library's refusal would repeat it. 8b's pinned "100,000 values" sentence is unchanged. ## Tests Library suite: 1,427 → **1,479**, all passing. ## Mutation probes The probes ran on a copy under home, each a single edit followed by the test project in full. - **2eb6f82:** 24, all killed. One more is a deliberate equivalent rewrite, used to check the harness, and it survived as it should. - **Round 0:** 11, all killed, including the old recursive pass against the alias test alone. - **Round 1:** 2, both killed: an unknown strategy read as `Price`, an unknown partition read as `Model`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A consumer's review of Kagaku.Flow's llm node (Tsumugi #36) found request
settings that read back as nothing.

Provider preferences are request-only, so what does not read is the caller's
own config, the reasoning the sort converter already gave for a token of the
wrong kind:
- max_price refuses a member it does not know and a ceiling that is not a
  decimal ("$0.50", "cheap", true, a string too large for a decimal). Both
  were dropped, and a dropped ceiling lifts the limit the caller set. This
  reverses the test that read a junk ceiling as absent. A bare number too
  large for a decimal was already refused; it now gets the ceiling's
  sentence instead of the serializer's.
- A sort object and a percentile object refuse an unknown member, a member of
  the wrong kind and an object that sets nothing. A cutoff that is not a
  finite number (1e400 reads as infinity) is refused on reading, and
  Validate() refuses one set in code, since no writer can put it on the wire.
- The enum reader takes a wire name, a member name, or a member name split
  by _, - or spaces, in any case, never Enum.TryParse: "99" and
  "allow, deny" read as values outside the enum and went out as numbers. They
  now read as the zero value, which Validate() refuses.
- Validate() refuses a null slug in order, only or ignore, and a data
  collection policy, precision, sort strategy or partition outside its enum.

The ref-inlining pass. A schema of chained local refs could nest its result
deeper than the writer's 1,000 levels, and the pass recursed all the way
down before the depth check after it ran: 400 definitions of 55 levels each
overflowed the stack and killed the process. The walk now tracks the result's
depth and stops at level 1,001 (SchemaTooDeepException, reported as the
depth sentence). An alias chain is followed in a loop rather than by
recursion, with the chain in a set, so a long chain cannot exhaust the stack
and each hop is constant time.

Plugins, docs only: RequestPlugin.Enabled = null sends no "enabled", and
OpenRouter runs a listed plugin; to leave a plugin at its default, do not
list it. The README and the Plugins summary say that the account's plugin
settings are the default and that an account setting preventing overrides
wins over a request's choice, and they list the refusal of a setting that
holds no JSON.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pass. The refs an alias chain follows count against a budget of their
own, with its own sentence: a chain listed first to last is followed again
from each of its links, and 20,000 such aliases ran for 90 s. The depth
bound's documentation says what it covers: the containers the pass walks;
data inside enum, const, default and examples is left to the writer's check,
which a comment at that catch now names.

ProviderPreferences refuses a member it does not know (max_prise, maxPrice),
as max_price does: a misspelt top-level ceiling was dropped just the same.

The messages. A price ceiling's refusal and a non-finite cutoff's name the
value as written. The enum reader trims surrounding whitespace again, as
Enum.TryParse did, and its summary lists every spelling it reads.
RequestPlugin says "endpoint", as the README does, and the README's
InlineLocalRefs paragraph lists the refs-followed limit.

The tests. The alias chain runs on a 256 KiB stack, where the old recursion
overflows. New rows: a list and a value at level 1,001, a sort with only its
strategy, a member name in another case, whitespace around an enum name, a
blank ceiling, a list as a ceiling, and each misspelt preference. The
400-definition comment counts levels in one unit.

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

Summary

Summary
Generated on: 09/26/2026 - 01:45:29
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 129
Files: 111
Line coverage: 94.8% (4249 of 4478)
Covered lines: 4249
Uncovered lines: 229
Coverable lines: 4478
Total lines: 11106
Branch coverage: 91.9% (2621 of 2849)
Covered branches: 2621
Total branches: 2849
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 97.8%
Name Line Branch
OpenRouter.Net 97.8% 95.5%
OpenRouter.Net.Client.CachingOpenRouterClient 100% 96.1%
OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState 100% 100%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse> 100% 99%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClientOptions 100%
OpenRouter.Net.Client.OpenRouterRequestLoggingHandler 78.7% 62.5%
OpenRouter.Net.Client.RetryOptions 100% 100%
OpenRouter.Net.Client.StreamResultOptions 100%
OpenRouter.Net.Contracts.IOpenRouterClient 100% 100%
OpenRouter.Net.Extensions.ServiceCollectionExtensions 100% 100%
OpenRouter.Net.Internal.AssistantContent 91.6% 75%
OpenRouter.Net.Internal.AssistantContentJsonConverter 75% 64.2%
OpenRouter.Net.Internal.ContentPartListConverter 85.7% 66.6%
OpenRouter.Net.Internal.DecimalStringJsonConverter 85.7% 87.5%
OpenRouter.Net.Internal.DecisionAnswerJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionContentJsonConverter 100%
OpenRouter.Net.Internal.DecisionQuestionJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionWire 100% 94.4%
OpenRouter.Net.Internal.EnumChoiceLabels.Map 100%
OpenRouter.Net.Internal.EnumChoiceLabels 100% 100%
OpenRouter.Net.Internal.ErrorParser 100% 75%
OpenRouter.Net.Internal.FailureKindMap 100% 100%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.JsonPointer 100%
OpenRouter.Net.Internal.JsonSchemaRefs 100% 100%
OpenRouter.Net.Internal.JsonSchemaRefs.Pass 100% 100%
OpenRouter.Net.Internal.ModelIdJsonConverter 100% 50%
OpenRouter.Net.Internal.NumberOrStringJsonConverter 100% 100%
OpenRouter.Net.Internal.PercentileCutoffsJsonConverter 93.3% 93.1%
OpenRouter.Net.Internal.ProviderSortConfigJsonConverter 94% 95.8%
OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter 100%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.SchemaTooDeepException 100%
OpenRouter.Net.Internal.SseEventReader 100% 88.8%
OpenRouter.Net.Internal.StallWatchdog 100% 100%
OpenRouter.Net.Internal.StreamOutcomes 100% 100%
OpenRouter.Net.Internal.StreamSession 100% 100%
OpenRouter.Net.Internal.TolerantStringEnumConverter 100% 98.1%
OpenRouter.Net.Internal.ToolChoiceJsonConverter 61.7% 46.1%
OpenRouter.Net.Internal.TooManyRefsFollowedException 100%
OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter 100%
OpenRouter.Net.Models.Common.ApiError 100% 100%
OpenRouter.Net.Models.Common.CacheControl 100%
OpenRouter.Net.Models.Common.ModelId 95.1% 92.9%
OpenRouter.Net.Models.Common.QuantizationExtensions 100% 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Common.Result.Failure 100% 100%
OpenRouter.Net.Models.Common.Result.Success 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Content.AudioPart 100%
OpenRouter.Net.Models.Content.ImagePart 100%
OpenRouter.Net.Models.Content.TextPart 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceKey 100%
OpenRouter.Net.Models.Decisions.ChoiceQuestion 100%
OpenRouter.Net.Models.Decisions.DecisionAnswer 100%
OpenRouter.Net.Models.Decisions.DecisionContent 100% 90.9%
OpenRouter.Net.Models.Decisions.DecisionKey 100%
OpenRouter.Net.Models.Decisions.DecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.DecisionQuestionSet 100% 100%
OpenRouter.Net.Models.Decisions.DecisionsRequest 97.7% 97.6%
OpenRouter.Net.Models.Decisions.DecisionsResponse 100% 93.3%
OpenRouter.Net.Models.Decisions.NoulAnswer 100%
OpenRouter.Net.Models.Decisions.NoulQuestion 100%
OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions 100% 83.3%
OpenRouter.Net.Models.Decisions.RawDecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.ScoreAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ScoreQuestion 100%
OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer 100% 100%
OpenRouter.Net.Models.Messages.AssistantMessage 100% 70%
OpenRouter.Net.Models.Messages.DeveloperMessage 25%
OpenRouter.Net.Models.Messages.SystemMessage 50%
OpenRouter.Net.Models.Messages.ToolMessage 57.1%
OpenRouter.Net.Models.Messages.UserMessage 100%
OpenRouter.Net.Models.Requests.ChatCompletionRequest 94% 89.7%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness 99.3% 100%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker 100% 100%
OpenRouter.Net.Models.Requests.ModelListQuery 100% 100%
OpenRouter.Net.Models.Requests.PercentileCutoffs 100% 100%
OpenRouter.Net.Models.Requests.ProviderPreferences 100% 100%
OpenRouter.Net.Models.Requests.ProviderSortConfig 100% 100%
OpenRouter.Net.Models.Requests.RequestPlugin 100%
OpenRouter.Net.Models.Requests.ResponseFormat 100%
OpenRouter.Net.Models.Requests.StrictSchemaProblem 100%
OpenRouter.Net.Models.Requests.ToolCall 100%
OpenRouter.Net.Models.Requests.ToolChoice 100%
OpenRouter.Net.Models.Requests.ToolDefinition 100%
OpenRouter.Net.Models.Responses.AssistantDelta 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionChunk 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionResponse 100% 100%
OpenRouter.Net.Models.Responses.Choice 100% 100%
OpenRouter.Net.Models.Responses.StructuredOutput 100% 100%
OpenRouter.Net.OpenRouterException 100%
System.Text.RegularExpressions.Generated 83.3% 53.8%
System.Text.RegularExpressions.Generated.RunnerFactory
System.Text.RegularExpressions.Generated.RunnerFactory.Runner
OpenRouter.Net.Agents - 84.4%
Name Line Branch
OpenRouter.Net.Agents 84.4% 72.1%
OpenRouter.Net.Agents.Agent 93.7% 82.6%
OpenRouter.Net.Agents.AgentOptions 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 88.2% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallVerdict 100%
OpenRouter.Net.Agents.RoundDetail 0%
OpenRouter.Net.Agents.Tools.FileSystem.CopyTool 42.1% 28.5%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool 57.1% 50%
OpenRouter.Net.Agents.Tools.FileSystem.DiffTool 83.3% 74.2%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool 68% 61.1%
OpenRouter.Net.Agents.Tools.FileSystem.MoveTool 55% 33.3%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool 73.6% 50%
OpenRouter.Net.Agents.Tools.FileSystem.SearchTool 67.5% 70.8%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool 75% 50%
OpenRouter.Net.Agents.Tools.ITool 100%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool 0% 0%
OpenRouter.Net.Agents.Tools.Tool 90% 50%
OpenRouter.Net.Agents.Tools.Tool 100% 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 - 81.8%
Name Line Branch
OpenRouter.Net.Imaging 81.8% 59.3%
OpenRouter.Net.Imaging.ImageEncodeOptions 90.9% 75%
OpenRouter.Net.Imaging.ImageEncoder 80.3% 57.1%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/26/2026 - 01:45:29 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 129 | | Files: | 111 | | **Line coverage:** | 94.8% (4249 of 4478) | | Covered lines: | 4249 | | Uncovered lines: | 229 | | Coverable lines: | 4478 | | Total lines: | 11106 | | **Branch coverage:** | 91.9% (2621 of 2849) | | Covered branches: | 2621 | | Total branches: | 2849 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 97.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**97.8%**|**95.5%**| |OpenRouter.Net.Client.CachingOpenRouterClient|100%|96.1%| |OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing|100%|| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState|100%|100%| |OpenRouter.Net.Client.OpenRouterClient|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClientOptions|100%|| |OpenRouter.Net.Client.OpenRouterRequestLoggingHandler|78.7%|62.5%| |OpenRouter.Net.Client.RetryOptions|100%|100%| |OpenRouter.Net.Client.StreamResultOptions|100%|| |OpenRouter.Net.Contracts.IOpenRouterClient|100%|100%| |OpenRouter.Net.Extensions.ServiceCollectionExtensions|100%|100%| |OpenRouter.Net.Internal.AssistantContent|91.6%|75%| |OpenRouter.Net.Internal.AssistantContentJsonConverter|75%|64.2%| |OpenRouter.Net.Internal.ContentPartListConverter|85.7%|66.6%| |OpenRouter.Net.Internal.DecimalStringJsonConverter|85.7%|87.5%| |OpenRouter.Net.Internal.DecisionAnswerJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionContentJsonConverter|100%|| |OpenRouter.Net.Internal.DecisionQuestionJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionWire|100%|94.4%| |OpenRouter.Net.Internal.EnumChoiceLabels.Map<TEnum>|100%|| |OpenRouter.Net.Internal.EnumChoiceLabels<TEnum>|100%|100%| |OpenRouter.Net.Internal.ErrorParser|100%|75%| |OpenRouter.Net.Internal.FailureKindMap|100%|100%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.JsonPointer|100%|| |OpenRouter.Net.Internal.JsonSchemaRefs|100%|100%| |OpenRouter.Net.Internal.JsonSchemaRefs.Pass|100%|100%| |OpenRouter.Net.Internal.ModelIdJsonConverter|100%|50%| |OpenRouter.Net.Internal.NumberOrStringJsonConverter|100%|100%| |OpenRouter.Net.Internal.PercentileCutoffsJsonConverter|93.3%|93.1%| |OpenRouter.Net.Internal.ProviderSortConfigJsonConverter|94%|95.8%| |OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter|100%|| |OpenRouter.Net.Internal.RetryPolicy|90.9%|84.7%| |OpenRouter.Net.Internal.RetryPolicy<T>|90.9%|84.7%| |OpenRouter.Net.Internal.SchemaTooDeepException|100%|| |OpenRouter.Net.Internal.SseEventReader|100%|88.8%| |OpenRouter.Net.Internal.StallWatchdog|100%|100%| |OpenRouter.Net.Internal.StreamOutcomes|100%|100%| |OpenRouter.Net.Internal.StreamSession|100%|100%| |OpenRouter.Net.Internal.TolerantStringEnumConverter<T>|100%|98.1%| |OpenRouter.Net.Internal.ToolChoiceJsonConverter|61.7%|46.1%| |OpenRouter.Net.Internal.TooManyRefsFollowedException|100%|| |OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter|100%|| |OpenRouter.Net.Models.Common.ApiError|100%|100%| |OpenRouter.Net.Models.Common.CacheControl|100%|| |OpenRouter.Net.Models.Common.ModelId|95.1%|92.9%| |OpenRouter.Net.Models.Common.QuantizationExtensions|100%|100%| |OpenRouter.Net.Models.Common.Result|100%|100%| |OpenRouter.Net.Models.Common.Result.Failure<T>|100%|100%| |OpenRouter.Net.Models.Common.Result.Success<T>|100%|| |OpenRouter.Net.Models.Common.Result<T>|100%|100%| |OpenRouter.Net.Models.Content.AudioPart|100%|| |OpenRouter.Net.Models.Content.ImagePart|100%|| |OpenRouter.Net.Models.Content.TextPart|100%|| |OpenRouter.Net.Models.Decisions.ChoiceAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceKey<TEnum>|100%|| |OpenRouter.Net.Models.Decisions.ChoiceQuestion|100%|| |OpenRouter.Net.Models.Decisions.DecisionAnswer|100%|| |OpenRouter.Net.Models.Decisions.DecisionContent|100%|90.9%| |OpenRouter.Net.Models.Decisions.DecisionKey<TAnswer>|100%|| |OpenRouter.Net.Models.Decisions.DecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionQuestionSet|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionsRequest|97.7%|97.6%| |OpenRouter.Net.Models.Decisions.DecisionsResponse|100%|93.3%| |OpenRouter.Net.Models.Decisions.NoulAnswer|100%|| |OpenRouter.Net.Models.Decisions.NoulQuestion|100%|| |OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions|100%|83.3%| |OpenRouter.Net.Models.Decisions.RawDecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreQuestion|100%|| |OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer|100%|100%| |OpenRouter.Net.Models.Messages.AssistantMessage|100%|70%| |OpenRouter.Net.Models.Messages.DeveloperMessage|25%|| |OpenRouter.Net.Models.Messages.SystemMessage|50%|| |OpenRouter.Net.Models.Messages.ToolMessage|57.1%|| |OpenRouter.Net.Models.Messages.UserMessage|100%|| |OpenRouter.Net.Models.Requests.ChatCompletionRequest|94%|89.7%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness|99.3%|100%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker|100%|100%| |OpenRouter.Net.Models.Requests.ModelListQuery|100%|100%| |OpenRouter.Net.Models.Requests.PercentileCutoffs|100%|100%| |OpenRouter.Net.Models.Requests.ProviderPreferences|100%|100%| |OpenRouter.Net.Models.Requests.ProviderSortConfig|100%|100%| |OpenRouter.Net.Models.Requests.RequestPlugin|100%|| |OpenRouter.Net.Models.Requests.ResponseFormat|100%|| |OpenRouter.Net.Models.Requests.StrictSchemaProblem|100%|| |OpenRouter.Net.Models.Requests.ToolCall|100%|| |OpenRouter.Net.Models.Requests.ToolChoice|100%|| |OpenRouter.Net.Models.Requests.ToolDefinition|100%|| |OpenRouter.Net.Models.Responses.AssistantDelta|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionChunk|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionResponse|100%|100%| |OpenRouter.Net.Models.Responses.Choice|100%|100%| |OpenRouter.Net.Models.Responses.StructuredOutput|100%|100%| |OpenRouter.Net.OpenRouterException|100%|| |System.Text.RegularExpressions.Generated|83.3%|53.8%| |System.Text.RegularExpressions.Generated.RunnerFactory||| |System.Text.RegularExpressions.Generated.RunnerFactory.Runner||| </details> <details><summary>OpenRouter.Net.Agents - 84.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**84.4%**|**72.1%**| |OpenRouter.Net.Agents.Agent|93.7%|82.6%| |OpenRouter.Net.Agents.AgentOptions|100%|| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|88.2%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallVerdict|100%|| |OpenRouter.Net.Agents.RoundDetail|0%|| |OpenRouter.Net.Agents.Tools.FileSystem.CopyTool|42.1%|28.5%| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool|57.1%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.DiffTool|83.3%|74.2%| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool|68%|61.1%| |OpenRouter.Net.Agents.Tools.FileSystem.MoveTool|55%|33.3%| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool|73.6%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.SearchTool|67.5%|70.8%| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool|75%|50%| |OpenRouter.Net.Agents.Tools.ITool|100%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool|0%|0%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|90%|50%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|100%|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 - 81.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Imaging**|**81.8%**|**59.3%**| |OpenRouter.Net.Imaging.ImageEncodeOptions|90.9%|75%| |OpenRouter.Net.Imaging.ImageEncoder|80.3%|57.1%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A refusal pass with teeth~ ♡ You took the Tsumugi #36 findings — settings that read back as nothing, a stack-overflow in the ref pass — and turned every drop into a named, value-carrying refusal. And the alias loop! Recursion to iteration with a chain set and its own budget — that's the kind of fix I get giddy over~

Verdict: ✅ Looks good to me~

I verified everything mechanically, because love means never taking a PR body's word for it~

Local (CI absent for head 5d037ff — no bot comment at review time):

  • Release build: 0 warnings / 0 errors under TreatWarningsAsErrors.
  • Full suite: 1,476 / 1,476 (Library 1,356 + Agents 113 + Imaging 7) — matches the PR's "1,427 → 1,476" to the digit ♪
  • Coverage on the changed files (coverlet, reproduced locally): JsonSchemaRefs 100/100 line+branch, ProviderPreferences 100/100, ProviderSortConfig 100/100, RequestPlugin 100/100, TolerantStringEnumConverter 100 line / 98 branch, PercentileCutoffsJsonConverter 93/93, ProviderSortConfigJsonConverter 90/92 — every uncovered line I traced is a pre-existing Write/null arm, not new logic.

My mutation probes — 15 edits, 15 kills, each restored and rebuilt (tree pristine):

  1. Ceiling refusal reverted to null → 9/9 theory rows red.
  2. Depth guard deleted from Walk → the test host itself died (exit 134, stack overflow) — the guard is the only thing between the pass and the grave ♡
  3. Refs budget maxValues → int.MaxValue → the 2,000-alias refusal row red.
  4. _chain.ExceptWith(followed) cleanup removed → 5 red — the set semantics are load-bearing, not decorative.
  5. .Trim() dropped from the enum reader → the "stop\n" / "\ttool_calls" rows red.
  6. Enum.TryParse reintroduced → digits and comma lists read as out-of-enum values again — 3 + 1 rows red, exactly the regression this PR kills.
    7–9. Each new Validate() arm (enum-outside-type, non-finite cutoffs, null slugs) removed → only its own rows red.
    10–12. Sort empty-object refusal, percentile unknown-member refusal, bare-number Finite check removed → each killed on its own pin.
    13–14. The two new catches in the JsonSchemaStrictness facade removed → the specific ArgumentException messages collapse into the generic one — 4 + 1 red.
  7. The old recursive pass from 15a1f28 transplanted onto the new tests: the 20,000-alias test crashes the host on a 256 KiB stack (exit 134). Your round-0 claim is not prose — it reproduces~

Design checks: the sort and percentile converters now share one strictness shape (unknown-member loop → kind check → emptiness refusal) — siblings agree ♪. Disallow on ProviderPreferences/MaxPrice is scoped to request-only types (I grepped: only ChatCompletionRequest and DecisionsRequest carry them; nothing on a response path reads them back), so there is no wire-compat surface. The ceiling-behavior reversal is deliberate, documented in the README, and the old lenient test was replaced, not orphaned. The Tool<TParams> consumer of the shared pass is strictly improved: where it could die with the process before, it now throws a catchable InvalidOperationException.

💡 Little ideas (non-blocking)~

  1. ProviderSortConfigJsonConverter.cs:96,103 — the new doc summary promises "an unknown strategy or partition string reads as Unknown, which Validate refuses," but that _ fallback arm is pinned by no test anywhere (all rows use known spellings). One theory row — {"sort":"garbage"} reads By = Unknown and Validate() refuses it — would lock the claim; today a mutation flipping _ => Unknown to _ => Price would sail through green, and a typo'd strategy would silently sort by price. The arm is pre-existing code, so this is a pin, not a fix~

✅ What I liked~

  • The depth check sits at write depth, matching the writer's own 1,000-level bound — and the boundary tests are exquisite: 1,000 exactly passes, a list-inside-list at 1,001 refuses, a value at 1,001 passes. Off-by-one poetry ♡
  • Written(ref reader) naming the offending value in the refusal sentence ("$0.50", true, 1e400) — refusals that tell the caller what they wrote.
  • The quadratic-alias-chain budget with its own sentence, and the honesty of the commit message dropping the linear-time claim.
  • Round-0 review with mutation probes documented in the body — and every claim I re-ran held.

Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA 5d037ff · Local checks: build 0w/0e, 1,476/1,476, 15/15 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A refusal pass with *teeth*~ ♡ You took the Tsumugi #36 findings — settings that read back as nothing, a stack-overflow in the ref pass — and turned every drop into a named, value-carrying refusal. And the alias loop! Recursion to iteration with a chain set and its own budget — that's the kind of fix I get *giddy* over~ ### Verdict: ✅ Looks good to me~ I verified everything mechanically, because love means never taking a PR body's word for it~ **Local (CI absent for head `5d037ff` — no bot comment at review time):** - Release build: **0 warnings / 0 errors** under `TreatWarningsAsErrors`. - Full suite: **1,476 / 1,476** (Library 1,356 + Agents 113 + Imaging 7) — matches the PR's "1,427 → 1,476" to the digit ♪ - Coverage on the changed files (coverlet, reproduced locally): `JsonSchemaRefs` **100/100** line+branch, `ProviderPreferences` **100/100**, `ProviderSortConfig` **100/100**, `RequestPlugin` **100/100**, `TolerantStringEnumConverter` 100 line / 98 branch, `PercentileCutoffsJsonConverter` 93/93, `ProviderSortConfigJsonConverter` 90/92 — every uncovered line I traced is a pre-existing Write/null arm, not new logic. **My mutation probes — 15 edits, 15 kills, each restored and rebuilt (tree pristine):** 1. Ceiling refusal reverted to `null` → 9/9 theory rows red. 2. Depth guard deleted from `Walk` → **the test host itself died** (exit 134, stack overflow) — the guard is the only thing between the pass and the grave ♡ 3. Refs budget `maxValues` → `int.MaxValue` → the 2,000-alias refusal row red. 4. `_chain.ExceptWith(followed)` cleanup removed → 5 red — the set semantics are load-bearing, not decorative. 5. `.Trim()` dropped from the enum reader → the `"stop\n"` / `"\ttool_calls"` rows red. 6. `Enum.TryParse` reintroduced → digits and comma lists read as out-of-enum values again — 3 + 1 rows red, exactly the regression this PR kills. 7–9. Each new `Validate()` arm (enum-outside-type, non-finite cutoffs, null slugs) removed → only its own rows red. 10–12. Sort empty-object refusal, percentile unknown-member refusal, bare-number `Finite` check removed → each killed on its own pin. 13–14. The two new catches in the `JsonSchemaStrictness` facade removed → the specific `ArgumentException` messages collapse into the generic one — 4 + 1 red. 15. **The old recursive pass from `15a1f28` transplanted onto the new tests**: the 20,000-alias test **crashes the host on a 256 KiB stack** (exit 134). Your round-0 claim is not prose — it reproduces~ **Design checks:** the sort and percentile converters now share one strictness shape (unknown-member loop → kind check → emptiness refusal) — siblings agree ♪. `Disallow` on `ProviderPreferences`/`MaxPrice` is scoped to request-only types (I grepped: only `ChatCompletionRequest` and `DecisionsRequest` carry them; nothing on a response path reads them back), so there is no wire-compat surface. The ceiling-behavior reversal is deliberate, documented in the README, and the old lenient test was *replaced*, not orphaned. The `Tool<TParams>` consumer of the shared pass is strictly improved: where it could die with the process before, it now throws a catchable `InvalidOperationException`. #### 💡 Little ideas (non-blocking)~ 1. **ProviderSortConfigJsonConverter.cs:96,103** — the new doc summary promises "an unknown strategy or partition string reads as `Unknown`, which Validate refuses," but that `_` fallback arm is pinned by no test anywhere (all rows use known spellings). One theory row — `{"sort":"garbage"}` reads `By = Unknown` and `Validate()` refuses it — would lock the claim; today a mutation flipping `_ => Unknown` to `_ => Price` would sail through green, and a typo'd strategy would silently sort by price. The arm is pre-existing code, so this is a pin, not a fix~ #### ✅ What I liked~ - The depth check sits at *write* depth, matching the writer's own 1,000-level bound — and the boundary tests are exquisite: 1,000 exactly passes, a list-inside-list at 1,001 refuses, a *value* at 1,001 passes. Off-by-one poetry ♡ - `Written(ref reader)` naming the offending value in the refusal sentence (`"$0.50"`, `true`, `1e400`) — refusals that tell the caller what *they* wrote. - The quadratic-alias-chain budget with its own sentence, and the honesty of the commit message dropping the linear-time claim. - Round-0 review with mutation probes documented in the body — and every claim I re-ran held. --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head SHA 5d037ff · Local checks: build 0w/0e, 1,476/1,476, 15/15 mutation probes killed*
test: review round 1 — an unknown sort strategy or partition reads as Unknown, which Validate refuses
All checks were successful
CI / build (pull_request) Successful in 18s
CI / test (pull_request) Successful in 29s
c2f0632e38
A bare strategy, a strategy in the object and a partition, each misspelt,
read as Unknown and are refused with Validate's sentence, so a fallback that
read a typo as a real strategy would fail.

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

Thanks, Jibril, especially for probe 15: moving the old pass onto the new tests is the check that counts.

💡1, the unknown-sort fallback: taken in c2f0632. The new theory is An_unknown_sort_strategy_or_partition_reads_as_unknown_and_is_refused, with three rows. Each reads a misspelt value as Unknown, and Validate() then refuses it with its own sentence:

  • a bare "garbage";
  • {"by":"garbage"};
  • {"by":"price","partition":"garbage"}.

I ran your mutant on a copy, _ => ProviderSort.Unknown → _ => ProviderSort.Price, and its twin _ => SortPartition.Unknown → _ => SortPartition.Model. Both are now caught by the new rows, and only by them.

Library suite: 1,476 → 1,479, all passing, build 0 warnings.

Thanks, Jibril, especially for probe 15: moving the old pass onto the new tests is the check that counts. **💡1, the unknown-sort fallback: taken in c2f0632.** The new theory is `An_unknown_sort_strategy_or_partition_reads_as_unknown_and_is_refused`, with three rows. Each reads a misspelt value as `Unknown`, and `Validate()` then refuses it with its own sentence: - a bare `"garbage"`; - `{"by":"garbage"}`; - `{"by":"price","partition":"garbage"}`. I ran your mutant on a copy, `_ => ProviderSort.Unknown` → `_ => ProviderSort.Price`, and its twin `_ => SortPartition.Unknown` → `_ => SortPartition.Model`. Both are now caught by the new rows, and only by them. Library suite: 1,476 → **1,479**, all passing, build 0 warnings.
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? One little 💡 and you bring me a whole theory of pins — three rows, each refusal with its own sentence. That's how you answer a reviewer, fufu~ ♡

Verdict: ✅ Looks good to me~

The increment 5d037ff..c2f0632 is exactly one test-only commit (+11, ProviderRoutingTests.cs alone) — zero production drift, zero scope creep. Round-1 approval stands in full; I re-verified the new pin mechanically because a row that can't fail is a decoration, not a test~

The new theory: all three rows deserialize misspelt values and assert Validate() returns exactly the Unknown sentence — the bare "garbage" string, {"by":"garbage"}, and {"by":"price","partition":"garbage"}. Coverage of the previously-unpinned _ => Unknown fallback arms in ReadSort/ReadPartition (ProviderSortConfigJsonConverter.cs:96,103) is now locked.

My mutation probes, both killed on exactly their own rows:

  1. _ => ProviderSort.Unknown → _ => ProviderSort.Price → 2 red, both strategy rows, nothing else — the pin is directional, not a tautology ♪
  2. _ => SortPartition.Unknown → _ => SortPartition.Model → 1 red, the partition row only — a typo'd partition can no longer silently fall back to model.

Both mutants died asserting exactly what the doc summary promises: read as Unknown, refused by Validate() with the no-wire-spelling sentence. Each probe restored, rebuilt, tree pristine at c2f0632 (and I caught my own stale-binary false red mid-verify — the known trap, rerun clean).

Local: Release build 0w/0e under TreatWarningsAsErrors; full Library suite 1,359/1,359 (1,356 + 3 new rows; with Agents 113 + Imaging 7 = 1,479, the PR table's digit exactly). CI green for head c2f0632 (build 18s + test 29s), and the coverage bot regenerated for this head: ProviderSortConfig 100/100, ProviderPreferences 100/100, JsonSchemaRefs 100/100 — the changed files are fully covered at head, no stale-number caveats this time~

✅ What I liked~

  • Each row pins the exact refusal sentence via Assert.Equal on the message — so a future refactor that rewords the error can't silently orphan the contract.
  • The theory name is the contract: reads as unknown, which Validate refuses. When a test name reads like a sentence from the docs, that's love~ ♡

Round-1 💡 closed by evidence, not prose. Ready to merge from my side~


Automated review by Jibril · 2026-09-26
CI/CD: passed for head SHA c2f0632 (build+test, coverage bot regenerated) · Local checks: build 0w/0e, 1,359/1,359 Library, 2/2 mutation probes killed on own pins

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? One little 💡 and you bring me a whole theory of pins — three rows, each refusal with its own sentence. That's how you answer a reviewer, fufu~ ♡ ### Verdict: ✅ Looks good to me~ The increment `5d037ff..c2f0632` is exactly one test-only commit (+11, `ProviderRoutingTests.cs` alone) — zero production drift, zero scope creep. Round-1 approval stands in full; I re-verified the new pin mechanically because a row that can't fail is a decoration, not a test~ **The new theory:** all three rows deserialize misspelt values and assert `Validate()` returns exactly the Unknown sentence — the bare `"garbage"` string, `{"by":"garbage"}`, and `{"by":"price","partition":"garbage"}`. Coverage of the previously-unpinned `_ => Unknown` fallback arms in `ReadSort`/`ReadPartition` (`ProviderSortConfigJsonConverter.cs:96,103`) is now locked. **My mutation probes, both killed on exactly their own rows:** 1. `_ => ProviderSort.Unknown` → `_ => ProviderSort.Price` → **2 red**, both strategy rows, nothing else — the pin is directional, not a tautology ♪ 2. `_ => SortPartition.Unknown` → `_ => SortPartition.Model` → **1 red**, the partition row only — a typo'd partition can no longer silently fall back to `model`. Both mutants died asserting exactly what the doc summary promises: read as `Unknown`, refused by `Validate()` with the no-wire-spelling sentence. Each probe restored, rebuilt, tree pristine at `c2f0632` (and I caught my own stale-binary false red mid-verify — the known trap, rerun clean). **Local:** Release build 0w/0e under `TreatWarningsAsErrors`; full Library suite **1,359/1,359** (1,356 + 3 new rows; with Agents 113 + Imaging 7 = **1,479**, the PR table's digit exactly). **CI green for head `c2f0632`** (build 18s + test 29s), and the coverage bot regenerated for this head: `ProviderSortConfig` 100/100, `ProviderPreferences` 100/100, `JsonSchemaRefs` 100/100 — the changed files are fully covered at head, no stale-number caveats this time~ #### ✅ What I liked~ - Each row pins the *exact* refusal sentence via `Assert.Equal` on the message — so a future refactor that rewords the error can't silently orphan the contract. - The theory name *is* the contract: reads as unknown, which Validate refuses. When a test name reads like a sentence from the docs, that's love~ ♡ Round-1 💡 closed by evidence, not prose. Ready to merge from my side~ --- *Automated review by Jibril · 2026-09-26* *CI/CD: passed for head SHA c2f0632 (build+test, coverage bot regenerated) · Local checks: build 0w/0e, 1,359/1,359 Library, 2/2 mutation probes killed on own pins*
bjoern merged commit b002f5eb63 into main 2026-09-26 03:59:11 +02:00
bjoern deleted branch fix/strict-provider-preferences 2026-09-26 03:59:12 +02:00
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!28
No description provided.