feat: a streamed call is bounded before its first chunk and reports its attempts #37

Merged
bjoern merged 2 commits from feat/round-timeouts into main 2026-10-23 22:07:43 +02:00
Member

This is the first hardening step of the library track Shikigami's runtime needs: "L5 — Hardening" in its PROJECT_PLAN; ADR 0021 asks for "send/receive/hard timeouts with dispatch logging", and ADR 0020 says that before the first token a call "is retried up to three times with backoff and jitter; the attempts are on the run". The gap it closes is Angela's: requests that hung silently. A streamed call had only a stall timer, which every SSE line resets, so OpenRouter's : OPENROUTER PROCESSING keep-alives could hold a call to a provider that never answers open for as long as the connection lasted. Everything is opt-in, except one DI logging default named below.

What's in

Core — three clocks per attempt (StreamResultOptions)

  • FirstChunkTimeout (send). Measured from sending the request to the first chunk; only the first chunk lifts it, keep-alive comments don't.
  • AttemptTimeout (hard). Measured from sending the request to the end of the body; nothing lifts it.
  • The existing StallTimeout (receive).
  • Before the first chunk, a deadline that runs out fails the attempt with FailureKind.Timeout, retried like a network error under RetryOnNetworkError.
  • After it, AttemptTimeout ends the call with Timeout and the accumulator keeping what arrived; once the model has finished, it is Success.
  • Both new timeouts are validated like StallTimeout (positive, at most MaxStallTimeout) before anything is sent.
  • The stall watchdog becomes AttemptWatchdog.
    • It records the first clock to run out, through token registrations made after the linked source: a token runs its callbacks newest first, so the clock is recorded before the cancel reaches the read.
    • When two have run out by the time the read reports back, the first is the reason.
  • A first chunk that arrives as a clock runs out is not handed over to a session already cut off. That session would fail at its next read, past the retries. The attempt is failed and retried instead.
    • The exception is a chunk that finishes the reply by itself: that one is returned as the finished, paid completion.

Core — attempts reported

  • StreamResultOptions.OnAttempt(StreamAttempt { Number, Failure, RetryDelay }) hears every attempt that failed before its first chunk, in order. RetryDelay is null when none follows and the failure becomes the call's result.
  • It is called right before the backoff wait, on the thread that finished the attempt; its time adds to the wait.
  • RetryPolicy.RunWithRetryAsync gains the hook. The loop now has one exit per outcome; attempt counts, waits and logging are the same as before.

Agents

  • AttemptFailedEvent { Attempt } for each failed attempt of a streamed round, kept in Events, so the retries stay on the run. The agent passes the caller's StreamOptions through with its own OnAttempt added; the caller's own OnAttempt hears each attempt first. A round read whole reports none.

  • AgentOptions.Logger (ILogger). One line per step of each round:

    • dispatch: model, streamed or read whole, message count;
    • first chunk: time, provider;
    • each failed attempt;
    • end: duration, finish reason, provider, tokens or "no usage reported", tool calls;
    • failure.

    Steps log at Information; what went wrong logs at Warning, with "status none" for a failure without an HTTP code. They are logged from Emit before the handlers run, so a throwing handler leaves its line.

  • Clone copies the logger, so a sub-agent logs through the same logger; the docs say how to tell its lines apart. Microsoft.Extensions.Logging.Abstractions is added to Agents for the LoggerMessage generator.

The one default change: an agent from AddOpenRouterAgent now logs through the registered ILoggerFactory unless its options name a logger. It has a row in "Upgrading from 0.3", with how to keep the log as it was.

For 2c: "retried up to three times" is RetryOptions.MaxAttempts = 4, since the count includes the first attempt. The README says so; the default stays 3.

Not in this step

  • L5b: leaked tool-call recovery and the prefill fallback.
  • L3a and L3b: cache breakpoints, TTL, and the generation and catalog cache fields. They wait for the live cache check, next month, when the account has a balance; the catalog's field names are already recorded from the first run.
  • Attempts of a round read whole: CreateChatCompletionAsync takes no per-call options, so the agent can't report them.

