feat(agents): opt-in usage tracking and per-round generation ids #5

Merged
bjoern merged 1 commit from feat/agent-usage-tracking into main 2026-07-25 07:52:40 +02:00
Member

Closes the cost gap Orihon's run monitor hit (TeamAI/Orihon Phase 2, cut 5): AgentResult.AggregatedUsage.Cost was always null because the agent loop never requested the usage block, and RoundDetail carried no generation id to fetch cost post-hoc.

  • AgentOptions.TrackUsage (default off — the usage block adds a little response latency on some providers): when set, BuildRequest adds usage: { include: true } to every request, so RoundDetail.Usage and the aggregated usage carry token counts and Cost. Clone() carries the flag.
  • RoundDetail.ResponseId — the completion's generation id on every round, so exact cost/routing detail is reachable via GetGenerationAsync even with tracking off.

4 new tests (flag on every request incl. multi-round, off-by-default, distinct per-round ids, clone). Full suite 143/143 green.

🤖 Generated with Claude Code

Closes the cost gap Orihon's run monitor hit (TeamAI/Orihon Phase 2, cut 5): `AgentResult.AggregatedUsage.Cost` was always null because the agent loop never requested the usage block, and `RoundDetail` carried no generation id to fetch cost post-hoc. - **`AgentOptions.TrackUsage`** (default **off** — the usage block adds a little response latency on some providers): when set, `BuildRequest` adds `usage: { include: true }` to every request, so `RoundDetail.Usage` and the aggregated usage carry token counts and `Cost`. `Clone()` carries the flag. - **`RoundDetail.ResponseId`** — the completion's generation id on every round, so exact cost/routing detail is reachable via `GetGenerationAsync` even with tracking off. 4 new tests (flag on every request incl. multi-round, off-by-default, distinct per-round ids, clone). Full suite 143/143 green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(agents): opt-in usage tracking and per-round generation ids
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
0d4e39f620
Orihon's run monitor wants per-run cost, and AgentResult's aggregated
usage was always empty because the loop never asked for the usage
block. AgentOptions.TrackUsage now sets usage.include on every request
(off by default — the block adds a little latency on some providers),
and RoundDetail carries the completion's generation id so exact cost
can be fetched post-hoc via GetGenerationAsync when tracking is off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 07/25/2026 - 05:48:20
Coverage date: 07/25/2026 - 05:48:16 - 07/25/2026 - 05:48:18
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 105
Files: 105
Line coverage: 75.9% (1401 of 1844)
Covered lines: 1401
Uncovered lines: 443
Coverable lines: 1844
Total lines: 5359
Branch coverage: 64.8% (477 of 736)
Covered branches: 477
Total branches: 736
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 69.6%
Name Line Branch
OpenRouter.Net 69.6% 64.5%
OpenRouter.Net.Client.CachingOpenRouterClient 72.4% 81.2%
OpenRouter.Net.Client.OpenRouterClient 87.2% 72.9%
OpenRouter.Net.Client.OpenRouterClientOptions 0%
OpenRouter.Net.Client.OpenRouterRequestLoggingHandler 27.2% 7.1%
OpenRouter.Net.Client.RetryOptions 80% 0%
OpenRouter.Net.Extensions.ServiceCollectionExtensions 0% 0%
OpenRouter.Net.Internal.ApiErrorEnvelope 100%
OpenRouter.Net.Internal.AssistantContent 92.8% 75%
OpenRouter.Net.Internal.AssistantContentJsonConverter 75% 64.2%
OpenRouter.Net.Internal.ContentPartListConverter 85.7% 66.6%
OpenRouter.Net.Internal.ErrorParser 92.3% 62.5%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.ListEnvelope`1 100%
OpenRouter.Net.Internal.ObjectEnvelope`1 100%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.SseEventReader 91.6% 83.3%
OpenRouter.Net.Internal.TolerantStringEnumConverter`1 90.1% 80%
OpenRouter.Net.Internal.ToolChoiceJsonConverter 61.7% 46.1%
OpenRouter.Net.Models.Common.ApiError 100%
OpenRouter.Net.Models.Common.CacheControl 100%
OpenRouter.Net.Models.Common.Result 100%
OpenRouter.Net.Models.Common.Result`1 60% 30%
OpenRouter.Net.Models.Content.AudioPart 100%
OpenRouter.Net.Models.Content.ImagePart 100%
OpenRouter.Net.Models.Content.ImageUrl 100%
OpenRouter.Net.Models.Content.InputAudio 100%
OpenRouter.Net.Models.Content.TextPart 100%
OpenRouter.Net.Models.Messages.AssistantMessage 100% 70%
OpenRouter.Net.Models.Messages.DeveloperMessage 0%
OpenRouter.Net.Models.Messages.SystemMessage 57.1%
OpenRouter.Net.Models.Messages.ToolMessage 54.5%
OpenRouter.Net.Models.Messages.UserMessage 100%
OpenRouter.Net.Models.Requests.ChatCompletionRequest 92.6% 58.3%
OpenRouter.Net.Models.Requests.FunctionCall 100%
OpenRouter.Net.Models.Requests.FunctionDefinition 75%
OpenRouter.Net.Models.Requests.JsonSchemaSpec 0%
OpenRouter.Net.Models.Requests.ProviderPreferences 0%
OpenRouter.Net.Models.Requests.ReasoningEncryptedDetail 100%
OpenRouter.Net.Models.Requests.ReasoningOptions 100%
OpenRouter.Net.Models.Requests.ReasoningSummaryDetail 100%
OpenRouter.Net.Models.Requests.ReasoningTextDetail 100%
OpenRouter.Net.Models.Requests.ResponseFormat 0%
OpenRouter.Net.Models.Requests.ToolCall 75%
OpenRouter.Net.Models.Requests.ToolChoice 100%
OpenRouter.Net.Models.Requests.ToolDefinition 100%
OpenRouter.Net.Models.Requests.UsageOptions 100%
OpenRouter.Net.Models.Responses.ChatCompletionChunk 44.4% 0%
OpenRouter.Net.Models.Responses.ChatCompletionResponse 80% 0%
OpenRouter.Net.Models.Responses.Choice 93.7% 100%
OpenRouter.Net.Models.Responses.CompletionTokensDetails 0%
OpenRouter.Net.Models.Responses.CreditsInfo 100%
OpenRouter.Net.Models.Responses.GenerationInfo 9%
OpenRouter.Net.Models.Responses.KeyInfo 0%
OpenRouter.Net.Models.Responses.ModelArchitecture 20%
OpenRouter.Net.Models.Responses.ModelEndpointsResponse 0%
OpenRouter.Net.Models.Responses.ModelInfo 20%
OpenRouter.Net.Models.Responses.ModelPricing 0%
OpenRouter.Net.Models.Responses.PromptTokensDetails 0%
OpenRouter.Net.Models.Responses.ProviderEndpoint 0%
OpenRouter.Net.Models.Responses.RateLimit 0%
OpenRouter.Net.Models.Responses.TopProvider 0%
OpenRouter.Net.Models.Responses.Usage 66.6%
OpenRouter.Net.OpenRouterException 83.3%
System.Text.RegularExpressions.Generated 83.3% 57.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FFC6B051A15CDCE5
43F954A3762D43C2CD5DCD5CDF29C19D3789DC49A7DCA1C47__Base64DataUriPattern_0
82% 58.3%
OpenRouter.Net.Agents - 81.6%
Name Line Branch
OpenRouter.Net.Agents 81.6% 65.6%
OpenRouter.Net.Agents.Agent 92.2% 78.2%
OpenRouter.Net.Agents.AgentCompletedEvent 100%
OpenRouter.Net.Agents.AgentEvent 100%
OpenRouter.Net.Agents.AgentOptions 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.AgentSnapshot 100%
OpenRouter.Net.Agents.AssistantTurnEvent 100%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 0%
OpenRouter.Net.Agents.RoundDetail 88.8%
OpenRouter.Net.Agents.RoundEndEvent 100%
OpenRouter.Net.Agents.ToolCallCompletedEvent 100%
OpenRouter.Net.Agents.ToolCallSkippedEvent 100%
OpenRouter.Net.Agents.ToolCallStartedEvent 100%
OpenRouter.Net.Agents.ToolExecutionDetail 100%
OpenRouter.Net.Agents.Tools.FileSystem.CopyParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.CopyTool 42.1% 21.4%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteParams 50%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool 57.1% 50%
OpenRouter.Net.Agents.Tools.FileSystem.DiffParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.DiffTool 83.3% 77.1%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool 68% 61.1%
OpenRouter.Net.Agents.Tools.FileSystem.MoveParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.MoveTool 55% 33.3%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool 73.6% 60%
OpenRouter.Net.Agents.Tools.FileSystem.SearchParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.SearchTool 67.5% 70.8%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileParams 100%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool 75% 62.5%
OpenRouter.Net.Agents.Tools.ITool 100%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentParams 0%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool 0% 0%
OpenRouter.Net.Agents.Tools.Tool`1 81.8% 63.6%
OpenRouter.Net.Agents.Tools.ToolInvocationContext 100%
OpenRouter.Net.Agents.Tools.ToolMessageExtensions 0%
OpenRouter.Net.Agents.Tools.ToolResult 100% 100%
OpenRouter.Net.Agents.Tools.Workspace 100% 91.6%
OpenRouter.Net.Imaging - 82.2%
Name Line Branch
OpenRouter.Net.Imaging 82.2% 59.3%
OpenRouter.Net.Imaging.ImageEncodeOptions 92.3% 75%
OpenRouter.Net.Imaging.ImageEncoder 80.3% 57.1%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/25/2026 - 05:48:20 | | Coverage date: | 07/25/2026 - 05:48:16 - 07/25/2026 - 05:48:18 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 105 | | Files: | 105 | | **Line coverage:** | 75.9% (1401 of 1844) | | Covered lines: | 1401 | | Uncovered lines: | 443 | | Coverable lines: | 1844 | | Total lines: | 5359 | | **Branch coverage:** | 64.8% (477 of 736) | | Covered branches: | 477 | | Total branches: | 736 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 69.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**69.6%**|**64.5%**| |OpenRouter.Net.Client.CachingOpenRouterClient|72.4%|81.2%| |OpenRouter.Net.Client.OpenRouterClient|87.2%|72.9%| |OpenRouter.Net.Client.OpenRouterClientOptions|0%|| |OpenRouter.Net.Client.OpenRouterRequestLoggingHandler|27.2%|7.1%| |OpenRouter.Net.Client.RetryOptions|80%|0%| |OpenRouter.Net.Extensions.ServiceCollectionExtensions|0%|0%| |OpenRouter.Net.Internal.ApiErrorEnvelope|100%|| |OpenRouter.Net.Internal.AssistantContent|92.8%|75%| |OpenRouter.Net.Internal.AssistantContentJsonConverter|75%|64.2%| |OpenRouter.Net.Internal.ContentPartListConverter|85.7%|66.6%| |OpenRouter.Net.Internal.ErrorParser|92.3%|62.5%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.ListEnvelope`1|100%|| |OpenRouter.Net.Internal.ObjectEnvelope`1|100%|| |OpenRouter.Net.Internal.RetryPolicy|90.9%|84.7%| |OpenRouter.Net.Internal.SseEventReader|91.6%|83.3%| |OpenRouter.Net.Internal.TolerantStringEnumConverter`1|90.1%|80%| |OpenRouter.Net.Internal.ToolChoiceJsonConverter|61.7%|46.1%| |OpenRouter.Net.Models.Common.ApiError|100%|| |OpenRouter.Net.Models.Common.CacheControl|100%|| |OpenRouter.Net.Models.Common.Result|100%|| |OpenRouter.Net.Models.Common.Result`1|60%|30%| |OpenRouter.Net.Models.Content.AudioPart|100%|| |OpenRouter.Net.Models.Content.ImagePart|100%|| |OpenRouter.Net.Models.Content.ImageUrl|100%|| |OpenRouter.Net.Models.Content.InputAudio|100%|| |OpenRouter.Net.Models.Content.TextPart|100%|| |OpenRouter.Net.Models.Messages.AssistantMessage|100%|70%| |OpenRouter.Net.Models.Messages.DeveloperMessage|0%|| |OpenRouter.Net.Models.Messages.SystemMessage|57.1%|| |OpenRouter.Net.Models.Messages.ToolMessage|54.5%|| |OpenRouter.Net.Models.Messages.UserMessage|100%|| |OpenRouter.Net.Models.Requests.ChatCompletionRequest|92.6%|58.3%| |OpenRouter.Net.Models.Requests.FunctionCall|100%|| |OpenRouter.Net.Models.Requests.FunctionDefinition|75%|| |OpenRouter.Net.Models.Requests.JsonSchemaSpec|0%|| |OpenRouter.Net.Models.Requests.ProviderPreferences|0%|| |OpenRouter.Net.Models.Requests.ReasoningEncryptedDetail|100%|| |OpenRouter.Net.Models.Requests.ReasoningOptions|100%|| |OpenRouter.Net.Models.Requests.ReasoningSummaryDetail|100%|| |OpenRouter.Net.Models.Requests.ReasoningTextDetail|100%|| |OpenRouter.Net.Models.Requests.ResponseFormat|0%|| |OpenRouter.Net.Models.Requests.ToolCall|75%|| |OpenRouter.Net.Models.Requests.ToolChoice|100%|| |OpenRouter.Net.Models.Requests.ToolDefinition|100%|| |OpenRouter.Net.Models.Requests.UsageOptions|100%|| |OpenRouter.Net.Models.Responses.ChatCompletionChunk|44.4%|0%| |OpenRouter.Net.Models.Responses.ChatCompletionResponse|80%|0%| |OpenRouter.Net.Models.Responses.Choice|93.7%|100%| |OpenRouter.Net.Models.Responses.CompletionTokensDetails|0%|| |OpenRouter.Net.Models.Responses.CreditsInfo|100%|| |OpenRouter.Net.Models.Responses.GenerationInfo|9%|| |OpenRouter.Net.Models.Responses.KeyInfo|0%|| |OpenRouter.Net.Models.Responses.ModelArchitecture|20%|| |OpenRouter.Net.Models.Responses.ModelEndpointsResponse|0%|| |OpenRouter.Net.Models.Responses.ModelInfo|20%|| |OpenRouter.Net.Models.Responses.ModelPricing|0%|| |OpenRouter.Net.Models.Responses.PromptTokensDetails|0%|| |OpenRouter.Net.Models.Responses.ProviderEndpoint|0%|| |OpenRouter.Net.Models.Responses.RateLimit|0%|| |OpenRouter.Net.Models.Responses.TopProvider|0%|| |OpenRouter.Net.Models.Responses.Usage|66.6%|| |OpenRouter.Net.OpenRouterException|83.3%|| |System.Text.RegularExpressions.Generated|83.3%|57.6%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FFC6B051A15CDCE5<br/>43F954A3762D43C2CD5DCD5CDF29C19D3789DC49A7DCA1C47__Base64DataUriPattern_0|82%|58.3%| </details> <details><summary>OpenRouter.Net.Agents - 81.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**81.6%**|**65.6%**| |OpenRouter.Net.Agents.Agent|92.2%|78.2%| |OpenRouter.Net.Agents.AgentCompletedEvent|100%|| |OpenRouter.Net.Agents.AgentEvent|100%|| |OpenRouter.Net.Agents.AgentOptions|100%|| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.AgentSnapshot|100%|| |OpenRouter.Net.Agents.AssistantTurnEvent|100%|| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|0%|| |OpenRouter.Net.Agents.RoundDetail|88.8%|| |OpenRouter.Net.Agents.RoundEndEvent|100%|| |OpenRouter.Net.Agents.ToolCallCompletedEvent|100%|| |OpenRouter.Net.Agents.ToolCallSkippedEvent|100%|| |OpenRouter.Net.Agents.ToolCallStartedEvent|100%|| |OpenRouter.Net.Agents.ToolExecutionDetail|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.CopyParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.CopyTool|42.1%|21.4%| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteParams|50%|| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool|57.1%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.DiffParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.DiffTool|83.3%|77.1%| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool|68%|61.1%| |OpenRouter.Net.Agents.Tools.FileSystem.MoveParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.MoveTool|55%|33.3%| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool|73.6%|60%| |OpenRouter.Net.Agents.Tools.FileSystem.SearchParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.SearchTool|67.5%|70.8%| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileParams|100%|| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool|75%|62.5%| |OpenRouter.Net.Agents.Tools.ITool|100%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentParams|0%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool|0%|0%| |OpenRouter.Net.Agents.Tools.Tool`1|81.8%|63.6%| |OpenRouter.Net.Agents.Tools.ToolInvocationContext|100%|| |OpenRouter.Net.Agents.Tools.ToolMessageExtensions|0%|| |OpenRouter.Net.Agents.Tools.ToolResult|100%|100%| |OpenRouter.Net.Agents.Tools.Workspace|100%|91.6%| </details> <details><summary>OpenRouter.Net.Imaging - 82.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Imaging**|**82.2%**|**59.3%**| |OpenRouter.Net.Imaging.ImageEncodeOptions|92.3%|75%| |OpenRouter.Net.Imaging.ImageEncoder|80.3%|57.1%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ ♪ A cost-visibility PR that's opt-in by default and threads a generation id through every round so cost is reachable even when tracking is off? Someone's been thinking about latency-sensitive callers. This is the kind of design restraint I adore — the feature is there when you want it, invisible when you don't. Delightful~

