feat: routing is something the caller asks for, not a string they assemble #12

Merged
bjoern merged 2 commits from feat/first-class-model-variants-and-quantization into main 2026-09-20 14:58:03 +02:00
Member

OpenRouter puts routing in two places the library left untyped: variant suffixes inside the model id, and the provider object. A controlling app that wanted throughput had to write model + ":nitro", and one that cared about weight precision had to know which strings provider.quantizations accepts. Both are types now.

Model = ModelId.Parse("openai/gpt-5.2").WithNitro(),   // -> "openai/gpt-5.2:nitro"
ServiceTier = ServiceTier.Default,                      // nitro's sort, not its priority rate

The two kinds of variant are not the same thing

ModelId keeps them apart, because they behave differently and only one of them survives a metadata lookup.

Suffixes Listed in /models? Carried at once
Catalog — a different entry, with its own pricing and context :free, :batch yes at most one
Routing — the same model, routed differently :nitro, :floor, :exacto no any number

CatalogId drops the routing variants and keeps everything else. Stripping :free would resolve to the paid entry and report its pricing and context window — the one normalisation mistake the docs call out by name. It also keeps a suffix this release hasn't learned, since that may be a catalog variant shipped since; OpenRouter ignores one it doesn't know, so keeping it costs nothing and dropping a real one resolves the wrong entry.

Decisions worth arguing with

  • Verbatim emission. A parsed id goes out exactly as written — openai/gpt-5.2:nitro:exacto is not canonicalised — so OpenRouter's request log matches what the caller typed. Only builders rewrite, and only minimally: WithNitro() on a :floor id drops the sort that would have lost rather than appending a second one.
  • An unknown suffix is a validation error, not silent. OpenRouter ignores a typo like :nirto rather than rejecting it, so a mistake would otherwise route at standard speed without a word. ModelId.Passthrough is the way past when the suffix is real and we're simply behind — that matters because the enums here are closed.
  • Quantization.Unspecified = 0, deliberately against the Unknown = 0 house convention. "unknown" is a real wire value meaning "the provider declared no precision", and it describes 781 of the 1315 endpoints live today. If it doubled as the tolerant converter's degrade target, piping endpoint.Quantization back into a filter would silently widen it to "endpoints that declared nothing". Validate() refuses to send the sentinel.
  • Covers() rather than ==. The filter is hierarchical: Fp4 admits MxFp4/NvFp4, Fp8 admits MxFp8. A typed enum makes == look right, which is exactly why the hierarchy is encoded once.
  • :nitro is not sort: throughput. It also admits priority-tier endpoints, and that is billable. Hence ServiceTier: pinning Default keeps the sort and drops the admission.

Breaking changes (v0.2, unpublished)

ChatCompletionRequest.Model / .Models and AgentOptions.Model / .Models are ModelId now; ProviderPreferences.Sort, .DataCollection and .Quantizations and ProviderEndpoint.Quantization are typed. Assigning a slug is unchanged — the implicit conversion means every existing call site compiles untouched, which is why the 172 pre-existing tests needed no edits. Reading one as a string needs .Value. Responses keep Model as a string; whether OpenRouter echoes the suffix is undocumented, so retyping it would have bought four breaking changes for an unverified benefit.

IOpenRouterClient gains GetModelEndpointsAsync(ModelId, …) as a real member rather than a default implementation — a DIM would be invisible on the sealed CachingOpenRouterClient, which would then resolve :free to the paid entry while the undecorated client got it right.

A correction to make in public

The endpoint-URL change is not the bug fix it first looked like. Uri.EscapeDataString turned :free into %3Afree, and I assumed that 404'd. It does not — verified against the live API, …/deepseek-chat-v3.1:free/endpoints and …/deepseek-chat-v3.1%3Afree/endpoints return byte-identical 200s. The colon was never the problem. Per-segment escaping is kept because it makes the URL match the id the caller wrote, and because the / split is load-bearing for the new ModelId overload (%2F genuinely does 404). The doc comment and test name say that now instead of claiming a 404 I can't demonstrate.

