docs: what the live Decisions API said, in place of what we guessed #22

Merged
bjoern merged 1 commit from docs/decisions-live-findings into main 2026-09-23 17:43:47 +02:00
Member

A follow-up to the Decisions series (#17, #19, #21). Four things in the Decisions docs were guesses until the owner ran the sample's smoke mode against the live API on 2026-09-23 (dotnet run --project samples/OpenRouter.Net.Sample -- decisions, run twice). This PR replaces the guesses with what the API actually did. No library behaviour changes: only XML docs, the README, test fixtures, tests, and a .gitignore entry.

What the live API did

Question Before this PR Live answer
Does /api/alpha/decisions take a bare jev-1.13? "documented only for System One" Yes. Both routes answer with the dated typesafe/jev-1.13-20260917. jev-latest was not tried, and the docs say so.
What happens over TypeSafe's limits, or with an unknown question type? "documented, not enforced" The API refuses with a 400 that has code: 400 and no error type, so it reads as InvalidRequest and is not retried. For a limit, the message wraps TypeSafe's reason (Too many score levels. Must have at most 10 levels., Too many choices. Must have at most 255 choices.). For an unknown type, it is a Zod invalid_union list.
Does GetGenerationAsync find a gen-dec-… id? "expected to work, not yet checked" No. 404 Generation … not found, still 404 six seconds after the decision came back, although the spec lists api_type: decisions.
Is the same request answered the same way? "several hundredths higher or lower" Six sends put payments between 0.74 and 0.82, and the labels came back in no fixed order.

Limits stay unenforced client-side, on purpose. The server already refuses over-limit requests with a clear message. If TypeSafe raises a limit, no new release of this library is needed. Validate()'s docs and the test pinning this (renamed TypeSafes_limits_are_left_to_the_API) now say that, instead of implying nothing checks the limits.

Changes

  • README, Decisions section:
    • The Routes paragraph now says the default route takes a bare id.
    • The failure list gains an InvalidRequest bullet for these refusals.
    • The "Not yet checked against the live API" paragraph is replaced by the generation-lookup finding and a pointer to Usage.Cost.
    • The calibration bullet gives the measured spread.
    • The Raw bullet says today's API knows only noul/choice/score.
    • The triage example's ticket gains the sentence ("I have tried two browsers.") that the measured request carried, so the numbers in its comments come from the request it shows.
  • README, Account & metadata: it no longer says GetGenerationAsync covers "any prior generation" with Decisions among the readable API types.
  • XML docs:
    • DecisionsRoute (the enum and Decisions) and DecisionsRequest.Model: bare ids.
    • ChoiceQuestion.Criteria, ScoreQuestion.Criteria and DecisionsRequest.Validate: the API enforces the limits.
    • IOpenRouterClient.GetGenerationAsync: gen-dec ids are not found.
    • ChoiceAnswer.Probabilities/Ranked: response order changes from call to call.
    • RawDecisionQuestion: today's accepted types.
  • The smoke sample says its output can hold the account's user id. .gitignore ignores decisions-smoke/.

Fixtures (OpenRouterSamples)

Each is verbatim from the live run:

  • DecisionsLiveResponse: the 200, with a third key order that matches neither the spec example nor the tutorial: model first, id after usage, confidence after probabilities.
  • DecisionsTooManyLevelsBody, DecisionsTooManyChoicesBody, DecisionsUnknownTypeBody: the three 400s. The unknown-type 400 carries a top-level user_id, which is replaced with user_REDACTED. git grep confirms the real id is nowhere in the tree.
  • DecisionGenerationNotFoundBody: the 404.

Tests

main (0479a53, after #21) this PR
OpenRouter.Net.Tests 1004 1010
OpenRouter.Net.Agents.Tests 63 63
OpenRouter.Net.Imaging.Tests 7 7
Total 1074 1080
  • The live 200 reads through CreateDecisionsAsync (the recorded-shapes theory is renamed Every_recorded_response_reads_as_typed_answers_whatever_its_key_order) and through DecideAsync with the typed keys.
  • Each live 400 fails as InvalidRequest with its reason in Error, RawCode "400", no error type, not transient, and sent once with retries on.
  • The live 404 generation lookup fails as InvalidRequest and is sent once.
  • A precedence test had a made-up message imitating a rank refusal. The real refusal is now recorded and looks nothing like it, so the test uses a neutral message instead.

Build: 0 errors, 0 warnings. There are no mutation probes this time, because no logic changed.

Review. Two review passes ran: one checked accuracy against the live JSON, the other looked for stale claims left elsewhere. A skeptic then tried to refute each finding.

  • 7 issues were confirmed and are fixed:
    • the README example's ticket did not match the measured request;
    • the Account & metadata paragraph still promised gen-dec records;
    • "six seconds later" was measured from the wrong event;
    • the DecisionsRoute enum summary still said the routes differ on bare ids, and jev-latest was overgeneralised;
    • a fixture doc presented one capture's label order as a rule;
    • a test class summary still said "both" recorded shapes;
    • one 400 fixture doc covered System One, which was never sent an invalid request.
  • 5 were rejected as wording preferences, among them the older gen-dec-1 deserialization mock in ListEndpointsTests, which tests parsing, not lookup.

Rebased onto main after #21 merged. main's tree equals #21's head, so this is the same tree that was tested.

🤖 Generated with Claude Code

A follow-up to the Decisions series (#17, #19, #21). Four things in the Decisions docs were guesses until the owner ran the sample's smoke mode against the live API on 2026-09-23 (`dotnet run --project samples/OpenRouter.Net.Sample -- decisions`, run twice). This PR replaces the guesses with what the API actually did. **No library behaviour changes**: only XML docs, the README, test fixtures, tests, and a `.gitignore` entry. ## What the live API did | Question | Before this PR | Live answer | |---|---|---| | Does `/api/alpha/decisions` take a bare `jev-1.13`? | "documented only for System One" | **Yes.** Both routes answer with the dated `typesafe/jev-1.13-20260917`. `jev-latest` was not tried, and the docs say so. | | What happens over TypeSafe's limits, or with an unknown question type? | "documented, not enforced" | **The API refuses with a 400** that has `code: 400` and no error type, so it reads as `InvalidRequest` and is not retried. For a limit, the message wraps TypeSafe's reason (`Too many score levels. Must have at most 10 levels.`, `Too many choices. Must have at most 255 choices.`). For an unknown type, it is a Zod `invalid_union` list. | | Does `GetGenerationAsync` find a `gen-dec-…` id? | "expected to work, not yet checked" | **No.** 404 `Generation … not found`, still 404 six seconds after the decision came back, although the spec lists `api_type: decisions`. | | Is the same request answered the same way? | "several hundredths higher or lower" | Six sends put `payments` between **0.74 and 0.82**, and the labels came back in no fixed order. | **Limits stay unenforced client-side, on purpose.** The server already refuses over-limit requests with a clear message. If TypeSafe raises a limit, no new release of this library is needed. `Validate()`'s docs and the test pinning this (renamed `TypeSafes_limits_are_left_to_the_API`) now say that, instead of implying nothing checks the limits. ## Changes - **README, Decisions section:** - The Routes paragraph now says the default route takes a bare id. - The failure list gains an `InvalidRequest` bullet for these refusals. - The "Not yet checked against the live API" paragraph is replaced by the generation-lookup finding and a pointer to `Usage.Cost`. - The calibration bullet gives the measured spread. - The `Raw` bullet says today's API knows only `noul`/`choice`/`score`. - The triage example's ticket gains the sentence ("I have tried two browsers.") that the measured request carried, so the numbers in its comments come from the request it shows. - **README, Account & metadata:** it no longer says `GetGenerationAsync` covers "any prior generation" with `Decisions` among the readable API types. - **XML docs:** - `DecisionsRoute` (the enum and `Decisions`) and `DecisionsRequest.Model`: bare ids. - `ChoiceQuestion.Criteria`, `ScoreQuestion.Criteria` and `DecisionsRequest.Validate`: the API enforces the limits. - `IOpenRouterClient.GetGenerationAsync`: `gen-dec` ids are not found. - `ChoiceAnswer.Probabilities`/`Ranked`: response order changes from call to call. - `RawDecisionQuestion`: today's accepted types. - **The smoke sample** says its output can hold the account's user id. **`.gitignore`** ignores `decisions-smoke/`. ## Fixtures (`OpenRouterSamples`) Each is verbatim from the live run: - **`DecisionsLiveResponse`:** the 200, with a third key order that matches neither the spec example nor the tutorial: `model` first, `id` after `usage`, `confidence` after `probabilities`. - **`DecisionsTooManyLevelsBody`**, **`DecisionsTooManyChoicesBody`**, **`DecisionsUnknownTypeBody`:** the three 400s. The unknown-type 400 carries a top-level **`user_id`**, which is replaced with `user_REDACTED`. `git grep` confirms the real id is nowhere in the tree. - **`DecisionGenerationNotFoundBody`:** the 404. ## Tests | | main (`0479a53`, after #21) | this PR | |---|---|---| | OpenRouter.Net.Tests | 1004 | 1010 | | OpenRouter.Net.Agents.Tests | 63 | 63 | | OpenRouter.Net.Imaging.Tests | 7 | 7 | | **Total** | **1074** | **1080** | - **The live 200** reads through `CreateDecisionsAsync` (the recorded-shapes theory is renamed `Every_recorded_response_reads_as_typed_answers_whatever_its_key_order`) and through `DecideAsync` with the typed keys. - **Each live 400** fails as `InvalidRequest` with its reason in `Error`, `RawCode` `"400"`, no error type, not transient, and **sent once** with retries on. - **The live 404 generation lookup** fails as `InvalidRequest` and is sent once. - **A precedence test** had a made-up message imitating a `rank` refusal. The real refusal is now recorded and looks nothing like it, so the test uses a neutral message instead. Build: 0 errors, 0 warnings. There are no mutation probes this time, because no logic changed. **Review.** Two review passes ran: one checked accuracy against the live JSON, the other looked for stale claims left elsewhere. A skeptic then tried to refute each finding. - **7 issues were confirmed and are fixed:** - the README example's ticket did not match the measured request; - the Account & metadata paragraph still promised `gen-dec` records; - "six seconds later" was measured from the wrong event; - the `DecisionsRoute` enum summary still said the routes differ on bare ids, and `jev-latest` was overgeneralised; - a fixture doc presented one capture's label order as a rule; - a test class summary still said "both" recorded shapes; - one 400 fixture doc covered System One, which was never sent an invalid request. - **5 were rejected as wording preferences**, among them the older `gen-dec-1` deserialization mock in `ListEndpointsTests`, which tests parsing, not lookup. Rebased onto `main` after #21 merged. `main`'s tree equals #21's head, so this is the same tree that was tested. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs: what the live Decisions API said, in place of what we guessed
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 24s
0c853443c4
Four claims in the Decisions docs were guesses: bare ids worked only on System One, TypeSafe's limits
were documented but unchecked, a gen-dec id could be looked up as a generation, and the route's 400 was
unrecorded. The sample's smoke mode ran all of them against the live API on 2026-09-23, twice.

- Bare ids work on both routes. The default route takes jev-1.13 as System One does, and both answer
  with the dated typesafe/jev-1.13-20260917. No pre-send check is needed. jev-latest was not tried,
  and the docs say so.
- The API enforces the limits. More than ten levels, more than 255 labels, or a question type other
  than noul, choice and score gets a 400 with no error type, which reads as InvalidRequest and is not
  retried. Validate() still does not check them, so a limit TypeSafe raises needs no release; the docs
  now say the API refuses them instead of that nothing does.
- GetGenerationAsync does not find a decision. It answered 404 for a gen-dec id and was still answering
  404 six seconds after the decision came back, although the spec lists an api_type of decisions. The
  README points at Usage.Cost for a decision's cost, and its Account & metadata section no longer
  promises a record for any generation.
- The same request moved: six sends put payments anywhere from 0.74 to 0.82, and the labels came back
  in no fixed order. The README says so, and ChoiceAnswer's docs say its probabilities come in the
  response's order and Ranked() orders them. The README's example ticket gains the sentence the
  measured request carried, so the numbers in its comments come from the request it shows.

The live bodies are now fixtures, verbatim: the 200, whose key order is neither the spec's nor the
tutorial's, the three 400s and the 404. One 400 carries the account's user_id, which is redacted in
the fixture; the smoke output folder is ignored so the raw files cannot be committed. Tests read the
live 200 through CreateDecisionsAsync and DecideAsync, fail each live 400 as InvalidRequest sent once
with its reason in Error, and fail the 404 lookup as InvalidRequest sent once.

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

Summary

Summary
Generated on: 09/23/2026 - 15:37:47
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 111
Files: 100
Line coverage: 92.5% (3269 of 3532)
Covered lines: 3269
Uncovered lines: 263
Coverable lines: 3532
Total lines: 9200
Branch coverage: 88% (1834 of 2083)
Covered branches: 1834
Total branches: 2083
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 96.6%
Name Line Branch
OpenRouter.Net 96.6% 93.1%
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 81.2% 70%
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.ErrorParser 100% 75%
OpenRouter.Net.Internal.FailureKindMap 100% 100%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.ModelIdJsonConverter 100% 50%
OpenRouter.Net.Internal.NumberOrStringJsonConverter 100% 100%
OpenRouter.Net.Internal.PercentileCutoffsJsonConverter 90.6% 87.5%
OpenRouter.Net.Internal.ProviderSortConfigJsonConverter 83.7% 65.6%
OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter 100%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
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% 100%
OpenRouter.Net.Internal.ToolChoiceJsonConverter 61.7% 46.1%
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.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% 92.8%
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 86.2% 76.1%
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.ResponseFormat 8.3%
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.OpenRouterException 100%
System.Text.RegularExpressions.Generated 83.3% 53.8%
System.Text.RegularExpressions.Generated.RunnerFactory
System.Text.RegularExpressions.Generated.RunnerFactory.Runner
OpenRouter.Net.Agents - 80%
Name Line Branch
OpenRouter.Net.Agents 80% 65.5%
OpenRouter.Net.Agents.Agent 92.4% 77.3%
OpenRouter.Net.Agents.AgentOptions 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 0%
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 80% 63.8%
OpenRouter.Net.Agents.Tools.Tool 90.9% 50%
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/23/2026 - 15:37:47 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 111 | | Files: | 100 | | **Line coverage:** | 92.5% (3269 of 3532) | | Covered lines: | 3269 | | Uncovered lines: | 263 | | Coverable lines: | 3532 | | Total lines: | 9200 | | **Branch coverage:** | 88% (1834 of 2083) | | Covered branches: | 1834 | | Total branches: | 2083 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 96.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**96.6%**|**93.1%**| |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|81.2%|70%| |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.ErrorParser|100%|75%| |OpenRouter.Net.Internal.FailureKindMap|100%|100%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.ModelIdJsonConverter|100%|50%| |OpenRouter.Net.Internal.NumberOrStringJsonConverter|100%|100%| |OpenRouter.Net.Internal.PercentileCutoffsJsonConverter|90.6%|87.5%| |OpenRouter.Net.Internal.ProviderSortConfigJsonConverter|83.7%|65.6%| |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.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%|100%| |OpenRouter.Net.Internal.ToolChoiceJsonConverter|61.7%|46.1%| |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.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%|92.8%| |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|86.2%|76.1%| |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.ResponseFormat|8.3%|| |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.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 - 80%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**80%**|**65.5%**| |OpenRouter.Net.Agents.Agent|92.4%|77.3%| |OpenRouter.Net.Agents.AgentOptions|100%|| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|0%|| |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>|80%|63.8%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|90.9%|50%| |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 PR that is nothing but facts~ You ran the smoke sample against the live API, and every guess in the Decisions docs got replaced with what the API actually did — dated, measured, and redacted. A knowledge-obsessed Flügel could not be happier~ ♡

Verdict: ✅ Looks good to me~

I checked your claims the way I check everything — mechanically, because trust is for people who don't read diffs:

  • "No library behaviour changes" — proven. I stripped every changed line in src/ and samples/ down to non-/// lines and got empty sets. The only executable-code changes are tests and fixtures.
  • Fixtures — all five parse as valid JSON, and DecisionsLiveResponse really has the third key order you describe (model first, id after usage, confidence after probabilities). It reads cleanly through both CreateDecisionsAsync and DecideAsync with the typed keys — the cross-checks pass on it (noul 0.96 ≥ 0.9, runner-up frontend at 0.2, most-likely level 2 at 0.99, score 1.99 in range). fufu~ the recorded truth feeds both doors~
  • The 400/404 semantics — I traced the new assertions through FailureKindMap and NumberOrStringJsonConverter myself: no error type → the status decides → 400/404 map to InvalidRequest, non-transient, and numeric code: 400 reads back as RawCode "400". The double-enqueued responses with Assert.Single(handler.Requests) pin sent-once exactly like the sibling …and_is_sent_once tests. Consistent~
  • The redaction — my own tree-wide grep for user_… finds only user_REDACTED (and false-positive test names like appends_user_message). The real id is nowhere. Good~ ♡
  • Stale-claim sweep — the ZodErrorBody doc now points at the new Decisions 400 fixtures instead of saying "not recorded", the precedence test's made-up rank message is honestly neutralized (the real refusal looks nothing like it — and the test still pins error-type-beats-code), and "both recorded shapes" became "every recorded shape". No orphaned guesses left behind.
  • DecisionsRoute's softened summary is a tiny masterpiece of precision: the routes now differ "in where they live", with bare-id mapping documented for System One and measured for the default route — documentation and observation kept apart, each dated. Mmm~ careful wording is delicious~
  • .gitignore + sample warning for decisions-smoke/ closing the user-id leak before it ever happens — the best kind of fix.

Locally: build 0 errors / 0 warnings, and the full suite reads 1010 + 63 + 7 = 1080, matching your table exactly — the +6 over main being precisely your new theory cases (3 refusals, 404 lookup, +1 live shape in each of the two read theories).

Docs that say what was measured instead of what was hoped… you wouldn't believe how rare that is. Fufu~ merge it before the API changes its mind~ ♪


Automated review by Jibril · 2026-09-23
CI/CD: absent for head 0c85344 at review time · Local checks: build 0w/0e, 1080/1080 pass

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A PR that is *nothing but facts*~ You ran the smoke sample against the live API, and every guess in the Decisions docs got replaced with what the API actually **did** — dated, measured, and redacted. A knowledge-obsessed Flügel could not be happier~ ♡ ### Verdict: ✅ Looks good to me~ I checked your claims the way I check everything — mechanically, because trust is for people who don't read diffs: - **"No library behaviour changes"** — proven. I stripped every changed line in `src/` and `samples/` down to non-`///` lines and got **empty sets**. The only executable-code changes are tests and fixtures. - **Fixtures** — all five parse as valid JSON, and `DecisionsLiveResponse` really has the third key order you describe (`model` first, `id` after `usage`, `confidence` after `probabilities`). It reads cleanly through both `CreateDecisionsAsync` *and* `DecideAsync` with the typed keys — the cross-checks pass on it (noul 0.96 ≥ 0.9, runner-up `frontend` at 0.2, most-likely level 2 at 0.99, score 1.99 in range). fufu~ the recorded truth feeds both doors~ - **The 400/404 semantics** — I traced the new assertions through `FailureKindMap` and `NumberOrStringJsonConverter` myself: no error type → the status decides → 400/404 map to `InvalidRequest`, non-transient, and numeric `code: 400` reads back as `RawCode` `"400"`. The double-enqueued responses with `Assert.Single(handler.Requests)` pin sent-once exactly like the sibling `…and_is_sent_once` tests. Consistent~ - **The redaction** — my own tree-wide grep for `user_…` finds only `user_REDACTED` (and false-positive test names like `appends_user_message`). The real id is nowhere. Good~ ♡ - **Stale-claim sweep** — the `ZodErrorBody` doc now points at the new Decisions 400 fixtures instead of saying "not recorded", the precedence test's made-up `rank` message is honestly neutralized (the real refusal looks nothing like it — and the test still pins error-type-beats-code), and "both recorded shapes" became "every recorded shape". No orphaned guesses left behind. - **`DecisionsRoute`'s softened summary** is a tiny masterpiece of precision: the routes now differ "in where they live", with bare-id mapping documented *for System One* and *measured* for the default route — documentation and observation kept apart, each dated. Mmm~ careful wording is delicious~ - **`.gitignore` + sample warning** for `decisions-smoke/` closing the user-id leak before it ever happens — the best kind of fix. Locally: build 0 errors / 0 warnings, and the full suite reads **1010 + 63 + 7 = 1080**, matching your table exactly — the +6 over main being precisely your new theory cases (3 refusals, 404 lookup, +1 live shape in each of the two read theories). Docs that say what was *measured* instead of what was *hoped*… you wouldn't believe how rare that is. Fufu~ merge it before the API changes its mind~ ♪ --- *Automated review by Jibril · 2026-09-23* *CI/CD: absent for head 0c85344 at review time · Local checks: build 0w/0e, 1080/1080 pass*
bjoern merged commit b901b1338f into main 2026-09-23 17:43:47 +02:00
bjoern deleted branch docs/decisions-live-findings 2026-09-23 17:43:47 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/OpenRouter.Net!22
No description provided.