Verdict: Looks good to me~

I tore this one apart looking for a loose thread and... there isn't one. The yandere is pouting because she wanted to find something to fix~ ♡ But correctness first, feelings later.

What I verified, branch by branch:

  • AgentOptions.TrackUsage — default false, XML doc explains the latency trade-off honestly. Clone() carries it (line 126, right beside EnableCaching where it belongs).
  • BuildRequest line 568Usage = _options.TrackUsage ? new UsageOptions { Include = true } : null. I checked UsageOptions — it's a sealed record with required bool Include and [JsonPropertyName("include")]. The shape is exactly right. Off-by-default means null on the wire → byte-identical requests to pre-PR behavior. Zero drift.
  • RoundDetail.ResponseIdstring? (correctly nullable, since a provider could omit it). Doc points at IOpenRouterClient.GetGenerationAsync. I confirmed ChatCompletionResponse.Id is required string, so success.Value.Id can never produce a wrong null.
  • Both construction sites covered — there are exactly two new RoundDetail { ... } sites in Agent.cs (no-tool-calls arm at :223, tool-calls arm at :264) and both got ResponseId = success.Value.Id. I grepped to make sure there's no third site that was missed. There isn't.

On the tests — these are real tests, not tautologies, and that makes me so happy~

  • TrackUsage_asks_for_the_usage_block_on_every_request — enqueues a tool-call + assistant (2 rounds), then asserts r.Usage?.Include is true on both captured requests via Assert.All. Proves the flag hits every round, not just the first.
  • Usage_tracking_is_off_by_default — asserts Assert.Single(...).Usage is null. Pins the opt-in contract.
  • Every_round_carries_its_generation_id — and here's the clever part: this test runs with TrackUsage off (default). It proves ResponseId is populated independently of usage tracking — exactly the PR body's "even with tracking off" promise. Then asserts Distinct().Count() == 2 so you know each round got its own id, not the same one duplicated. That's how you test an invariant.
  • Clone_carries_the_tracking_flag — round-trips the flag through Clone().