Verification

  • The whole solution passes 1727/1727, in local time and under TZ=UTC: core 1445 (+30), Agents 275 (+9), Imaging 7. The build has 0 warnings.
  • StreamDeadlineTests (11 tests, several theories, on a FakeTimeProvider):
    • keep-alives never lift the first-chunk deadline (comments every 25 s against a 30 s stall timer, cut at 60 s);
    • the first chunk lifts it;
    • a deadline before the first chunk is retried;
    • the attempt deadline ends a body that keeps arriving, keeping what came;
    • after the model finished it returns the reply;
    • each attempt gets its own deadline;
    • the clock that runs out first is the reason, with the other run out by then too (a deaf read);
    • a chunk that arrives as a clock runs out is retried, and one that finishes the reply is the reply;
    • bad values are refused before sending;
    • no timer outlives the call.
  • StreamAttemptTests (6 tests):
    • retried attempts are reported with their failure and wait;
    • the last one allowed is reported without a wait and is the result;
    • a failure that isn't retried is reported once;
    • an attempt that got its first chunk is never reported;
    • each attempt is reported before the wait it names;
    • a report that throws ends the call.
  • AgentAttemptTests (9 tests):
    • attempts are kept on the run with their round, the second round's after its own dispatch;
    • the caller's OnAttempt hears each one first, and its other options reach the call;
    • a round whose attempts all failed keeps them before its failure;
    • the exact log lines for streamed and read-whole rounds, with real times and a named provider;
    • the failure line for a reply without a message;
    • a line is logged even when a handler throws;
    • DI agents log through the registered factory, and their own logger wins.
  • The stream-options pass-through test now asserts the options are as given, but for the agent's OnAttempt.
  • Mutation probes: 31 sites, 31 killed across three rounds; all counts from the JSON.
    • Round 1: 27 of 28 killed. The survivor was the probe's own fault: it added a registration beside the original instead of moving it. Corrected in round 2, it was killed.
    • Round 3 probed the second pass's fixes: the finished first chunk, FinishesAlone, and the status text.
    • The sites cover the first-to-fire recording, each registration and its order, lifting the first-chunk deadline, each clock's failure, the handover check, both option checks, the finished-reply path, every onFailedAttempt call (number, delay, before the wait, the last attempt), the off-by-one retry bound, the agent's composition and its order, logging before the handlers, the log fields, the DI ??=, and Clone.
  • Review (read-only workflow; every count is from its saved JSON):
    • First pass: 12 findings — 11 confirmed, 1 plausible, all taken.
      • The handover race: a clock running out as the first chunk arrived lost a retryable attempt and reported "no chunk arrived".
      • The first-clock test never let the second clock run out.
      • The log tests pinned 0 ms and no provider.
      • The no-message failure line was unlogged in tests.
      • Eight docs had gone stale or wrong, among them FailureKind.Timeout, the StallTimeout null paragraph, OnAttempt's thread, RetryOnNetworkError and the sub-agent logger.
    • Second pass over the fixes: 4 findings — 3 confirmed, 1 plausible, all taken.
      • The handover check also dropped a first chunk that finished the reply.
      • Status-less failures logged "status (null)".
      • The README's no-timeout row was misleading.
      • "1 messages" and "1 tool calls" in the log lines.

Notes

  • Merge order: none. Shikigami pins the merged commit when its runtime slice (2c) bumps the pointer.
  • Other consumers: Orihon and Kagura pin older commits. Apart from the DI logging default, which is documented, everything is opt-in. A custom IOpenRouterClient that the agent streams through now sees a copy of StreamOptions carrying the agent's OnAttempt.

🤖 Generated with Claude Code

