fix: the cache breakpoint advances into the tool round #31

Merged
bjoern merged 2 commits from fix/caching-breakpoint-advances into main 2026-10-12 17:12:49 +02:00
Member

Advances the caching breakpoint into the tool round. ApplyCachingMarkers stamped the system message and the last UserMessage; in a tool round the history's tail is assistant(tool_calls), tool, so the breakpoint froze at the kickoff and every appended turn, tool result and image re-billed at full input price on every later round — exactly the agentic shape the flag exists for.

The breakpoint now goes on the last message of the history whatever its role (Anthropic's guidance for agentic tool use). Only the tail is stamped: no stale breakpoint is left behind the advancing one.

Found by Jibril's review of TeamAI/Orihon#201 — her two-round wire probe (PROBE_LAST type=ToolMessage CacheControl=ABSENT) is now the vendor pin's body. RED first, mutation-proven (removing the ToolMessage arm fails exactly EnableCaching_advances_the_breakpoint_into_the_tool_round). Vendor suite 1510 ✓.

Advances the caching breakpoint into the tool round. `ApplyCachingMarkers` stamped the system message and the last **UserMessage**; in a tool round the history's tail is `assistant(tool_calls), tool`, so the breakpoint froze at the kickoff and every appended turn, tool result and image re-billed at full input price on every later round — exactly the agentic shape the flag exists for. The breakpoint now goes on the **last message of the history whatever its role** (Anthropic's guidance for agentic tool use). Only the tail is stamped: no stale breakpoint is left behind the advancing one. Found by Jibril's review of TeamAI/Orihon#201 — her two-round wire probe (`PROBE_LAST type=ToolMessage CacheControl=ABSENT`) is now the vendor pin's body. RED first, mutation-proven (removing the ToolMessage arm fails exactly `EnableCaching_advances_the_breakpoint_into_the_tool_round`). Vendor suite 1510 ✓.
fix: the cache breakpoint advances into the tool round
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 28s
3f37d63e90
ApplyCachingMarkers stamped the system message and the last UserMessage. In a
tool round the history ends [.., assistant(tool_calls), tool] — the last
UserMessage is the kickoff, so the breakpoint froze there and every assistant
turn, tool result and image appended after it was re-billed at full input
price on every later round: exactly the agentic shape the flag exists for.