Coverage (local, since the coverage bot hasn't posted for 0d4e39f yet):

  • Full suite 143/143 green (45 Agents + 7 Imaging + 91 Net = 143, matches PR body exactly).
  • Agent.cs: both new ResponseId sites exercised (24 hits no-tool arm, 13 hits tool arm), BuildRequest ternary hit 38×.
  • AgentOptions.cs: 100% line / 100% branch. RoundDetail.cs: 88.8% line / 100% branch.
  • Build: 0 warnings, 0 errors.

What I liked~

  • Opt-in by default with an honest latency rationale in the doc comment. That's senior-grade API design — you didn't just turn it on because you could.
  • ResponseId decoupled from TrackUsage. Cost is reachable post-hoc via GetGenerationAsync even when the caller opted out of per-request usage blocks. Two ways to get cost, neither forces the other. Elegant.
  • The off-by-default test for ResponseId. Most authors would've written that test with TrackUsage = true and not noticed it muddies the invariant. You left it default-off on purpose and it makes the test mean more.
  • Clone() consistency. Every existing field is copied member-wise; you slotted TrackUsage into the same cluster without disrupting the pattern.

Fufu~ nothing to fix, nothing to block, nothing even worth a "consider also." Go merge it~ ♡


Automated review by Jibril · 2026-07-25
CI/CD: absent for head 0d4e39f (PR just opened, no coverage bot comment yet) · Local checks: build 0/0, 143/143 tests pass, coverage inspected on changed files

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ ♪ A cost-visibility PR that's *opt-in by default* and threads a generation id through every round so cost is reachable *even when tracking is off*? Someone's been thinking about latency-sensitive callers. This is the kind of design restraint I adore — the feature is there when you want it, invisible when you don't. *Delightful~* ### Verdict: ✅ Looks good to me~ I tore this one apart looking for a loose thread and... there isn't one. The yandere is *pouting* because she wanted to find something to fix~ ♡ But correctness first, feelings later. **What I verified, branch by branch:** - **`AgentOptions.TrackUsage`** — default `false`, XML doc explains the latency trade-off honestly. `Clone()` carries it (line 126, right beside `EnableCaching` where it belongs). ✅ - **`BuildRequest` line 568** — `Usage = _options.TrackUsage ? new UsageOptions { Include = true } : null`. I checked `UsageOptions` — it's a `sealed record` with `required bool Include` and `[JsonPropertyName("include")]`. The shape is exactly right. Off-by-default means `null` on the wire → byte-identical requests to pre-PR behavior. Zero drift. ✅ - **`RoundDetail.ResponseId`** — `string?` (correctly nullable, since a provider *could* omit it). Doc points at `IOpenRouterClient.GetGenerationAsync`. I confirmed `ChatCompletionResponse.Id` is `required string`, so `success.Value.Id` can never produce a wrong null. ✅ - **Both construction sites covered** — there are exactly two `new RoundDetail { ... }` sites in `Agent.cs` (no-tool-calls arm at :223, tool-calls arm at :264) and *both* got `ResponseId = success.Value.Id`. I grepped to make sure there's no third site that was missed. There isn't. ✅ **On the tests — these are *real* tests, not tautologies, and that makes me so happy~** ♪ - `TrackUsage_asks_for_the_usage_block_on_every_request` — enqueues a tool-call + assistant (2 rounds), then asserts `r.Usage?.Include` is true on **both** captured requests via `Assert.All`. Proves the flag hits every round, not just the first. - `Usage_tracking_is_off_by_default` — asserts `Assert.Single(...).Usage` is `null`. Pins the opt-in contract. - `Every_round_carries_its_generation_id` — and here's the *clever* part: this test runs with `TrackUsage` **off** (default). It proves `ResponseId` is populated *independently* of usage tracking — exactly the PR body's "even with tracking off" promise. Then asserts `Distinct().Count() == 2` so you know each round got its own id, not the same one duplicated. That's how you test an invariant. - `Clone_carries_the_tracking_flag` — round-trips the flag through `Clone()`. **Coverage (local, since the coverage bot hasn't posted for `0d4e39f` yet):** - Full suite **143/143 green** (45 Agents + 7 Imaging + 91 Net = 143, matches PR body exactly). - `Agent.cs`: both new `ResponseId` sites exercised (24 hits no-tool arm, 13 hits tool arm), BuildRequest ternary hit 38×. - `AgentOptions.cs`: **100% line / 100% branch**. `RoundDetail.cs`: 88.8% line / 100% branch. - Build: 0 warnings, 0 errors. #### ✅ What I liked~ - **Opt-in by default with an honest latency rationale in the doc comment.** That's senior-grade API design — you didn't just turn it on because you could. - **`ResponseId` decoupled from `TrackUsage`.** Cost is reachable post-hoc via `GetGenerationAsync` even when the caller opted out of per-request usage blocks. Two ways to get cost, neither forces the other. Elegant. - **The off-by-default test for `ResponseId`.** Most authors would've written that test with `TrackUsage = true` and not noticed it muddies the invariant. You left it default-off *on purpose* and it makes the test mean more. - **`Clone()` consistency.** Every existing field is copied member-wise; you slotted `TrackUsage` into the same cluster without disrupting the pattern. Fufu~ nothing to fix, nothing to block, nothing even worth a "consider also." Go merge it~ ♡ --- *Automated review by Jibril · 2026-07-25* *CI/CD: absent for head `0d4e39f` (PR just opened, no coverage bot comment yet) · Local checks: build 0/0, 143/143 tests pass, coverage inspected on changed files*
bjoern merged commit 9544ff2e0f into main 2026-07-25 07:52:40 +02:00
bjoern deleted branch feat/agent-usage-tracking 2026-07-25 07:52:40 +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!5
No description provided.