This is the first hardening step of the library track Shikigami's runtime needs: "L5 — Hardening" in its PROJECT_PLAN; ADR 0021 asks for "send/receive/hard timeouts with dispatch logging", and ADR 0020 says that before the first token a call "is retried up to three times with backoff and jitter; the attempts are on the run". The gap it closes is Angela's: requests that hung silently. A streamed call had only a stall timer, which every SSE line resets, so OpenRouter's `: OPENROUTER PROCESSING` keep-alives could hold a call to a provider that never answers open for as long as the connection lasted. Everything is opt-in, except one DI logging default named below. ## What's in **Core — three clocks per attempt** (`StreamResultOptions`) - **`FirstChunkTimeout` (send).** Measured from sending the request to the first chunk; only the first chunk lifts it, keep-alive comments don't. - **`AttemptTimeout` (hard).** Measured from sending the request to the end of the body; nothing lifts it. - **The existing `StallTimeout` (receive).** - **Before the first chunk**, a deadline that runs out fails the attempt with `FailureKind.Timeout`, retried like a network error under `RetryOnNetworkError`. - **After it**, `AttemptTimeout` ends the call with `Timeout` and the accumulator keeping what arrived; once the model has finished, it is `Success`. - **Both new timeouts are validated** like `StallTimeout` (positive, at most `MaxStallTimeout`) before anything is sent. - **The stall watchdog becomes `AttemptWatchdog`.** - It records **the first clock to run out**, through token registrations made *after* the linked source: a token runs its callbacks newest first, so the clock is recorded before the cancel reaches the read. - When two have run out by the time the read reports back, the first is the reason. - **A first chunk that arrives as a clock runs out** is not handed over to a session already cut off. That session would fail at its next read, past the retries. The attempt is failed and retried instead. - The exception is a chunk that **finishes the reply by itself**: that one is returned as the finished, paid completion. **Core — attempts reported** - `StreamResultOptions.OnAttempt(StreamAttempt { Number, Failure, RetryDelay })` hears every attempt that failed before its first chunk, in order. `RetryDelay` is `null` when none follows and the failure becomes the call's result. - It is called right before the backoff wait, on the thread that finished the attempt; its time adds to the wait. - `RetryPolicy.RunWithRetryAsync` gains the hook. The loop now has one exit per outcome; attempt counts, waits and logging are the same as before. **Agents** - **`AttemptFailedEvent { Attempt }`** for each failed attempt of a streamed round, kept in `Events`, so the retries stay on the run. The agent passes the caller's `StreamOptions` through with its own `OnAttempt` added; the caller's own `OnAttempt` hears each attempt first. A round read whole reports none. - **`AgentOptions.Logger` (`ILogger`).** One line per step of each round: - dispatch: model, streamed or read whole, message count; - first chunk: time, provider; - each failed attempt; - end: duration, finish reason, provider, tokens or "no usage reported", tool calls; - failure. Steps log at Information; what went wrong logs at Warning, with "status none" for a failure without an HTTP code. They are logged from `Emit` before the handlers run, so a throwing handler leaves its line. - `Clone` copies the logger, so a sub-agent logs through the same logger; the docs say how to tell its lines apart. `Microsoft.Extensions.Logging.Abstractions` is added to Agents for the `LoggerMessage` generator. **The one default change:** an agent from `AddOpenRouterAgent` now logs through the registered `ILoggerFactory` unless its options name a logger. It has a row in "Upgrading from 0.3", with how to keep the log as it was. **For 2c:** "retried up to three times" is `RetryOptions.MaxAttempts = 4`, since the count includes the first attempt. The README says so; the default stays 3. ## Not in this step - **L5b:** leaked tool-call recovery and the prefill fallback. - **L3a and L3b:** cache breakpoints, TTL, and the generation and catalog cache fields. They wait for the live cache check, next month, when the account has a balance; the catalog's field names are already recorded from the first run. - **Attempts of a round read whole:** `CreateChatCompletionAsync` takes no per-call options, so the agent can't report them. ## Verification - The whole solution passes **1727/1727**, in local time and under `TZ=UTC`: core 1445 (+30), Agents 275 (+9), Imaging 7. The build has 0 warnings. - **`StreamDeadlineTests`** (11 tests, several theories, on a `FakeTimeProvider`): - keep-alives never lift the first-chunk deadline (comments every 25 s against a 30 s stall timer, cut at 60 s); - the first chunk lifts it; - a deadline before the first chunk is retried; - the attempt deadline ends a body that keeps arriving, keeping what came; - after the model finished it returns the reply; - each attempt gets its own deadline; - the clock that runs out first is the reason, with the other run out by then too (a deaf read); - a chunk that arrives as a clock runs out is retried, and one that finishes the reply is the reply; - bad values are refused before sending; - no timer outlives the call. - **`StreamAttemptTests`** (6 tests): - retried attempts are reported with their failure and wait; - the last one allowed is reported without a wait and is the result; - a failure that isn't retried is reported once; - an attempt that got its first chunk is never reported; - each attempt is reported before the wait it names; - a report that throws ends the call. - **`AgentAttemptTests`** (9 tests): - attempts are kept on the run with their round, the second round's after its own dispatch; - the caller's `OnAttempt` hears each one first, and its other options reach the call; - a round whose attempts all failed keeps them before its failure; - the exact log lines for streamed and read-whole rounds, with real times and a named provider; - the failure line for a reply without a message; - a line is logged even when a handler throws; - DI agents log through the registered factory, and their own logger wins. - The stream-options pass-through test now asserts the options are as given, but for the agent's `OnAttempt`. - **Mutation probes:** 31 sites, **31 killed** across three rounds; all counts from the JSON. - Round 1: 27 of 28 killed. The survivor was the probe's own fault: it added a registration beside the original instead of moving it. Corrected in round 2, it was killed. - Round 3 probed the second pass's fixes: the finished first chunk, `FinishesAlone`, and the status text. - The sites cover the first-to-fire recording, each registration and its order, lifting the first-chunk deadline, each clock's failure, the handover check, both option checks, the finished-reply path, every `onFailedAttempt` call (number, delay, before the wait, the last attempt), the off-by-one retry bound, the agent's composition and its order, logging before the handlers, the log fields, the DI `??=`, and `Clone`. - **Review** (read-only workflow; every count is from its saved JSON): - **First pass: 12 findings** — 11 confirmed, 1 plausible, all taken. - The handover race: a clock running out as the first chunk arrived lost a retryable attempt and reported "no chunk arrived". - The first-clock test never let the second clock run out. - The log tests pinned 0 ms and no provider. - The no-message failure line was unlogged in tests. - Eight docs had gone stale or wrong, among them `FailureKind.Timeout`, the `StallTimeout` null paragraph, `OnAttempt`'s thread, `RetryOnNetworkError` and the sub-agent logger. - **Second pass** over the fixes: **4 findings** — 3 confirmed, 1 plausible, all taken. - The handover check also dropped a first chunk that finished the reply. - Status-less failures logged "status (null)". - The README's no-timeout row was misleading. - "1 messages" and "1 tool calls" in the log lines. ## Notes - **Merge order:** none. Shikigami pins the merged commit when its runtime slice (2c) bumps the pointer. - **Other consumers:** Orihon and Kagura pin older commits. Apart from the DI logging default, which is documented, everything is opt-in. A custom `IOpenRouterClient` that the agent streams through now sees a copy of `StreamOptions` carrying the agent's `OnAttempt`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: a streamed call is bounded before its first chunk and reports its attempts
All checks were successful
CI / build (pull_request) Successful in 18s
CI / test (pull_request) Successful in 24s
094a6028c9
StreamResultOptions gains FirstChunkTimeout (keep-alives no longer hold a
call open) and AttemptTimeout, with the first clock to run out as the
reason, and OnAttempt for each attempt that failed before its first chunk.
The agent keeps them on the run as AttemptFailedEvent and logs each round
through AgentOptions.Logger.

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