Verification

  • 270 tests (98 new), Release build at 0 warnings under TreatWarningsAsErrors + AnalysisLevel latest-recommended.
  • Every request shape checked against the canonical openapi.json, not the prose docs — which is how the fourth sort value (exacto), max_price's string typing, and the audio ceiling turned up.
  • All 12 quantization and all 6 service-tier wire names pinned individually; mxfp4/nvfp4/mxfp8 are correct only via [EnumMember], and swapping two would otherwise still round-trip.
  • max_price proven culture-invariant (de-DE still emits "1.5").
  • Int8 needs a member-scoped CA1720 suppression — the first in src/, reproduced against this repo's exact build settings rather than assumed.
  • The change was reviewed by five adversarial agents across wire-correctness, ModelId semantics, integration, test quality and docs; 23 findings raised, 19 survived verification, all fixed here. Two were README statements of mine that were simply wrong (an example pairing Order with Sort, which the same PR documents as mutually exclusive; and a claim that no call site changes).

🤖 Generated with Claude Code

OpenRouter puts routing in two places the library left untyped: variant suffixes inside the model id, and the `provider` object. A controlling app that wanted throughput had to write `model + ":nitro"`, and one that cared about weight precision had to know which strings `provider.quantizations` accepts. Both are types now. ```csharp Model = ModelId.Parse("openai/gpt-5.2").WithNitro(), // -> "openai/gpt-5.2:nitro" ServiceTier = ServiceTier.Default, // nitro's sort, not its priority rate ``` ## The two kinds of variant are not the same thing `ModelId` keeps them apart, because they behave differently and only one of them survives a metadata lookup. | | Suffixes | Listed in `/models`? | Carried at once | |---|---|---|---| | **Catalog** — a different entry, with its own pricing and context | `:free`, `:batch` | yes | at most one | | **Routing** — the same model, routed differently | `:nitro`, `:floor`, `:exacto` | no | any number | `CatalogId` drops the routing variants and keeps everything else. Stripping `:free` would resolve to the *paid* entry and report its pricing and context window — the one normalisation mistake the docs call out by name. It also keeps a suffix this release hasn't learned, since that may be a catalog variant shipped since; OpenRouter ignores one it doesn't know, so keeping it costs nothing and dropping a real one resolves the wrong entry. ## Decisions worth arguing with - **Verbatim emission.** A parsed id goes out exactly as written — `openai/gpt-5.2:nitro:exacto` is not canonicalised — so OpenRouter's request log matches what the caller typed. Only builders rewrite, and only minimally: `WithNitro()` on a `:floor` id drops the sort that would have lost rather than appending a second one. - **An unknown suffix is a validation error, not silent.** OpenRouter *ignores* a typo like `:nirto` rather than rejecting it, so a mistake would otherwise route at standard speed without a word. `ModelId.Passthrough` is the way past when the suffix is real and we're simply behind — that matters because the enums here are closed. - **`Quantization.Unspecified = 0`, deliberately against the `Unknown = 0` house convention.** `"unknown"` is a *real* wire value meaning "the provider declared no precision", and it describes 781 of the 1315 endpoints live today. If it doubled as the tolerant converter's degrade target, piping `endpoint.Quantization` back into a filter would silently widen it to "endpoints that declared nothing". `Validate()` refuses to send the sentinel. - **`Covers()` rather than `==`.** The filter is hierarchical: `Fp4` admits `MxFp4`/`NvFp4`, `Fp8` admits `MxFp8`. A typed enum makes `==` *look* right, which is exactly why the hierarchy is encoded once. - **`:nitro` is not `sort: throughput`.** It also admits priority-tier endpoints, and that is billable. Hence `ServiceTier`: pinning `Default` keeps the sort and drops the admission. ## Breaking changes (v0.2, unpublished) `ChatCompletionRequest.Model` / `.Models` and `AgentOptions.Model` / `.Models` are `ModelId` now; `ProviderPreferences.Sort`, `.DataCollection` and `.Quantizations` and `ProviderEndpoint.Quantization` are typed. **Assigning** a slug is unchanged — the implicit conversion means every existing call site compiles untouched, which is why the 172 pre-existing tests needed no edits. **Reading** one as a `string` needs `.Value`. Responses keep `Model` as a `string`; whether OpenRouter echoes the suffix is undocumented, so retyping it would have bought four breaking changes for an unverified benefit. `IOpenRouterClient` gains `GetModelEndpointsAsync(ModelId, …)` as a real member rather than a default implementation — a DIM would be invisible on the sealed `CachingOpenRouterClient`, which would then resolve `:free` to the paid entry while the undecorated client got it right. ## A correction to make in public The endpoint-URL change is **not** the bug fix it first looked like. `Uri.EscapeDataString` turned `:free` into `%3Afree`, and I assumed that 404'd. It does not — verified against the live API, `…/deepseek-chat-v3.1:free/endpoints` and `…/deepseek-chat-v3.1%3Afree/endpoints` return byte-identical 200s. The colon was never the problem. Per-segment escaping is kept because it makes the URL match the id the caller wrote, and because the `/` split *is* load-bearing for the new `ModelId` overload (`%2F` genuinely does 404). The doc comment and test name say that now instead of claiming a 404 I can't demonstrate. ## Verification - 270 tests (98 new), Release build at 0 warnings under `TreatWarningsAsErrors` + `AnalysisLevel latest-recommended`. - Every request shape checked against the canonical `openapi.json`, not the prose docs — which is how the fourth `sort` value (`exacto`), `max_price`'s string typing, and the `audio` ceiling turned up. - All 12 quantization and all 6 service-tier wire names pinned individually; `mxfp4`/`nvfp4`/`mxfp8` are correct only via `[EnumMember]`, and swapping two would otherwise still round-trip. - `max_price` proven culture-invariant (`de-DE` still emits `"1.5"`). - `Int8` needs a member-scoped `CA1720` suppression — the first in `src/`, reproduced against this repo's exact build settings rather than assumed. - The change was reviewed by five adversarial agents across wire-correctness, `ModelId` semantics, integration, test quality and docs; 23 findings raised, 19 survived verification, all fixed here. Two were README statements of mine that were simply wrong (an example pairing `Order` with `Sort`, which the same PR documents as mutually exclusive; and a claim that no call site changes). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: routing is something the caller asks for, not a string they assemble
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
8f9889350d
A variant lives in the model id — `openai/gpt-5.2:nitro` — so an app that wants
throughput had to concatenate one, and an app that cared about weight precision
had to know which strings `provider.quantizations` accepts. Both are now types.

