| File: UsageDetails.cs | |
| Project: ..\..\..\src\Libraries\Microsoft.Extensions.AI.Abstractions\Microsoft.Extensions.AI.Abstractions.csproj (Microsoft.Extensions.AI.Abstractions) |
// 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.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// <summary>Provides usage details about a request/response.</summary> [DebuggerDisplay("{DebuggerDisplay,nq}")] public class UsageDetails { /// <summary>Gets or sets the number of tokens in the input.</summary> public long? InputTokenCount { get; set; } /// <summary>Gets or sets the number of tokens in the output.</summary> public long? OutputTokenCount { get; set; } /// <summary>Gets or sets the total number of tokens used to produce the response.</summary> public long? TotalTokenCount { get; set; } /// <summary> /// Gets or sets the number of input tokens that were read from a cache. /// </summary> /// <remarks> /// Cached input tokens should be counted as part of <see cref="InputTokenCount"/>. /// </remarks> public long? CachedInputTokenCount { get; set; } /// <summary> /// Gets or sets the number of "reasoning" / "thinking" tokens used internally /// by the model. /// </summary> /// <remarks> /// Reasoning tokens should be counted as part of <see cref="OutputTokenCount"/>. /// </remarks> public long? ReasoningTokenCount { get; set; } /// <summary>Gets or sets the number of audio input tokens used.</summary> /// <remarks> /// Audio input tokens should be counted as part of <see cref="InputTokenCount"/>. /// </remarks> [Experimental(DiagnosticIds.Experiments.AIRealTime, UrlFormat = DiagnosticIds.UrlFormat)] [JsonIgnore] public long? InputAudioTokenCount { get => InputAudioTokenCountCore; set => InputAudioTokenCountCore = value; } [JsonInclude] [JsonPropertyName("inputAudioTokenCount")] internal long? InputAudioTokenCountCore { get; set; } /// <summary>Gets or sets the number of text input tokens used.</summary> /// <remarks> /// Text input tokens should be counted as part of <see cref="InputTokenCount"/>. /// </remarks> [Experimental(DiagnosticIds.Experiments.AIRealTime, UrlFormat = DiagnosticIds.UrlFormat)] [JsonIgnore] public long? InputTextTokenCount { get => InputTextTokenCountCore; set => InputTextTokenCountCore = value; } [JsonInclude] [JsonPropertyName("inputTextTokenCount")] internal long? InputTextTokenCountCore { get; set; } /// <summary>Gets or sets the number of audio output tokens used.</summary> /// <remarks> /// Audio output tokens should be counted as part of <see cref="OutputTokenCount"/>. /// </remarks> [Experimental(DiagnosticIds.Experiments.AIRealTime, UrlFormat = DiagnosticIds.UrlFormat)] [JsonIgnore] public long? OutputAudioTokenCount { get => OutputAudioTokenCountCore; set => OutputAudioTokenCountCore = value; } [JsonInclude] [JsonPropertyName("outputAudioTokenCount")] internal long? OutputAudioTokenCountCore { get; set; } /// <summary>Gets or sets the number of text output tokens used.</summary> /// <remarks> /// Text output tokens should be counted as part of <see cref="OutputTokenCount"/>. /// </remarks> [Experimental(DiagnosticIds.Experiments.AIRealTime, UrlFormat = DiagnosticIds.UrlFormat)] [JsonIgnore] public long? OutputTextTokenCount { get => OutputTextTokenCountCore; set => OutputTextTokenCountCore = value; } [JsonInclude] [JsonPropertyName("outputTextTokenCount")] internal long? OutputTextTokenCountCore { get; set; } /// <summary>Gets or sets a dictionary of additional usage counts.</summary> /// <remarks> /// All values set here are assumed to be summable. For example, when middleware makes multiple calls to an underlying /// service, it may sum the counts from multiple results to produce an overall <see cref="UsageDetails"/>. /// </remarks> public AdditionalPropertiesDictionary<long>? AdditionalCounts { get; set; } /// <summary>Adds usage data from another <see cref="UsageDetails"/> into this instance.</summary> /// <param name="usage">The source <see cref="UsageDetails"/> with which to augment this instance.</param> /// <exception cref="ArgumentNullException"><paramref name="usage"/> is <see langword="null"/>.</exception> public void Add(UsageDetails usage) { _ = Throw.IfNull(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); InputAudioTokenCount = NullableSum(InputAudioTokenCount, usage.InputAudioTokenCount); InputTextTokenCount = NullableSum(InputTextTokenCount, usage.InputTextTokenCount); OutputAudioTokenCount = NullableSum(OutputAudioTokenCount, usage.OutputAudioTokenCount); OutputTextTokenCount = NullableSum(OutputTextTokenCount, usage.OutputTextTokenCount); if (usage.AdditionalCounts is { } countsToAdd) { if (AdditionalCounts is null) { AdditionalCounts = new(countsToAdd); } else { foreach (var kvp in countsToAdd) { AdditionalCounts[kvp.Key] = AdditionalCounts.TryGetValue(kvp.Key, out var existingValue) ? kvp.Value + existingValue : kvp.Value; } } } } /// <summary>Gets a string representing this instance to display in the debugger.</summary> [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal string DebuggerDisplay { get { List<string> parts = []; if (InputTokenCount is { } input) { parts.Add($"{nameof(InputTokenCount)} = {input}"); } if (OutputTokenCount is { } output) { parts.Add($"{nameof(OutputTokenCount)} = {output}"); } if (TotalTokenCount is { } total) { parts.Add($"{nameof(TotalTokenCount)} = {total}"); } if (CachedInputTokenCount is { } cached) { parts.Add($"{nameof(CachedInputTokenCount)} = {cached}"); } if (ReasoningTokenCount is { } reasoning) { parts.Add($"{nameof(ReasoningTokenCount)} = {reasoning}"); } if (InputAudioTokenCount is { } inputAudio) { parts.Add($"{nameof(InputAudioTokenCount)} = {inputAudio}"); } if (InputTextTokenCount is { } inputText) { parts.Add($"{nameof(InputTextTokenCount)} = {inputText}"); } if (OutputAudioTokenCount is { } outputAudio) { parts.Add($"{nameof(OutputAudioTokenCount)} = {outputAudio}"); } if (OutputTextTokenCount is { } outputText) { parts.Add($"{nameof(OutputTextTokenCount)} = {outputText}"); } if (AdditionalCounts is { } additionalCounts) { foreach (var entry in additionalCounts) { parts.Add($"{entry.Key} = {entry.Value}"); } } return string.Join(", ", parts); } } private static long? NullableSum(long? a, long? b) => (a.HasValue || b.HasValue) ? (a ?? 0) + (b ?? 0) : null; }