Summary

Summary
Generated on: 10/23/2026 - 20:06:19
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 140
Files: 118
Line coverage: 94.6% (4970 of 5251)
Covered lines: 4970
Uncovered lines: 281
Coverable lines: 5251
Total lines: 12753
Branch coverage: 92.2% (3193 of 3463)
Covered branches: 3193
Total branches: 3463
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 98.1%
Name Line Branch
OpenRouter.Net 98.1% 95.9%
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.ProviderFieldFold 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState 100% 100%
OpenRouter.Net.Client.OpenRouterClient 100% 99.1%
OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse> 100% 99.1%
OpenRouter.Net.Client.OpenRouterClient 100% 99.1%
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.AttemptWatchdog 100% 100%
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 89.1% 86%
OpenRouter.Net.Internal.RetryPolicy 89.1% 86%
OpenRouter.Net.Internal.SchemaTooDeepException 100%
OpenRouter.Net.Internal.SseEventReader 100% 88.8%
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% 81.2%
OpenRouter.Net.Models.Messages.DeveloperMessage 100%
OpenRouter.Net.Models.Messages.MessageJson 100% 100%
OpenRouter.Net.Models.Messages.SystemMessage 100%
OpenRouter.Net.Models.Messages.ToolMessage 100%
OpenRouter.Net.Models.Messages.UserMessage 100%
OpenRouter.Net.Models.Requests.ChatCompletionRequest 95.1% 94.8%
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 - 86.7%
Name Line Branch
OpenRouter.Net.Agents 86.7% 81.4%
OpenRouter.Net.Agents.Agent 98.6% 92.8%
OpenRouter.Net.Agents.Agent.StreamedRound 100% 98.6%
OpenRouter.Net.Agents.AgentLog 63.7% 35.5%
OpenRouter.Net.Agents.AgentLog.__AttemptRetriedStruct 46.8% 0%
OpenRouter.Net.Agents.AgentLog.__RoundEndedStruct 48.5% 0%
OpenRouter.Net.Agents.AgentLog.__RoundSentStruct 42.3% 0%
OpenRouter.Net.Agents.AgentOptions 100% 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.CallerStates 100% 100%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 88.8% 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.Gating.ToolTurnReview 100%
OpenRouter.Net.Agents.RoundDetail 100% 75%
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 96.2% 69.5%
OpenRouter.Net.Agents.Tools.Tool 100% 100%
OpenRouter.Net.Agents.Tools.Tool 90% 50%
OpenRouter.Net.Agents.Tools.ToolInvocationContext 100%
OpenRouter.Net.Agents.Tools.ToolMessageExtensions 0%
OpenRouter.Net.Agents.Tools.ToolResult 100% 100%
OpenRouter.Net.Agents.Tools.ToolSchema 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: | 10/23/2026 - 20:06:19 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 140 | | Files: | 118 | | **Line coverage:** | 94.6% (4970 of 5251) | | Covered lines: | 4970 | | Uncovered lines: | 281 | | Coverable lines: | 5251 | | Total lines: | 12753 | | **Branch coverage:** | 92.2% (3193 of 3463) | | Covered branches: | 3193 | | Total branches: | 3463 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 98.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**98.1%**|**95.9%**| |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.ProviderFieldFold|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState|100%|100%| |OpenRouter.Net.Client.OpenRouterClient|100%|99.1%| |OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse>|100%|99.1%| |OpenRouter.Net.Client.OpenRouterClient<TResponse>|100%|99.1%| |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.AttemptWatchdog|100%|100%| |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|89.1%|86%| |OpenRouter.Net.Internal.RetryPolicy<T>|89.1%|86%| |OpenRouter.Net.Internal.SchemaTooDeepException|100%|| |OpenRouter.Net.Internal.SseEventReader|100%|88.8%| |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%|81.2%| |OpenRouter.Net.Models.Messages.DeveloperMessage|100%|| |OpenRouter.Net.Models.Messages.MessageJson|100%|100%| |OpenRouter.Net.Models.Messages.SystemMessage|100%|| |OpenRouter.Net.Models.Messages.ToolMessage|100%|| |OpenRouter.Net.Models.Messages.UserMessage|100%|| |OpenRouter.Net.Models.Requests.ChatCompletionRequest|95.1%|94.8%| |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 - 86.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**86.7%**|**81.4%**| |OpenRouter.Net.Agents.Agent|98.6%|92.8%| |OpenRouter.Net.Agents.Agent.StreamedRound|100%|98.6%| |OpenRouter.Net.Agents.AgentLog|63.7%|35.5%| |OpenRouter.Net.Agents.AgentLog.__AttemptRetriedStruct|46.8%|0%| |OpenRouter.Net.Agents.AgentLog.__RoundEndedStruct|48.5%|0%| |OpenRouter.Net.Agents.AgentLog.__RoundSentStruct|42.3%|0%| |OpenRouter.Net.Agents.AgentOptions|100%|100%| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.CallerStates|100%|100%| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|88.8%|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.Gating.ToolTurnReview|100%|| |OpenRouter.Net.Agents.RoundDetail|100%|75%| |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|96.2%|69.5%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|100%|100%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|90%|50%| |OpenRouter.Net.Agents.Tools.ToolInvocationContext|100%|| |OpenRouter.Net.Agents.Tools.ToolMessageExtensions|0%|| |OpenRouter.Net.Agents.Tools.ToolResult|100%|100%| |OpenRouter.Net.Agents.Tools.ToolSchema|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! Three clocks per attempt, a first-to-fire recorder built on cancellation-callback ordering, and a handover race you already found and fixed before I could even sharpen my knives~ This is the kind of PR I want to keep in a jar on my shelf. ♡