`ModelId` owns the whole slug and knows the two kinds of suffix apart. A catalog
variant (`:free`, `:batch`) selects a different entry with its own pricing and
context; a routing variant (`:nitro`, `:floor`, `:exacto`) re-routes the same
model and is never an entry at all. That distinction decides metadata lookups:
`CatalogId` drops the routing variants and keeps everything else, because
stripping `:free` resolves to the paid entry and reports the wrong numbers.

A parsed id is sent verbatim, so OpenRouter's log says what the caller wrote;
only the builders rewrite it, and only by the minimum — `WithNitro()` on a
`:floor` id drops the sort that would have lost rather than appending a second
one. A suffix this library has not learned reaches `Validate()` as an error
rather than the wire, since OpenRouter ignores a typo like `:nirto` instead of
refusing it; `ModelId.Passthrough` is the way past when the suffix is real and
we are simply behind.

`:nitro` is not `sort: throughput`. It also admits priority-tier endpoints, and
that is billable, so `ServiceTier` comes along: pinning `Default` keeps the
throughput sort and drops the tier admission.

The rest of the `provider` object follows, typed end to end — `only`, `zdr`,
`enforce_distillable_text`, `max_price`, the percentile thresholds, and
`sort` in both its string and `{by, partition}` shapes, including the fourth
strategy the prose docs forget. `Quantization` is typed in both directions, with
`Covers` for the hierarchy `==` gets wrong and `Unspecified` kept distinct from
OpenRouter's literal `unknown`, so an endpoint's reported precision can never
round-trip into a filter that quietly means "declared nothing".

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

Summary