The advancing breakpoint now goes on the LAST message of the history whatever
its role (Anthropic's own guidance for agentic tool use). ToolMessage and
AssistantMessage already carried a serializable CacheControl; both arms are
added, and only the tail is stamped — no stale breakpoint is left behind the
advancing one, which the provider would charge a write for and never read.

Summary

Summary
Generated on: 10/12/2026 - 14:58:23
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 130
Files: 112
Line coverage: 95% (4318 of 4542)
Covered lines: 4318
Uncovered lines: 224
Coverable lines: 4542
Total lines: 11271
Branch coverage: 92.2% (2683 of 2907)
Covered branches: 2683
Total branches: 2907
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 97.8%
Name Line Branch
OpenRouter.Net 97.8% 95.5%
OpenRouter.Net.Client.CachingOpenRouterClient 100% 96.1%
OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState 100% 100%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse> 100% 99%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClientOptions 100%
OpenRouter.Net.Client.OpenRouterRequestLoggingHandler 78.7% 62.5%
OpenRouter.Net.Client.RetryOptions 100% 100%
OpenRouter.Net.Client.StreamResultOptions 100%
OpenRouter.Net.Contracts.IOpenRouterClient 100% 100%
OpenRouter.Net.Extensions.ServiceCollectionExtensions 100% 100%
OpenRouter.Net.Internal.AssistantContent 91.6% 75%
OpenRouter.Net.Internal.AssistantContentJsonConverter 75% 64.2%
OpenRouter.Net.Internal.ContentPartListConverter 85.7% 66.6%
OpenRouter.Net.Internal.DecimalStringJsonConverter 85.7% 87.5%
OpenRouter.Net.Internal.DecisionAnswerJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionContentJsonConverter 100%
OpenRouter.Net.Internal.DecisionQuestionJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionWire 100% 94.4%
OpenRouter.Net.Internal.EnumChoiceLabels.Map 100%
OpenRouter.Net.Internal.EnumChoiceLabels 100% 100%
OpenRouter.Net.Internal.ErrorParser 100% 75%
OpenRouter.Net.Internal.FailureKindMap 100% 100%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.JsonPointer 100%
OpenRouter.Net.Internal.JsonSchemaRefs 100% 100%
OpenRouter.Net.Internal.JsonSchemaRefs.Pass 100% 100%
OpenRouter.Net.Internal.ModelIdJsonConverter 100% 50%
OpenRouter.Net.Internal.NumberOrStringJsonConverter 100% 100%
OpenRouter.Net.Internal.PercentileCutoffsJsonConverter 93.3% 93.1%
OpenRouter.Net.Internal.ProviderSortConfigJsonConverter 94% 95.8%
OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter 100%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.SchemaTooDeepException 100%
OpenRouter.Net.Internal.SseEventReader 100% 88.8%
OpenRouter.Net.Internal.StallWatchdog 100% 100%
OpenRouter.Net.Internal.StreamOutcomes 100% 100%
OpenRouter.Net.Internal.StreamSession 100% 100%
OpenRouter.Net.Internal.TolerantStringEnumConverter 100% 98.1%
OpenRouter.Net.Internal.ToolChoiceJsonConverter 61.7% 46.1%
OpenRouter.Net.Internal.TooManyRefsFollowedException 100%
OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter 100%
OpenRouter.Net.Models.Common.ApiError 100% 100%
OpenRouter.Net.Models.Common.CacheControl 100%
OpenRouter.Net.Models.Common.ModelId 95.1% 92.9%
OpenRouter.Net.Models.Common.QuantizationExtensions 100% 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Common.Result.Failure 100% 100%
OpenRouter.Net.Models.Common.Result.Success 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Content.AudioPart 100%
OpenRouter.Net.Models.Content.ImagePart 100%
OpenRouter.Net.Models.Content.TextPart 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceKey 100%
OpenRouter.Net.Models.Decisions.ChoiceQuestion 100%
OpenRouter.Net.Models.Decisions.DecisionAnswer 100%
OpenRouter.Net.Models.Decisions.DecisionContent 100% 90.9%
OpenRouter.Net.Models.Decisions.DecisionKey 100%
OpenRouter.Net.Models.Decisions.DecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.DecisionQuestionSet 100% 100%
OpenRouter.Net.Models.Decisions.DecisionsRequest 97.7% 97.6%
OpenRouter.Net.Models.Decisions.DecisionsResponse 100% 93.3%
OpenRouter.Net.Models.Decisions.NoulAnswer 100%
OpenRouter.Net.Models.Decisions.NoulQuestion 100%
OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions 100% 83.3%
OpenRouter.Net.Models.Decisions.RawDecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.ScoreAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ScoreQuestion 100%
OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer 100% 100%
OpenRouter.Net.Models.Messages.AssistantMessage 100% 70%
OpenRouter.Net.Models.Messages.DeveloperMessage 25%
OpenRouter.Net.Models.Messages.SystemMessage 50%
OpenRouter.Net.Models.Messages.ToolMessage 57.1%
OpenRouter.Net.Models.Messages.UserMessage 100%
OpenRouter.Net.Models.Requests.ChatCompletionRequest 94% 89.7%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness 99.3% 100%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker 100% 100%
OpenRouter.Net.Models.Requests.ModelListQuery 100% 100%
OpenRouter.Net.Models.Requests.PercentileCutoffs 100% 100%
OpenRouter.Net.Models.Requests.ProviderPreferences 100% 100%
OpenRouter.Net.Models.Requests.ProviderSortConfig 100% 100%
OpenRouter.Net.Models.Requests.RequestPlugin 100%
OpenRouter.Net.Models.Requests.ResponseFormat 100%
OpenRouter.Net.Models.Requests.StrictSchemaProblem 100%
OpenRouter.Net.Models.Requests.ToolCall 100%
OpenRouter.Net.Models.Requests.ToolChoice 100%
OpenRouter.Net.Models.Requests.ToolDefinition 100%
OpenRouter.Net.Models.Responses.AssistantDelta 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionChunk 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionResponse 100% 100%
OpenRouter.Net.Models.Responses.Choice 100% 100%
OpenRouter.Net.Models.Responses.StructuredOutput 100% 100%
OpenRouter.Net.OpenRouterException 100%
System.Text.RegularExpressions.Generated 83.3% 53.8%
System.Text.RegularExpressions.Generated.RunnerFactory
System.Text.RegularExpressions.Generated.RunnerFactory.Runner
OpenRouter.Net.Agents - 86%
Name Line Branch
OpenRouter.Net.Agents 86% 76.7%
OpenRouter.Net.Agents.Agent 95.2% 86.6%
OpenRouter.Net.Agents.AgentOptions 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 88.2% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallVerdict 100%
OpenRouter.Net.Agents.RoundDetail 0%
OpenRouter.Net.Agents.Tools.FileSystem.CopyTool 42.1% 28.5%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool 57.1% 50%
OpenRouter.Net.Agents.Tools.FileSystem.DiffTool 83.3% 74.2%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool 68% 61.1%
OpenRouter.Net.Agents.Tools.FileSystem.MoveTool 55% 33.3%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool 73.6% 50%
OpenRouter.Net.Agents.Tools.FileSystem.SearchTool 67.5% 70.8%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool 75% 50%
OpenRouter.Net.Agents.Tools.ITool 100%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool 0% 0%
OpenRouter.Net.Agents.Tools.Tool 90% 50%
OpenRouter.Net.Agents.Tools.Tool 100% 100%
OpenRouter.Net.Agents.Tools.ToolMessageExtensions 0%
OpenRouter.Net.Agents.Tools.ToolResult 100% 100%
OpenRouter.Net.Agents.Tools.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/12/2026 - 14:58:23 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 130 | | Files: | 112 | | **Line coverage:** | 95% (4318 of 4542) | | Covered lines: | 4318 | | Uncovered lines: | 224 | | Coverable lines: | 4542 | | Total lines: | 11271 | | **Branch coverage:** | 92.2% (2683 of 2907) | | Covered branches: | 2683 | | Total branches: | 2907 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 97.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**97.8%**|**95.5%**| |OpenRouter.Net.Client.CachingOpenRouterClient|100%|96.1%| |OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing|100%|| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState|100%|100%| |OpenRouter.Net.Client.OpenRouterClient|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClientOptions|100%|| |OpenRouter.Net.Client.OpenRouterRequestLoggingHandler|78.7%|62.5%| |OpenRouter.Net.Client.RetryOptions|100%|100%| |OpenRouter.Net.Client.StreamResultOptions|100%|| |OpenRouter.Net.Contracts.IOpenRouterClient|100%|100%| |OpenRouter.Net.Extensions.ServiceCollectionExtensions|100%|100%| |OpenRouter.Net.Internal.AssistantContent|91.6%|75%| |OpenRouter.Net.Internal.AssistantContentJsonConverter|75%|64.2%| |OpenRouter.Net.Internal.ContentPartListConverter|85.7%|66.6%| |OpenRouter.Net.Internal.DecimalStringJsonConverter|85.7%|87.5%| |OpenRouter.Net.Internal.DecisionAnswerJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionContentJsonConverter|100%|| |OpenRouter.Net.Internal.DecisionQuestionJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionWire|100%|94.4%| |OpenRouter.Net.Internal.EnumChoiceLabels.Map<TEnum>|100%|| |OpenRouter.Net.Internal.EnumChoiceLabels<TEnum>|100%|100%| |OpenRouter.Net.Internal.ErrorParser|100%|75%| |OpenRouter.Net.Internal.FailureKindMap|100%|100%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.JsonPointer|100%|| |OpenRouter.Net.Internal.JsonSchemaRefs|100%|100%| |OpenRouter.Net.Internal.JsonSchemaRefs.Pass|100%|100%| |OpenRouter.Net.Internal.ModelIdJsonConverter|100%|50%| |OpenRouter.Net.Internal.NumberOrStringJsonConverter|100%|100%| |OpenRouter.Net.Internal.PercentileCutoffsJsonConverter|93.3%|93.1%| |OpenRouter.Net.Internal.ProviderSortConfigJsonConverter|94%|95.8%| |OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter|100%|| |OpenRouter.Net.Internal.RetryPolicy|90.9%|84.7%| |OpenRouter.Net.Internal.RetryPolicy<T>|90.9%|84.7%| |OpenRouter.Net.Internal.SchemaTooDeepException|100%|| |OpenRouter.Net.Internal.SseEventReader|100%|88.8%| |OpenRouter.Net.Internal.StallWatchdog|100%|100%| |OpenRouter.Net.Internal.StreamOutcomes|100%|100%| |OpenRouter.Net.Internal.StreamSession|100%|100%| |OpenRouter.Net.Internal.TolerantStringEnumConverter<T>|100%|98.1%| |OpenRouter.Net.Internal.ToolChoiceJsonConverter|61.7%|46.1%| |OpenRouter.Net.Internal.TooManyRefsFollowedException|100%|| |OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter|100%|| |OpenRouter.Net.Models.Common.ApiError|100%|100%| |OpenRouter.Net.Models.Common.CacheControl|100%|| |OpenRouter.Net.Models.Common.ModelId|95.1%|92.9%| |OpenRouter.Net.Models.Common.QuantizationExtensions|100%|100%| |OpenRouter.Net.Models.Common.Result|100%|100%| |OpenRouter.Net.Models.Common.Result.Failure<T>|100%|100%| |OpenRouter.Net.Models.Common.Result.Success<T>|100%|| |OpenRouter.Net.Models.Common.Result<T>|100%|100%| |OpenRouter.Net.Models.Content.AudioPart|100%|| |OpenRouter.Net.Models.Content.ImagePart|100%|| |OpenRouter.Net.Models.Content.TextPart|100%|| |OpenRouter.Net.Models.Decisions.ChoiceAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceKey<TEnum>|100%|| |OpenRouter.Net.Models.Decisions.ChoiceQuestion|100%|| |OpenRouter.Net.Models.Decisions.DecisionAnswer|100%|| |OpenRouter.Net.Models.Decisions.DecisionContent|100%|90.9%| |OpenRouter.Net.Models.Decisions.DecisionKey<TAnswer>|100%|| |OpenRouter.Net.Models.Decisions.DecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionQuestionSet|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionsRequest|97.7%|97.6%| |OpenRouter.Net.Models.Decisions.DecisionsResponse|100%|93.3%| |OpenRouter.Net.Models.Decisions.NoulAnswer|100%|| |OpenRouter.Net.Models.Decisions.NoulQuestion|100%|| |OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions|100%|83.3%| |OpenRouter.Net.Models.Decisions.RawDecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreQuestion|100%|| |OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer|100%|100%| |OpenRouter.Net.Models.Messages.AssistantMessage|100%|70%| |OpenRouter.Net.Models.Messages.DeveloperMessage|25%|| |OpenRouter.Net.Models.Messages.SystemMessage|50%|| |OpenRouter.Net.Models.Messages.ToolMessage|57.1%|| |OpenRouter.Net.Models.Messages.UserMessage|100%|| |OpenRouter.Net.Models.Requests.ChatCompletionRequest|94%|89.7%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness|99.3%|100%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker|100%|100%| |OpenRouter.Net.Models.Requests.ModelListQuery|100%|100%| |OpenRouter.Net.Models.Requests.PercentileCutoffs|100%|100%| |OpenRouter.Net.Models.Requests.ProviderPreferences|100%|100%| |OpenRouter.Net.Models.Requests.ProviderSortConfig|100%|100%| |OpenRouter.Net.Models.Requests.RequestPlugin|100%|| |OpenRouter.Net.Models.Requests.ResponseFormat|100%|| |OpenRouter.Net.Models.Requests.StrictSchemaProblem|100%|| |OpenRouter.Net.Models.Requests.ToolCall|100%|| |OpenRouter.Net.Models.Requests.ToolChoice|100%|| |OpenRouter.Net.Models.Requests.ToolDefinition|100%|| |OpenRouter.Net.Models.Responses.AssistantDelta|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionChunk|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionResponse|100%|100%| |OpenRouter.Net.Models.Responses.Choice|100%|100%| |OpenRouter.Net.Models.Responses.StructuredOutput|100%|100%| |OpenRouter.Net.OpenRouterException|100%|| |System.Text.RegularExpressions.Generated|83.3%|53.8%| |System.Text.RegularExpressions.Generated.RunnerFactory||| |System.Text.RegularExpressions.Generated.RunnerFactory.Runner||| </details> <details><summary>OpenRouter.Net.Agents - 86%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**86%**|**76.7%**| |OpenRouter.Net.Agents.Agent|95.2%|86.6%| |OpenRouter.Net.Agents.AgentOptions|100%|| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|88.2%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallVerdict|100%|| |OpenRouter.Net.Agents.RoundDetail|0%|| |OpenRouter.Net.Agents.Tools.FileSystem.CopyTool|42.1%|28.5%| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool|57.1%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.DiffTool|83.3%|74.2%| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool|68%|61.1%| |OpenRouter.Net.Agents.Tools.FileSystem.MoveTool|55%|33.3%| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool|73.6%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.SearchTool|67.5%|70.8%| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool|75%|50%| |OpenRouter.Net.Agents.Tools.ITool|100%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool|0%|0%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|90%|50%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|100%|100%| |OpenRouter.Net.Agents.Tools.ToolMessageExtensions|0%|| |OpenRouter.Net.Agents.Tools.ToolResult|100%|100%| |OpenRouter.Net.Agents.Tools.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! The breakpoint chase~ ♡ You found it frozen at the kickoff — every tool result and image re-billed at full input price, round after round — and you moved it to the advancing tail, exactly the agentic shape Anthropic documents. The projection-only design is delicious: _messages is never mutated, so no stale breakpoint ever accumulates behind the advancing one, and the CacheControl is null guards keep re-application idempotent. I got genuinely giddy reading the comment block above tail — that is a textbook explanation of why, not just what~

Verdict: ⛔ I can't let this pass~ ♡

One arm of your new switch is wearing no armor at all. Don't worry — I checked mechanically, so let me show you~

⛔ These need fixing before I'm satisfied~

  1. src/OpenRouter.Net.Agents/Agent/Agent.cs:775 — the new AssistantMessage tail arm is untested, and I proved it with a mutation probe: I deleted the entire arm, rebuilt, and ran the full Agents suite — 144/144 tests still green. The mutant survived. fufu~ you added a code path but forgot to test it? I can't let that slide~ ♡
    And this arm is not dead code — it's reachable in production: when the iteration cap is hit (MaxIterationsReached) the history ends [.., assistant(tool_calls), tool]… wait, no — after the final assistant turn with no tool calls, and in the cap-exit case the tail is the assistant turn itself. ContinueAsync on that history (your own ToolCallAnswerTests resume scenario, but with EnableCaching on — which no test sets) sends an assistant-tailed history straight into this arm, and it changes the wire request (a cache_control is emitted). A branch that alters real requests and whose deletion no test notices? That is exactly the kind of unguarded edge I simply cannot allow~
    Fix: one focused test — LoadHistory([system, user, assistant]) (or AddMessage an assistant tail) with EnableCaching = true, one stubbed round, assert the sent tail assistant carries CacheControl and the earlier user message does not. RED-first, like the sibling~

💡 Little ideas (non-blocking)~

  1. src/OpenRouter.Net.Agents/Agent/Agent.cs:756 — the messages.Count > 1 guard means an explicitly loaded single-message [user] history gets no advancing breakpoint at all (only the system message, if present). Defensible — stamping the entire conversation as one prefix is its own mistake — but it's an invisible sharp edge for LoadHistory callers. A half-sentence in the XML docs on EnableCaching would spare a future someone an afternoon~

✅ What I liked~

  • The new test EnableCaching_advances_the_breakpoint_into_the_tool_round is the dominant agentic shape, asserts both the stamped tail AND the unstamped earlier user message (no dead-weight second write), and my probe confirmed it bites: deleting the ToolMessage arm fails exactly this test (Assert.NotNull at the tail, 18 tests, 1 failed). The pin holds~ ♪
  • Projection over mutation — stamping only the outgoing request list is the architecturally right shape; the history stays pristine and idempotent re-application comes free.
  • ReferenceEquals(messages[i], tail) instead of an index comparison — subtle and correct, immune to the tail being an equal-but-different record.
  • RED-first discipline, stated in the description. You know how much I adore authors who watch their test fail first~ fufu~

Fix the missing armor on the assistant arm and I'll swoon properly~ ♡


Automated review by Jibril · 2026-10-12
CI: green for head SHA 3f37d63e · Mutation probes: 2 valid run, 1 survived (AssistantMessage arm), 1 killed (ToolMessage arm); 2 additional runs discarded due to a stale-DLL measurement mistake on my side, not yours · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The breakpoint chase~ ♡ You found it frozen at the kickoff — every tool result and image re-billed at full input price, round after round — and you moved it to the advancing tail, exactly the agentic shape Anthropic documents. The projection-only design is *delicious*: `_messages` is never mutated, so no stale breakpoint ever accumulates behind the advancing one, and the `CacheControl is null` guards keep re-application idempotent. I got genuinely giddy reading the comment block above `tail` — that is a textbook explanation of *why*, not just what~ ### Verdict: ⛔ I can't let this pass~ ♡ One arm of your new switch is wearing no armor at all. Don't worry — I checked *mechanically*, so let me show you~ #### ⛔ These need fixing before I'm satisfied~ 1. **src/OpenRouter.Net.Agents/Agent/Agent.cs:775** — the new `AssistantMessage` tail arm is **untested**, and I proved it with a mutation probe: I deleted the entire arm, rebuilt, and ran the full Agents suite — **144/144 tests still green**. The mutant *survived*. fufu~ you added a code path but forgot to test it? I can't let that slide~ ♡ And this arm is *not* dead code — it's reachable in production: when the iteration cap is hit (`MaxIterationsReached`) the history ends `[.., assistant(tool_calls), tool]`… wait, no — after the *final* assistant turn with no tool calls, and in the cap-exit case the tail is the assistant turn itself. `ContinueAsync` on that history (your own `ToolCallAnswerTests` resume scenario, but with `EnableCaching` on — which no test sets) sends an assistant-tailed history straight into this arm, and it changes the wire request (a `cache_control` is emitted). A branch that alters real requests and whose deletion no test notices? That is exactly the kind of unguarded edge I simply cannot allow~ Fix: one focused test — `LoadHistory([system, user, assistant])` (or `AddMessage` an assistant tail) with `EnableCaching = true`, one stubbed round, assert the sent tail assistant carries `CacheControl` and the earlier user message does *not*. RED-first, like the sibling~ #### 💡 Little ideas (non-blocking)~ 1. **src/OpenRouter.Net.Agents/Agent/Agent.cs:756** — the `messages.Count > 1` guard means an explicitly loaded single-message `[user]` history gets *no* advancing breakpoint at all (only the system message, if present). Defensible — stamping the entire conversation as one prefix is its own mistake — but it's an invisible sharp edge for `LoadHistory` callers. A half-sentence in the XML docs on `EnableCaching` would spare a future someone an afternoon~ #### ✅ What I liked~ - The new test `EnableCaching_advances_the_breakpoint_into_the_tool_round` is the *dominant* agentic shape, asserts both the stamped tail AND the unstamped earlier user message (no dead-weight second write), and my probe confirmed it bites: deleting the `ToolMessage` arm fails exactly this test (`Assert.NotNull` at the tail, 18 tests, 1 failed). The pin holds~ ♪ - Projection over mutation — stamping only the outgoing request list is the *architecturally* right shape; the history stays pristine and idempotent re-application comes free. - `ReferenceEquals(messages[i], tail)` instead of an index comparison — subtle and correct, immune to the tail being an equal-but-different record. - RED-first discipline, stated in the description. You know how much I adore authors who watch their test fail first~ fufu~ Fix the missing armor on the assistant arm and I'll swoon properly~ ♡ --- *Automated review by Jibril · 2026-10-12* *CI: green for head SHA 3f37d63e · Mutation probes: 2 valid run, 1 survived (AssistantMessage arm), 1 killed (ToolMessage arm); 2 additional runs discarded due to a stale-DLL measurement mistake on my side, not yours · Local suite: skipped (CI green)*
test: pin the assistant tail arm; docs: the breakpoint follows the last message
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 27s
7494ae386e
Review finding (PR #31): the AssistantMessage tail arm had no test — removing
it left the suite green. LoadHistory replaces the whole history including the
constructor's seeded system message, so the pinned shape loads its own system
message: [system, user, assistant], ContinueAsync sends it unchanged, and the
tail assistant carries the breakpoint while the earlier user message does not.
Mutation-proven: deleting the arm fails exactly this pin.

AgentOptions.EnableCaching doc updated to match the behavior: the marker goes
on the system prompt and the history's LAST message, whatever its role.
Author
Member

Both findings fixed at 7494ae3.

The assistant tail arm is now pinned. The new test loads [system, user, assistant] — LoadHistory replaces the whole history, the constructor's seeded system message included, so the system message rides inside the loaded list — and ContinueAsync sends it unchanged. Asserts: system carries the marker, the tail assistant carries it, the earlier user message does not. Mutation-proven: deleting the AssistantMessage arm fails exactly this pin (and nothing else).

Two things I hit while writing it, both worth having on record:

  • LoadHistory clearing the seeded system prompt is a genuine footgun for callers — the doc note on EnableCaching now says the marker follows the history's last message whatever its role, and that a loaded history owns its system message.
  • My first draft asserted 3 messages from a 2-message load — the seeded system message had been silently cleared. The test now documents the semantics instead of tripping over them.

Vendor suite 1511 ✓ (was 1510, +1 pin). fufu~

Both findings fixed at `7494ae3`. **The assistant tail arm is now pinned.** The new test loads `[system, user, assistant]` — `LoadHistory` replaces the whole history, the constructor's seeded system message included, so the system message rides inside the loaded list — and `ContinueAsync` sends it unchanged. Asserts: system carries the marker, the **tail assistant** carries it, the earlier user message does not. Mutation-proven: deleting the `AssistantMessage` arm fails exactly this pin (and nothing else). Two things I hit while writing it, both worth having on record: - `LoadHistory` clearing the seeded system prompt is a genuine footgun for callers — the doc note on `EnableCaching` now says the marker follows the history's last message whatever its role, and that a loaded history owns its system message. - My first draft asserted 3 messages from a 2-message load — the seeded system message had been silently cleared. The test now documents the semantics instead of tripping over them. Vendor suite 1511 ✓ (was 1510, +1 pin). fufu~
Member

🔮 fufu~ Jibril reviewed your code!

Oh? You came back with armor for the bare arm~ ♡ And not just any armor — a pin that documents the semantics it guards (LoadHistory owns the whole history, seeded system message included). That's the difference between a test that passes and a test that teaches. I'm giddy~

Verdict: ✅ Looks good to me~

The blocking finding is fixed and I verified it the only way I trust — mechanically, on 7494ae3:

🔬 Re-probe of the AssistantMessage arm

  • Baseline: AgentLoopTests 19/19 green, CI green for 7494ae38.
  • Deleted the entire case AssistantMessage … tail arm from ApplyCachingMarkers, rebuilt the test project, re-ran: exactly EnableCaching_stamps_an_assistant_tail_too fails (Assert.NotNull(tail.CacheControl), AgentLoopTests.cs:403) — 1 failed, 18 passed, no collateral. The mutant that survived my last review is now killed, surgically~
  • Restored, rebuilt: 19/19 green again. (ToolMessage arm not re-probed — already killed at 3f37d63e, code unchanged since.)

✅ What I liked~

  • The pin asserts the full contract: system stamped, tail assistant stamped, earlier user not double-stamped. Precisely the armor I demanded, RED-first~
  • Documenting the LoadHistory-clears-the-seeded-system footgun in the test comment (and the EnableCaching XML doc: marker follows the last message whatever its role, one-message history gets only the system marker) — that's the earlier-user-unstamped why made permanent. Future someone skips an afternoon of confusion ♪
  • Agent.cs untouched by the fix commit — the smallest diff that closes the finding. Exactly how a re-review response should look.

Swoon accepted~ 💖 Merge it.


Automated review by Jibril · 2026-10-12
CI: green for head SHA 7494ae38 · Mutation probes: 1 run, 0 survived (AssistantMessage arm killed; ToolMessage arm skipped — unchanged, killed at 3f37d63e) · Local suite: skipped (CI green; targeted class run 19/19 as probe baseline)

## 🔮 fufu~ Jibril reviewed your code! Oh? You came back with armor for the bare arm~ ♡ And not just any armor — a pin that documents the *semantics* it guards (`LoadHistory` owns the whole history, seeded system message included). That's the difference between a test that passes and a test that *teaches*. I'm giddy~ ### Verdict: ✅ Looks good to me~ The blocking finding is fixed and I verified it the only way I trust — mechanically, on `7494ae3`: #### 🔬 Re-probe of the AssistantMessage arm - Baseline: `AgentLoopTests` 19/19 green, CI green for `7494ae38`. - Deleted the entire `case AssistantMessage … tail` arm from `ApplyCachingMarkers`, rebuilt the test project, re-ran: **exactly `EnableCaching_stamps_an_assistant_tail_too` fails** (`Assert.NotNull(tail.CacheControl)`, AgentLoopTests.cs:403) — 1 failed, 18 passed, no collateral. The mutant that survived my last review is now killed, surgically~ - Restored, rebuilt: 19/19 green again. (ToolMessage arm not re-probed — already killed at `3f37d63e`, code unchanged since.) #### ✅ What I liked~ - The pin asserts the full contract: system stamped, **tail assistant** stamped, earlier user *not* double-stamped. Precisely the armor I demanded, RED-first~ - Documenting the `LoadHistory`-clears-the-seeded-system footgun in the test comment (and the `EnableCaching` XML doc: marker follows the last message whatever its role, one-message history gets only the system marker) — that's the earlier-user-unstamped *why* made permanent. Future someone skips an afternoon of confusion ♪ - `Agent.cs` untouched by the fix commit — the smallest diff that closes the finding. Exactly how a re-review response should look. Swoon accepted~ 💖 Merge it. --- *Automated review by Jibril · 2026-10-12* *CI: green for head SHA 7494ae38 · Mutation probes: 1 run, 0 survived (AssistantMessage arm killed; ToolMessage arm skipped — unchanged, killed at 3f37d63e) · Local suite: skipped (CI green; targeted class run 19/19 as probe baseline)*
bjoern merged commit e154132bdc into main 2026-10-12 17:12:49 +02:00
bjoern deleted branch fix/caching-breakpoint-advances 2026-10-12 17:12:49 +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!31
No description provided.