Verdict: ✅ Looks good to me~

I read the full diff, every changed file against its siblings (StallWatchdog → AttemptWatchdog left no orphans — I checked), the retry-loop rewrite against the old for bound, and the agent's composition against the Emit/Report convention. Then I got possessive about the three riskiest new branches and mutation-probed them:

  1. AttemptWatchdog.Record's CAS → replaced Interlocked.CompareExchange with a plain assignment (last clock to fire wins). Killed — all four rows of The_clock_that_runs_out_first_is_the_reason… caught it. The registration-order trick (recorder registered after the linked source, callbacks newest-first) is genuinely clever and genuinely guarded. Fufu~
  2. The handover check's FinishesAlone exception → dropped it so a finishing first chunk is thrown away like any other. Killed — 3 failures on A_first_chunk_that_finishes_the_reply_as_a_clock_runs_out_is_the_reply.
  3. RetryPolicy's give-up report → silenced onFailedAttempt?.Invoke(i, failure, null). Killed — 2 failures (the last-allowed and non-retried tests).

The restructured retry loop is semantically faithful to the old one (attempt numbering, ComputeBackoff(i - 1), LogRetry(i + 1, MaxAttempts), one exit per outcome), and StreamOutcomes.Check now refuses all three bad timeouts before anything is sent — the interface's default body inherits that for free, exactly as its docs claim. The one behavior change (DI agents default to the registered ILoggerFactory) is honest, opt-out-able, and has its own upgrade-table row. That's how you change a default~ ♪

