diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 32faa8a1f4d..b0b47c4ef6a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -2236,6 +2236,14 @@ { "Member": "long? Microsoft.Extensions.AI.UsageDetails.TotalTokenCount { get; set; }", "Stage": "Stable" + }, + { + "Member": "long? Microsoft.Extensions.AI.UsageDetails.CachedInputTokenCount { get; set; }", + "Stage": "Stable" + }, + { + "Member": "long? Microsoft.Extensions.AI.UsageDetails.ReasoningTokenCount { get; set; }", + "Stage": "Stable" } ] } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/UsageDetails.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/UsageDetails.cs index b3c62cb67e0..b3edbad5e99 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/UsageDetails.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/UsageDetails.cs @@ -21,6 +21,23 @@ public class UsageDetails /// Gets or sets the total number of tokens used to produce the response. public long? TotalTokenCount { get; set; } + /// + /// Gets or sets the number of input tokens that were read from a cache. + /// + /// + /// Cached input tokens should be counted as part of . + /// + public long? CachedInputTokenCount { get; set; } + + /// + /// Gets or sets the number of "reasoning" / "thinking" tokens used internally + /// by the model. + /// + /// + /// Reasoning tokens should be counted as part of . + /// + public long? ReasoningTokenCount { get; set; } + /// Gets or sets a dictionary of additional usage counts. /// /// All values set here are assumed to be summable. For example, when middleware makes multiple calls to an underlying @@ -38,6 +55,8 @@ public void Add(UsageDetails usage) InputTokenCount = NullableSum(InputTokenCount, usage.InputTokenCount); OutputTokenCount = NullableSum(OutputTokenCount, usage.OutputTokenCount); TotalTokenCount = NullableSum(TotalTokenCount, usage.TotalTokenCount); + CachedInputTokenCount = NullableSum(CachedInputTokenCount, usage.CachedInputTokenCount); + ReasoningTokenCount = NullableSum(ReasoningTokenCount, usage.ReasoningTokenCount); if (usage.AdditionalCounts is { } countsToAdd) { @@ -80,6 +99,16 @@ internal string DebuggerDisplay parts.Add($"{nameof(TotalTokenCount)} = {total}"); } + if (CachedInputTokenCount is { } cached) + { + parts.Add($"{nameof(CachedInputTokenCount)} = {cached}"); + } + + if (ReasoningTokenCount is { } reasoning) + { + parts.Add($"{nameof(ReasoningTokenCount)} = {reasoning}"); + } + if (AdditionalCounts is { } additionalCounts) { foreach (var entry in additionalCounts) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 70ef6674bd4..a7ca8c08d95 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -644,6 +644,8 @@ private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage) InputTokenCount = tokenUsage.InputTokenCount, OutputTokenCount = tokenUsage.OutputTokenCount, TotalTokenCount = tokenUsage.TotalTokenCount, + CachedInputTokenCount = tokenUsage.InputTokenDetails?.CachedTokenCount, + ReasoningTokenCount = tokenUsage.OutputTokenDetails?.ReasoningTokenCount, AdditionalCounts = [], }; @@ -653,13 +655,11 @@ private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage) { const string InputDetails = nameof(ChatTokenUsage.InputTokenDetails); counts.Add($"{InputDetails}.{nameof(ChatInputTokenUsageDetails.AudioTokenCount)}", inputDetails.AudioTokenCount); - counts.Add($"{InputDetails}.{nameof(ChatInputTokenUsageDetails.CachedTokenCount)}", inputDetails.CachedTokenCount); } if (tokenUsage.OutputTokenDetails is ChatOutputTokenUsageDetails outputDetails) { const string OutputDetails = nameof(ChatTokenUsage.OutputTokenDetails); - counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount); counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.AudioTokenCount)}", outputDetails.AudioTokenCount); counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.AcceptedPredictionTokenCount)}", outputDetails.AcceptedPredictionTokenCount); counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.RejectedPredictionTokenCount)}", outputDetails.RejectedPredictionTokenCount); diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index eb39754d5fd..1c172db283a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -1143,19 +1143,9 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera InputTokenCount = usage.InputTokenCount, OutputTokenCount = usage.OutputTokenCount, TotalTokenCount = usage.TotalTokenCount, + CachedInputTokenCount = usage.InputTokenDetails?.CachedTokenCount, + ReasoningTokenCount = usage.OutputTokenDetails?.ReasoningTokenCount, }; - - if (usage.InputTokenDetails is { } inputDetails) - { - ud.AdditionalCounts ??= []; - ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.CachedTokenCount)}", inputDetails.CachedTokenCount); - } - - if (usage.OutputTokenDetails is { } outputDetails) - { - ud.AdditionalCounts ??= []; - ud.AdditionalCounts.Add($"{nameof(usage.OutputTokenDetails)}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount); - } } return ud; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs index ed268176c5d..d3bf0889821 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs @@ -66,7 +66,9 @@ public void Serialization_Roundtrips() { InputTokenCount = 10, OutputTokenCount = 20, - TotalTokenCount = 30 + TotalTokenCount = 30, + CachedInputTokenCount = 5, + ReasoningTokenCount = 8 }); var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); @@ -77,5 +79,7 @@ public void Serialization_Roundtrips() Assert.Equal(content.Details.InputTokenCount, deserializedContent.Details.InputTokenCount); Assert.Equal(content.Details.OutputTokenCount, deserializedContent.Details.OutputTokenCount); Assert.Equal(content.Details.TotalTokenCount, deserializedContent.Details.TotalTokenCount); + Assert.Equal(content.Details.CachedInputTokenCount, deserializedContent.Details.CachedInputTokenCount); + Assert.Equal(content.Details.ReasoningTokenCount, deserializedContent.Details.ReasoningTokenCount); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/UsageDetailsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/UsageDetailsTests.cs new file mode 100644 index 00000000000..d7fcd2545f0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/UsageDetailsTests.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class UsageDetailsTests +{ + [Fact] + public void Constructor_PropsDefault() + { + UsageDetails details = new(); + Assert.Null(details.InputTokenCount); + Assert.Null(details.OutputTokenCount); + Assert.Null(details.TotalTokenCount); + Assert.Null(details.CachedInputTokenCount); + Assert.Null(details.ReasoningTokenCount); + Assert.Null(details.AdditionalCounts); + } + + [Fact] + public void Properties_Roundtrip() + { + UsageDetails details = new() + { + InputTokenCount = 10, + OutputTokenCount = 20, + TotalTokenCount = 30, + CachedInputTokenCount = 5, + ReasoningTokenCount = 8, + AdditionalCounts = new() { ["custom"] = 100 } + }; + + Assert.Equal(10, details.InputTokenCount); + Assert.Equal(20, details.OutputTokenCount); + Assert.Equal(30, details.TotalTokenCount); + Assert.Equal(5, details.CachedInputTokenCount); + Assert.Equal(8, details.ReasoningTokenCount); + Assert.NotNull(details.AdditionalCounts); + Assert.Equal(100, details.AdditionalCounts["custom"]); + } + + [Fact] + public void Add_NullUsage_Throws() + { + UsageDetails details = new(); + Assert.Throws("usage", () => details.Add(null!)); + } + + [Fact] + public void Add_SumsAllProperties() + { + UsageDetails details1 = new() + { + InputTokenCount = 10, + OutputTokenCount = 20, + TotalTokenCount = 30, + CachedInputTokenCount = 5, + ReasoningTokenCount = 8, + }; + + UsageDetails details2 = new() + { + InputTokenCount = 15, + OutputTokenCount = 25, + TotalTokenCount = 40, + CachedInputTokenCount = 7, + ReasoningTokenCount = 12, + }; + + details1.Add(details2); + + Assert.Equal(25, details1.InputTokenCount); + Assert.Equal(45, details1.OutputTokenCount); + Assert.Equal(70, details1.TotalTokenCount); + Assert.Equal(12, details1.CachedInputTokenCount); + Assert.Equal(20, details1.ReasoningTokenCount); + } + + [Fact] + public void Add_WithNullValues_HandlesCorrectly() + { + UsageDetails details1 = new() + { + InputTokenCount = 10, + CachedInputTokenCount = 5, + }; + + UsageDetails details2 = new() + { + OutputTokenCount = 25, + ReasoningTokenCount = 12, + }; + + details1.Add(details2); + + Assert.Equal(10, details1.InputTokenCount); + Assert.Equal(25, details1.OutputTokenCount); + Assert.Null(details1.TotalTokenCount); + Assert.Equal(5, details1.CachedInputTokenCount); + Assert.Equal(12, details1.ReasoningTokenCount); + } + + [Fact] + public void Add_FromNullToValue_SetsValue() + { + UsageDetails details1 = new(); + + UsageDetails details2 = new() + { + CachedInputTokenCount = 5, + ReasoningTokenCount = 10, + }; + + details1.Add(details2); + + Assert.Equal(5, details1.CachedInputTokenCount); + Assert.Equal(10, details1.ReasoningTokenCount); + } + + [Fact] + public void Add_AdditionalCounts_MergesCorrectly() + { + UsageDetails details1 = new() + { + AdditionalCounts = new() { ["key1"] = 10, ["key2"] = 20 } + }; + + UsageDetails details2 = new() + { + AdditionalCounts = new() { ["key2"] = 30, ["key3"] = 40 } + }; + + details1.Add(details2); + + Assert.NotNull(details1.AdditionalCounts); + Assert.Equal(10, details1.AdditionalCounts["key1"]); + Assert.Equal(50, details1.AdditionalCounts["key2"]); + Assert.Equal(40, details1.AdditionalCounts["key3"]); + } + + [Fact] + public void Serialization_Roundtrips() + { + UsageDetails details = new() + { + InputTokenCount = 10, + OutputTokenCount = 20, + TotalTokenCount = 30, + CachedInputTokenCount = 5, + ReasoningTokenCount = 8, + AdditionalCounts = new() { ["custom"] = 100 } + }; + + string json = JsonSerializer.Serialize(details, AIJsonUtilities.DefaultOptions); + UsageDetails? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(details.InputTokenCount, deserialized.InputTokenCount); + Assert.Equal(details.OutputTokenCount, deserialized.OutputTokenCount); + Assert.Equal(details.TotalTokenCount, deserialized.TotalTokenCount); + Assert.Equal(details.CachedInputTokenCount, deserialized.CachedInputTokenCount); + Assert.Equal(details.ReasoningTokenCount, deserialized.ReasoningTokenCount); + Assert.NotNull(deserialized.AdditionalCounts); + Assert.Equal(100, deserialized.AdditionalCounts["custom"]); + } + + [Fact] + public void Serialization_WithNullProperties_Roundtrips() + { + UsageDetails details = new() + { + InputTokenCount = 10, + OutputTokenCount = 20, + }; + + string json = JsonSerializer.Serialize(details, AIJsonUtilities.DefaultOptions); + UsageDetails? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(10, deserialized.InputTokenCount); + Assert.Equal(20, deserialized.OutputTokenCount); + Assert.Null(deserialized.TotalTokenCount); + Assert.Null(deserialized.CachedInputTokenCount); + Assert.Null(deserialized.ReasoningTokenCount); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 5e4932c6736..458256523e4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -171,11 +171,11 @@ public async Task BasicRequestResponse_NonStreaming() Assert.Equal(8, response.Usage.InputTokenCount); Assert.Equal(9, response.Usage.OutputTokenCount); Assert.Equal(17, response.Usage.TotalTokenCount); + Assert.Equal(13, response.Usage.CachedInputTokenCount); + Assert.Equal(90, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 13 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -258,12 +258,12 @@ public async Task BasicRequestResponse_Streaming() Assert.Equal(8, usage.Details.InputTokenCount); Assert.Equal(9, usage.Details.OutputTokenCount); Assert.Equal(17, usage.Details.TotalTokenCount); + Assert.Equal(5, usage.Details.CachedInputTokenCount); + Assert.Equal(90, usage.Details.ReasoningTokenCount); Assert.Equal(new AdditionalPropertiesDictionary { { "InputTokenDetails.AudioTokenCount", 123 }, - { "InputTokenDetails.CachedTokenCount", 5 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 456 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -845,11 +845,11 @@ public async Task MultipleMessages_NonStreaming() Assert.Equal(42, response.Usage.InputTokenCount); Assert.Equal(15, response.Usage.OutputTokenCount); Assert.Equal(57, response.Usage.TotalTokenCount); + Assert.Equal(13, response.Usage.CachedInputTokenCount); + Assert.Equal(90, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 123 }, - { "InputTokenDetails.CachedTokenCount", 13 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 456 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -942,11 +942,11 @@ public async Task MultiPartSystemMessage_NonStreaming() Assert.Equal(42, response.Usage.InputTokenCount); Assert.Equal(15, response.Usage.OutputTokenCount); Assert.Equal(57, response.Usage.TotalTokenCount); + Assert.Equal(13, response.Usage.CachedInputTokenCount); + Assert.Equal(90, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 13 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -1040,11 +1040,11 @@ public async Task EmptyAssistantMessage_NonStreaming() Assert.Equal(42, response.Usage.InputTokenCount); Assert.Equal(15, response.Usage.OutputTokenCount); Assert.Equal(57, response.Usage.TotalTokenCount); + Assert.Equal(13, response.Usage.CachedInputTokenCount); + Assert.Equal(90, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 13 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -1151,12 +1151,12 @@ public async Task FunctionCallContent_NonStreaming() Assert.Equal(61, response.Usage.InputTokenCount); Assert.Equal(16, response.Usage.OutputTokenCount); Assert.Equal(77, response.Usage.TotalTokenCount); + Assert.Equal(13, response.Usage.CachedInputTokenCount); + Assert.Equal(90, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 13 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -1235,12 +1235,12 @@ public async Task UnavailableBuiltInFunctionCall_NonStreaming() Assert.Equal(61, response.Usage.InputTokenCount); Assert.Equal(16, response.Usage.OutputTokenCount); Assert.Equal(77, response.Usage.TotalTokenCount); + Assert.Equal(13, response.Usage.CachedInputTokenCount); + Assert.Equal(90, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 13 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -1351,12 +1351,12 @@ public async Task FunctionCallContent_Streaming() Assert.Equal(61, usage.Details.InputTokenCount); Assert.Equal(16, usage.Details.OutputTokenCount); Assert.Equal(77, usage.Details.TotalTokenCount); + Assert.Equal(0, usage.Details.CachedInputTokenCount); + Assert.Equal(90, usage.Details.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 0 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -1493,11 +1493,11 @@ public async Task AssistantMessageWithBothToolsAndContent_NonStreaming() Assert.Equal(42, response.Usage.InputTokenCount); Assert.Equal(15, response.Usage.OutputTokenCount); Assert.Equal(57, response.Usage.TotalTokenCount); + Assert.Equal(20, response.Usage.CachedInputTokenCount); + Assert.Equal(90, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 20 }, - { "OutputTokenDetails.ReasoningTokenCount", 90 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, @@ -1608,11 +1608,11 @@ private static async Task DataContentMessage_Image_AdditionalPropertyDetail_NonS Assert.Equal(8513, response.Usage.InputTokenCount); Assert.Equal(56, response.Usage.OutputTokenCount); Assert.Equal(8569, response.Usage.TotalTokenCount); + Assert.Equal(0, response.Usage.CachedInputTokenCount); + Assert.Equal(0, response.Usage.ReasoningTokenCount); Assert.Equal(new Dictionary { { "InputTokenDetails.AudioTokenCount", 0 }, - { "InputTokenDetails.CachedTokenCount", 0 }, - { "OutputTokenDetails.ReasoningTokenCount", 0 }, { "OutputTokenDetails.AudioTokenCount", 0 }, { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 94d767f67d4..34526b683bd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -4584,12 +4584,12 @@ public async Task ResponseWithUsageDetails_ParsesTokenCounts() var response = await client.GetResponseAsync("test"); Assert.NotNull(response.Usage); + Assert.Null(response.Usage.AdditionalCounts); Assert.Equal(50, response.Usage.InputTokenCount); Assert.Equal(25, response.Usage.OutputTokenCount); Assert.Equal(75, response.Usage.TotalTokenCount); - Assert.NotNull(response.Usage.AdditionalCounts); - Assert.Equal(10, response.Usage.AdditionalCounts["InputTokenDetails.CachedTokenCount"]); - Assert.Equal(5, response.Usage.AdditionalCounts["OutputTokenDetails.ReasoningTokenCount"]); + Assert.Equal(10, response.Usage.CachedInputTokenCount); + Assert.Equal(5, response.Usage.ReasoningTokenCount); } [Fact]