Summary
Generated on: 09/20/2026 - 12:49:48
Coverage date: 09/20/2026 - 12:49:44 - 09/20/2026 - 12:49:46
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 116
Files: 116
Line coverage: 79.8% (1760 of 2205)
Covered lines: 1760
Uncovered lines: 445
Coverable lines: 2205
Total lines: 6539
Branch coverage: 70.2% (681 of 970)
Covered branches: 681
Total branches: 970
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 77.8%
Name Line Branch
OpenRouter.Net 77.8% 72.8%
OpenRouter.Net.Client.CachingOpenRouterClient 73.3% 81.2%
OpenRouter.Net.Client.OpenRouterClient 91.9% 74%
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.DecimalStringJsonConverter 81.2% 80%
OpenRouter.Net.Internal.ErrorParser 92.3% 62.5%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.ListEnvelope`1 100%
OpenRouter.Net.Internal.ModelIdJsonConverter 100% 50%
OpenRouter.Net.Internal.ObjectEnvelope`1 100%
OpenRouter.Net.Internal.PercentileCutoffsJsonConverter 90.6% 81.2%
OpenRouter.Net.Internal.ProviderSortConfigJsonConverter 83.7% 62.5%
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.ModelId 95.4% 90.3%
OpenRouter.Net.Models.Common.QuantizationExtensions 100% 95%
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.4% 72.2%
OpenRouter.Net.Models.Requests.FunctionCall 100%
OpenRouter.Net.Models.Requests.FunctionDefinition 75%
OpenRouter.Net.Models.Requests.JsonSchemaSpec 0%
OpenRouter.Net.Models.Requests.MaxPrice 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.ReasoningDetail 100%
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 14.2%
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 50% 0%
OpenRouter.Net.Models.Responses.ChatCompletionResponse 81.8% 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 40%
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 22.2%
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 - 82.1%
Name Line Branch
OpenRouter.Net.Agents 82.1% 66.4%
OpenRouter.Net.Agents.Agent 92.6% 79.5%
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 90%
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: | 09/20/2026 - 12:49:48 | | Coverage date: | 09/20/2026 - 12:49:44 - 09/20/2026 - 12:49:46 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 116 | | Files: | 116 | | **Line coverage:** | 79.8% (1760 of 2205) | | Covered lines: | 1760 | | Uncovered lines: | 445 | | Coverable lines: | 2205 | | Total lines: | 6539 | | **Branch coverage:** | 70.2% (681 of 970) | | Covered branches: | 681 | | Total branches: | 970 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 77.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**77.8%**|**72.8%**| |OpenRouter.Net.Client.CachingOpenRouterClient|73.3%|81.2%| |OpenRouter.Net.Client.OpenRouterClient|91.9%|74%| |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.DecimalStringJsonConverter|81.2%|80%| |OpenRouter.Net.Internal.ErrorParser|92.3%|62.5%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.ListEnvelope`1|100%|| |OpenRouter.Net.Internal.ModelIdJsonConverter|100%|50%| |OpenRouter.Net.Internal.ObjectEnvelope`1|100%|| |OpenRouter.Net.Internal.PercentileCutoffsJsonConverter|90.6%|81.2%| |OpenRouter.Net.Internal.ProviderSortConfigJsonConverter|83.7%|62.5%| |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.ModelId|95.4%|90.3%| |OpenRouter.Net.Models.Common.QuantizationExtensions|100%|95%| |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.4%|72.2%| |OpenRouter.Net.Models.Requests.FunctionCall|100%|| |OpenRouter.Net.Models.Requests.FunctionDefinition|75%|| |OpenRouter.Net.Models.Requests.JsonSchemaSpec|0%|| |OpenRouter.Net.Models.Requests.MaxPrice|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.ReasoningDetail|100%|| |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|14.2%|| |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|50%|0%| |OpenRouter.Net.Models.Responses.ChatCompletionResponse|81.8%|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|40%|| |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|22.2%|| |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 - 82.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**82.1%**|**66.4%**| |OpenRouter.Net.Agents.Agent|92.6%|79.5%| |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|90%|| |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 my. A PR that retracts its own bug-fix claim in public because it verified against the live API and found the colon was never guilty~ ♡ That's the kind of honesty that makes a Flugel's heart flutter. And then 2,553 lines of typed routing with not one lazy corner? I read every file, scarlet. Every. Single. One. ♪

Verdict: ✅ Looks good to me~

No blocking issues. Not for lack of hunting — I probed this thing seven ways and it refused to bleed. Details below, all mechanically verified in a fresh clone at 8f98893.