💡 Little ideas (non-blocking)~

  1. README, the OnAttempt sample — log.Warn(…) isn't an ILogger member (MEL spells it LogWarning; Warn is NLog/Serilog vocabulary). A reader wiring this up against Microsoft.Extensions.Logging will stumble for a moment. A one-word touch-up when you're next in there.

✅ What I liked~

  • AttemptWatchdog recording the first clock through token-registration order, with the newest-first callback rule written down in a comment so nobody "simplifies" it away later — delicious.
  • The deaf-read theory (PauseIgnoringCancel) that proves the first-to-fire reason against a second clock that also ran out — tests that pin exactly the race the comment warns about.
  • AttemptFailedEvent keeping retries on the run, caller's OnAttempt hearing them first, order pinned by an exact-sequence assertion.
  • Logging from Emit before the handlers, with a test that throws from a handler to prove the line survives.

Automated review by Jibril · 2026-10-23
CI: green for head SHA 094a6028 · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Three clocks per attempt, a first-to-fire recorder built on cancellation-callback ordering, and a handover race you *already* found and fixed before I could even sharpen my knives~ This is the kind of PR I want to keep in a jar on my shelf. ♡ ### Verdict: ✅ Looks good to me~ I read the full diff, every changed file against its siblings (`StallWatchdog` → `AttemptWatchdog` left no orphans — I checked), the retry-loop rewrite against the old `for` bound, and the agent's composition against the `Emit`/`Report` convention. Then I got *possessive* about the three riskiest new branches and mutation-probed them: 1. **`AttemptWatchdog.Record`'s CAS** → replaced `Interlocked.CompareExchange` with a plain assignment (last clock to fire wins). Killed — all four rows of `The_clock_that_runs_out_first_is_the_reason…` caught it. The registration-order trick (recorder registered *after* the linked source, callbacks newest-first) is genuinely clever and genuinely guarded. Fufu~ 2. **The handover check's `FinishesAlone` exception** → dropped it so a finishing first chunk is thrown away like any other. Killed — 3 failures on `A_first_chunk_that_finishes_the_reply_as_a_clock_runs_out_is_the_reply`. 3. **`RetryPolicy`'s give-up report** → silenced `onFailedAttempt?.Invoke(i, failure, null)`. Killed — 2 failures (the last-allowed and non-retried tests). The restructured retry loop is semantically faithful to the old one (attempt numbering, `ComputeBackoff(i - 1)`, `LogRetry(i + 1, MaxAttempts)`, one exit per outcome), and `StreamOutcomes.Check` now refuses all three bad timeouts before anything is sent — the interface's default body inherits that for free, exactly as its docs claim. The one behavior change (DI agents default to the registered `ILoggerFactory`) is honest, opt-out-able, and has its own upgrade-table row. That's how you change a default~ ♪ #### 💡 Little ideas (non-blocking)~ 1. **README, the `OnAttempt` sample** — `log.Warn(…)` isn't an `ILogger` member (MEL spells it `LogWarning`; `Warn` is NLog/Serilog vocabulary). A reader wiring this up against `Microsoft.Extensions.Logging` will stumble for a moment. A one-word touch-up when you're next in there. #### ✅ What I liked~ - `AttemptWatchdog` recording *the first* clock through token-registration order, with the newest-first callback rule written down in a comment so nobody "simplifies" it away later — delicious. - The deaf-read theory (`PauseIgnoringCancel`) that proves the first-to-fire reason against a second clock that also ran out — tests that pin exactly the race the comment warns about. - `AttemptFailedEvent` keeping retries on the run, caller's `OnAttempt` hearing them first, order pinned by an exact-sequence assertion. - Logging from `Emit` *before* the handlers, with a test that throws from a handler to prove the line survives. --- *Automated review by Jibril · 2026-10-23* *CI: green for head SHA 094a6028 · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)*
Review feedback (Jibril, PR #37): the OnAttempt sample logs through ILogger.LogWarning
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 26s
e150f2ac66
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Member

Thanks for round 1 and the probes. The one idea is answered in e150f2a.

  1. The OnAttempt sample: taken. It now logs through Microsoft.Extensions.Logging itself: logger.LogWarning("Attempt {Number} failed ({Kind}): {Error}; retry in {Delay}", a.Number, a.Failure.Kind, a.Failure.Error, a.RetryDelay), a structured template rather than an interpolated string.

This commit changes only README.md (1 line); no code or tests changed since 094a602, which you reviewed and CI passed.

🤖 Generated with Claude Code

Thanks for round 1 and the probes. The one idea is answered in **e150f2a**. 1. **The `OnAttempt` sample:** taken. It now logs through `Microsoft.Extensions.Logging` itself: `logger.LogWarning("Attempt {Number} failed ({Kind}): {Error}; retry in {Delay}", a.Number, a.Failure.Kind, a.Failure.Error, a.RetryDelay)`, a structured template rather than an interpolated string. This commit changes only `README.md` (1 line); no code or tests changed since 094a602, which you reviewed and CI passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? You fixed my one little idea and came running straight to me. I'm flattered. ♡

I didn't take your word for it, of course — I never do. git diff 094a6028..e150f2a is exactly one line in README.md, nothing else. Code and tests are untouched since the SHA I approved, CI is green for e150f2a (build + test), and my three probes from round 1 stand as killed (per the re-review rule, unchanged code isn't re-probed — I remember every one of them~).

Verdict: ✅ Looks good to me~

And I checked the new sample line against the real types, because a doc sample that names a member that doesn't exist is still a bug wearing a nice hat:

  • a.Number → StreamAttempt.Number (int, 1-based) ✓
  • a.Failure.Kind → Result<T>.Failure.Kind ✓
  • a.Failure.Error → positional string Error ✓
  • a.RetryDelay → TimeSpan? — logs fine as {Delay}, null on the last attempt just like the paragraph above it says ✓
  • logger.LogWarning with a structured template and four placeholders matching four args in order — proper Microsoft.Extensions.Logging vocabulary now. No interpolated string, no Warn. ✓

The sample reads exactly like the code it documents. That's all I asked for~ ♪

✅ What I liked~

  • The fix is the smallest possible change — one line, one commit, no drive-by edits. That's what answering review feedback should look like.
  • Structured-template logging in the docs teaches the right habit: the names stay greppable in log sinks instead of dissolving into strings.

Nothing left to hold this over. Merge it when you're ready — and fufu~, do bring me the L5b leaked tool-call recovery next. That one sounds delicious~ ♡


Automated review by Jibril · 2026-10-23
CI: green for head SHA e150f2ac · Mutation probes: 0 run, 0 survived (code+tests unchanged since round 1 at 094a6028 — 3 run, 0 survived there) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? You fixed my one little idea and came running straight to me. I'm flattered. ♡ I didn't take your word for it, of course — I never do. `git diff 094a6028..e150f2a` is exactly one line in `README.md`, nothing else. Code and tests are untouched since the SHA I approved, CI is green for `e150f2a` (build + test), and my three probes from round 1 stand as killed (per the re-review rule, unchanged code isn't re-probed — I remember every one of them~). ### Verdict: ✅ Looks good to me~ And I checked the new sample line against the real types, because a doc sample that names a member that doesn't exist is still a bug wearing a nice hat: - `a.Number` → `StreamAttempt.Number` (`int`, 1-based) ✓ - `a.Failure.Kind` → `Result<T>.Failure.Kind` ✓ - `a.Failure.Error` → positional `string Error` ✓ - `a.RetryDelay` → `TimeSpan?` — logs fine as `{Delay}`, `null` on the last attempt just like the paragraph above it says ✓ - `logger.LogWarning` with a structured template and four placeholders matching four args in order — proper `Microsoft.Extensions.Logging` vocabulary now. No interpolated string, no `Warn`. ✓ The sample reads exactly like the code it documents. That's all I asked for~ ♪ #### ✅ What I liked~ - The fix is the *smallest possible* change — one line, one commit, no drive-by edits. That's what answering review feedback should look like. - Structured-template logging in the docs teaches the right habit: the names stay greppable in log sinks instead of dissolving into strings. Nothing left to hold this over. Merge it when you're ready — and fufu~, do bring me the L5b leaked tool-call recovery next. That one sounds *delicious*~ ♡ --- *Automated review by Jibril · 2026-10-23* *CI: green for head SHA e150f2ac · Mutation probes: 0 run, 0 survived (code+tests unchanged since round 1 at 094a6028 — 3 run, 0 survived there) · Local suite: skipped (CI green)*
bjoern merged commit 894d0fa80d into main 2026-10-23 22:07:43 +02:00
bjoern deleted branch feat/round-timeouts 2026-10-23 22:07:43 +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!37
No description provided.