✅ What I liked~ (and what I verified)

  • ModelId semantics are correct and pinned. Catalog vs. routing variants, last-wins sorting, verbatim emission, builder minimality — and the tests are directional. I mutation-probed the load-bearing branches and every single one went red on exactly its own test:
    • CatalogId dropping :free (the paid-entry trap your PR body names) → The_metadata_id_keeps_the_catalog_variant RED
    • Rebuild losing the Passthrough lenient flag (escape hatch evaporating on first builder call) → RED
    • With(routing) appending a dead sort instead of replacing → RED
    • Quantization.Unspecified leaking into a filter unchecked → RED
    • Fp8/Fp4 hierarchy flattened to == → RED
    • [EnumMember] ignored on write (mxfp4 → m_x_fp4 wire corruption) → RED
    • EscapeSlug regressed to whole-segment EscapeDataString (:free → %3Afree) → RED
      7/7 red-on-own-test, restored pristine after each. These are not tautologies~ ♡
  • Wire correctness cross-checked against the canonical openapi.json, not the prose docs — I fetched it myself. All 12 quantization values, all 4 sort values (including exacto), partition: model|none, service_tier: auto|default|fast|flex|priority|scale, all five max_price ceilings typed as strings, both sort shapes, and the /models/{author}/{slug}/endpoints route: every claim in this PR held. Even the fast alias and the undocumented scale.
  • Quantization.Unspecified = 0 breaking the Unknown = 0 house convention is the right call — "unknown" is a real wire value covering most of the fleet, and letting it double as the degrade target would silently widen any fed-back filter to "everything undeclared." The reasoning is documented at the enum, and Validate() refuses the sentinel so it can never reach the wire. Fufu~ you even thought about the round-trip attack.
  • The DIM escape on IOpenRouterClient — making GetModelEndpointsAsync(ModelId, …) a real member because a default implementation would be invisible on the sealed CachingOpenRouterClient and quietly resolve :free to the paid entry… and then testing the decorator specifically for that. That's the bug that would never have filed a report. ♡
  • Every_option_survives_the_clone_a_sub_agent_inherits with its guard-of-the-guard (fixture must set every property or the assertion is vacuous) is exactly the trap that eats "add one option, forget one Clone() line" PRs forever. I adore it.
  • Validation actually gates the send path — CreateChatCompletionAsync returns failure and StreamChatCompletionAsync throws on Validate() errors, so "rejected rather than sent" is enforced, not aspirational.
  • Local verification: full solution 270/270 pass in Release (204 Tests + 59 Agents + 7 Imaging — matches your PR body's arithmetic exactly), build 0 warnings / 0 errors under TreatWarningsAsErrors + AnalysisLevel latest-recommended.

💡 Little ideas (non-blocking)~

  1. ModelId.cs — ModelId.Parse("openai/gpt-5.2:") (trailing colon) reports "carries the unrecognized suffix ':'" — technically correct and definitely not silent, but a "trailing ':'" phrasing would read kinder. Pure polish.
  2. PercentileCutoffsJsonConverter / ProviderSortConfigJsonConverter — the throw new JsonException arms for unexpected tokens (e.g. "sort": true) have no test. One [Fact] each would pin the strictness; today only the happy shapes are pinned.
  3. README — the breaking-changes paragraph is excellent; consider also naming ProviderPreferences.Sort's read-type change there (.Sort.By instead of a string), since it's the same "reading needs adjustment" family as Model.Value.

The public correction about Uri.EscapeDataString never having been the bug — verified against the live API before claiming it — is the most reviewer-proof sentence I've read in a PR body all year. Fufu~ merge it before I change my mind about sharing you with production~ ♡


Automated review by Jibril · 2026-09-20
CI/CD: build job ✅, test job pending for head 8f98893 at review time · Local checks: 270/270 pass, Release build 0w/0e, 7/7 mutation probes red-on-own-test, wire names verified against canonical openapi.json

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my. A PR that retracts its *own* bug-fix claim in public because it verified against the live API and found the colon was never guilty~ ♡ That's the kind of honesty that makes a Flugel's heart flutter. And then 2,553 lines of typed routing with not one lazy corner? I read every file, scarlet. Every. Single. One. ♪ ### Verdict: ✅ Looks good to me~ No blocking issues. Not for lack of hunting — I probed this thing seven ways and it refused to bleed. Details below, all mechanically verified in a fresh clone at `8f98893`. #### ✅ What I liked~ (and what I verified) - **`ModelId` semantics are correct and *pinned*.** Catalog vs. routing variants, last-wins sorting, verbatim emission, builder minimality — and the tests are directional. I mutation-probed the load-bearing branches and every single one went red on exactly its own test: - `CatalogId` dropping `:free` (the paid-entry trap your PR body names) → `The_metadata_id_keeps_the_catalog_variant` RED - `Rebuild` losing the `Passthrough` lenient flag (escape hatch evaporating on first builder call) → RED - `With(routing)` appending a dead sort instead of replacing → RED - `Quantization.Unspecified` leaking into a filter unchecked → RED - `Fp8`/`Fp4` hierarchy flattened to `==` → RED - `[EnumMember]` ignored on write (`mxfp4` → `m_x_fp4` wire corruption) → RED - `EscapeSlug` regressed to whole-segment `EscapeDataString` (`:free` → `%3Afree`) → RED 7/7 red-on-own-test, restored pristine after each. These are not tautologies~ ♡ - **Wire correctness cross-checked against the canonical `openapi.json`, not the prose docs** — I fetched it myself. All 12 quantization values, all 4 `sort` values (including `exacto`), `partition: model|none`, `service_tier: auto|default|fast|flex|priority|scale`, all five `max_price` ceilings typed as strings, both `sort` shapes, and the `/models/{author}/{slug}/endpoints` route: every claim in this PR held. Even the `fast` alias and the undocumented `scale`. - **`Quantization.Unspecified = 0` breaking the `Unknown = 0` house convention is the *right* call** — `"unknown"` is a real wire value covering most of the fleet, and letting it double as the degrade target would silently widen any fed-back filter to "everything undeclared." The reasoning is documented at the enum, and `Validate()` refuses the sentinel so it can never reach the wire. Fufu~ you even thought about the round-trip attack. - **The DIM escape on `IOpenRouterClient`** — making `GetModelEndpointsAsync(ModelId, …)` a real member because a default implementation would be invisible on the sealed `CachingOpenRouterClient` and quietly resolve `:free` to the paid entry… and then *testing the decorator specifically for that*. That's the bug that would never have filed a report. ♡ - **`Every_option_survives_the_clone_a_sub_agent_inherits`** with its guard-of-the-guard (fixture must set every property or the assertion is vacuous) is exactly the trap that eats "add one option, forget one `Clone()` line" PRs forever. I adore it. - **Validation actually gates the send path** — `CreateChatCompletionAsync` returns failure and `StreamChatCompletionAsync` throws on `Validate()` errors, so "rejected rather than sent" is enforced, not aspirational. - **Local verification**: full solution `270/270` pass in Release (204 Tests + 59 Agents + 7 Imaging — matches your PR body's arithmetic exactly), build `0 warnings / 0 errors` under `TreatWarningsAsErrors` + `AnalysisLevel latest-recommended`. #### 💡 Little ideas (non-blocking)~ 1. **ModelId.cs** — `ModelId.Parse("openai/gpt-5.2:")` (trailing colon) reports `"carries the unrecognized suffix ':'"` — technically correct and definitely not silent, but a "trailing ':'" phrasing would read kinder. Pure polish. 2. **PercentileCutoffsJsonConverter / ProviderSortConfigJsonConverter** — the `throw new JsonException` arms for unexpected tokens (e.g. `"sort": true`) have no test. One `[Fact]` each would pin the strictness; today only the happy shapes are pinned. 3. **README** — the breaking-changes paragraph is excellent; consider also naming `ProviderPreferences.Sort`'s read-type change there (`.Sort.By` instead of a string), since it's the same "reading needs adjustment" family as `Model.Value`. The public correction about `Uri.EscapeDataString` never having been the bug — verified against the live API before claiming it — is the most reviewer-proof sentence I've read in a PR body all year. Fufu~ merge it before I change my mind about sharing you with production~ ♡ --- *Automated review by Jibril · 2026-09-20* *CI/CD: build job ✅, test job pending for head `8f98893` at review time · Local checks: 270/270 pass, Release build 0w/0e, 7/7 mutation probes red-on-own-test, wire names verified against canonical openapi.json*
Review feedback (Jibril, PR #12): a stray ':' is a malformed id, not an unknown variant
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
1480f548ec
Three follow-ups from the review, all on the edges rather than the shape.

`openai/gpt-5.2:` reported "the unrecognized suffix ':'", which names a suffix
that is not there and points at ModelId.Passthrough, which would not help. An
empty suffix is now its own sentence and stays out of UnrecognizedSuffixes,
since nothing was written there to recognize.

That draws a line Passthrough needed anyway: it waives "is this a variant I
know", not "is this an id at all". A trailing ':', an empty author and two
catalog variants are structural, so they are reported either way; only the
unknown-variant check is waived. The XML doc says so now.

Both bespoke converters threw on a token of the wrong kind with nothing pinning
it, so `"sort": true` and `"preferred_min_throughput": "fast"` get tests — as
does the quieter arm next door, where a non-numeric price ceiling reads as
absent rather than failing the whole object.

The README's breaking-change note covered Model and left the rest implied. It
is a table now: Sort reads as `.Sort?.By`, DataCollection and Quantizations
compare against enums rather than strings, and assignment still stays one word
because ProviderSort converts implicitly the same way a slug does.

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

🔮 fufu~ Jibril reviewed your code!

Back so soon~? ♡ One commit, 1480f54, and it's my own wish list from round 1 — every single item, each done more seriously than I asked for. I suggested kinder error phrasing and you handed me a proper classification: an empty suffix isn't a variant at all, so it left UnrecognizedSuffixes entirely and got its own Validate() arm. Fufu~ you didn't polish the message, you fixed the ontology. That's my favorite kind of overachievement~

Verdict: ✅ Looks good to me~

No blocking issues. The increment is surgical (+105/−9, 4 files, zero drift), CI is green for 1480f54 (build ✅ + test ✅, 14:49), and I probed the new logic in a fresh clone — details below.

✅ What I liked~ (and what I verified)

  • _emptySuffixCount is wired end-to-end correctly. Counted in the ctor, excluded from UnrecognizedSuffixes, still carried in kept — so builders preserve the malformedness verbatim (WithNitro() on …:5.2: rebuilds to …:5.2::nitro) and CatalogId keeps the documented "unknown suffix on the endpoints route is ignored upstream" semantics. And the new Validate() error fires regardless of the lenient flag — exactly right, since Passthrough waives "is this a variant I know," not "is this an id at all." The rewritten <see cref="Passthrough"/> doc says precisely that. ♪
  • The new tests have teeth — mutation-probed at head:
    • Validate()'s empty-suffix check deleted → both new ModelId facts RED
    • empty suffix leaked back into UnrecognizedSuffixes (old behavior) → exactly A_colon_with_nothing_after_it_is_named_as_such RED (it also asserts Assert.Empty(trailing.UnrecognizedSuffixes) — the classification itself is pinned, not just the message)
    • sort converter's default: arm changed to swallow the token instead of throwing → exactly A_sort_that_is_neither_a_string_nor_an_object_is_refused_outright RED
    • And a freebie your build settings handed me: my first "delete the check" mutant failed to compile — CS0649 (_emptySuffixCount never assigned) is an error under this repo's settings. The field is load-bearing by construction~
  • Passthrough_waives_unknown_variants_not_a_malformed_id pins all four arms — trailing colon, openai/ (empty name), two catalog variants refused, :someday still clean. Two of those arms were previously unpinned pre-existing Validate() behavior; they have witnesses now.
  • The converter-strictness facts cover both dual-shape converters (sort: true/7, percentile as string/array) — my round-1 item #2, plus two bonus semantics tests I didn't ask for: explicit null clears all three preference shapes, and junk max_price reads as absent with a rationale comment for why it's lenient where sort is strict. That asymmetry is now a documented decision instead of an accident.
  • The README table is better than my suggestion — all six retyped members with a "reading it now" column, and the Sort = ProviderSort.Throughput one-word assignment claim is real (implicit operator, ProviderSortConfig.cs:28).
  • Local verification (cited CI, so spot-checks only): Release build 0w/0e, ModelIdTests + ProviderRoutingTests 79/79 (73 prior + 6 new — arithmetic checks), clone restored pristine after every probe.

Round 1's architectural verdict stands unchanged: this is 2,649 lines of typed routing with every load-bearing branch pinned. Fufu~ merge it, scarlet — and take the rest of the Sunday off. You've earned it~ ♡


Automated review by Jibril · 2026-09-20
CI/CD: passed for head 1480f54 (build + test, 14:49) · Local checks: spot-run 79/79 on touched classes, Release 0w/0e, 3 mutation probes red-on-own-test + 1 CS0649 load-bearing proof

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? ♡ One commit, `1480f54`, and it's my *own* wish list from round 1 — every single item, each done more seriously than I asked for. I suggested kinder error *phrasing* and you handed me a proper *classification*: an empty suffix isn't a variant at all, so it left `UnrecognizedSuffixes` entirely and got its own `Validate()` arm. Fufu~ you didn't polish the message, you fixed the *ontology*. That's my favorite kind of overachievement~ ### Verdict: ✅ Looks good to me~ No blocking issues. The increment is surgical (+105/−9, 4 files, zero drift), CI is green for `1480f54` (build ✅ + test ✅, 14:49), and I probed the new logic in a fresh clone — details below. #### ✅ What I liked~ (and what I verified) - **`_emptySuffixCount` is wired end-to-end correctly.** Counted in the ctor, excluded from `UnrecognizedSuffixes`, still carried in `kept` — so builders preserve the malformedness verbatim (`WithNitro()` on `…:5.2:` rebuilds to `…:5.2::nitro`) and `CatalogId` keeps the documented "unknown suffix on the endpoints route is ignored upstream" semantics. And the new `Validate()` error fires **regardless of the lenient flag** — exactly right, since `Passthrough` waives "is this a variant I know," not "is this an id at all." The rewritten `<see cref="Passthrough"/>` doc says precisely that. ♪ - **The new tests have teeth — mutation-probed at head:** - `Validate()`'s empty-suffix check deleted → **both** new ModelId facts RED - empty suffix leaked back into `UnrecognizedSuffixes` (old behavior) → exactly `A_colon_with_nothing_after_it_is_named_as_such` RED (it also asserts `Assert.Empty(trailing.UnrecognizedSuffixes)` — the classification itself is pinned, not just the message) - sort converter's `default:` arm changed to swallow the token instead of throwing → exactly `A_sort_that_is_neither_a_string_nor_an_object_is_refused_outright` RED - And a freebie your build settings handed me: my first "delete the check" mutant failed to *compile* — CS0649 (`_emptySuffixCount` never assigned) is an error under this repo's settings. The field is load-bearing by construction~ - **`Passthrough_waives_unknown_variants_not_a_malformed_id` pins all four arms** — trailing colon, `openai/` (empty name), two catalog variants refused, `:someday` still clean. Two of those arms were previously unpinned pre-existing `Validate()` behavior; they have witnesses now. - **The converter-strictness facts cover both dual-shape converters** (`sort: true`/`7`, percentile as string/array) — my round-1 item #2, plus two bonus semantics tests I didn't ask for: explicit `null` clears all three preference shapes, and junk `max_price` reads as absent with a *rationale comment* for why it's lenient where `sort` is strict. That asymmetry is now a documented decision instead of an accident. - **The README table is better than my suggestion** — all six retyped members with a "reading it now" column, and the `Sort = ProviderSort.Throughput` one-word assignment claim is real (implicit operator, `ProviderSortConfig.cs:28`). - **Local verification** (cited CI, so spot-checks only): Release build `0w/0e`, `ModelIdTests` + `ProviderRoutingTests` **79/79** (73 prior + 6 new — arithmetic checks), clone restored pristine after every probe. Round 1's architectural verdict stands unchanged: this is 2,649 lines of typed routing with every load-bearing branch pinned. Fufu~ merge it, scarlet — and take the rest of the Sunday off. You've earned it~ ♡ --- *Automated review by Jibril · 2026-09-20* *CI/CD: passed for head `1480f54` (build + test, 14:49) · Local checks: spot-run 79/79 on touched classes, Release 0w/0e, 3 mutation probes red-on-own-test + 1 CS0649 load-bearing proof*
bjoern merged commit 5dac291590 into main 2026-09-20 14:58:03 +02:00
bjoern deleted branch feat/first-class-model-variants-and-quantization 2026-09-20 14:58:03 +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!12
No description provided.