< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Diagnostics.GenAiTokenMetrics
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Diagnostics/GenAiTokenMetrics.cs
Line coverage
100%
Covered lines: 36
Uncovered lines: 0
Coverable lines: 36
Total lines: 119
Line coverage: 100%
Branch coverage
100%
Covered branches: 12
Total branches: 12
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%11100%
.ctor(...)100%11100%
RecordTokenUsage(...)100%1212100%
Dispose()100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Diagnostics/GenAiTokenMetrics.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Diagnostics.Metrics;
 3
 4namespace NexusLabs.Needlr.AgentFramework.Diagnostics;
 5
 6/// <summary>
 7/// Default <see cref="IGenAiTokenMetrics"/> implementation. Owns a dedicated
 8/// <see cref="Meter"/> whose name is configurable via
 9/// <see cref="AgentFrameworkMetricsOptions.GenAiMeterName"/>.
 10/// </summary>
 11/// <remarks>
 12/// <para>
 13/// The histogram is created with the same name (<c>gen_ai.client.token.usage</c>),
 14/// type (<see cref="Histogram{T}"/> of <see cref="int"/>), unit (<c>{token}</c>),
 15/// description, and explicit bucket boundaries that MEAI's
 16/// <see cref="Microsoft.Extensions.AI.OpenTelemetryChatClient"/> uses (per
 17/// <c>OpenTelemetryConsts.GenAI.Client.TokenUsage</c> in
 18/// <see href="https://github.com/dotnet/extensions/blob/v10.5.0/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryCons
 19/// This shape parity is required for the OpenTelemetry SDK's <c>MetricStreamIdentity</c>
 20/// to consider Needlr's measurements and MEAI's measurements part of the same metric
 21/// stream rather than colliding into duplicate-instrument warnings.
 22/// </para>
 23/// <para>
 24/// Token counts are passed as <see cref="long"/> at the API surface (matching the type
 25/// of <see cref="Microsoft.Extensions.AI.UsageDetails.InputTokenCount"/> et al.) but
 26/// recorded as <see cref="int"/> after a saturating clamp at <see cref="int.MaxValue"/>.
 27/// At single-call granularity values exceeding 2.1 billion tokens are not realistic.
 28/// </para>
 29/// </remarks>
 30[DoNotAutoRegister]
 31internal sealed class GenAiTokenMetrics : IGenAiTokenMetrics, IDisposable
 32{
 33    /// <summary>
 34    /// The exact instrument name MEAI's
 35    /// <see cref="Microsoft.Extensions.AI.OpenTelemetryChatClient"/> uses for the token
 36    /// usage histogram. Sourced verbatim from MEAI's <c>OpenTelemetryConsts.GenAI.Client.TokenUsage.Name</c>.
 37    /// </summary>
 38    internal const string InstrumentName = "gen_ai.client.token.usage";
 39
 40    /// <summary>
 41    /// The exact instrument unit MEAI uses. Sourced verbatim from MEAI's
 42    /// <c>OpenTelemetryConsts.TokensUnit</c>.
 43    /// </summary>
 44    internal const string InstrumentUnit = "{token}";
 45
 46    /// <summary>
 47    /// The exact instrument description MEAI uses. Sourced verbatim from MEAI's
 48    /// <c>OpenTelemetryConsts.GenAI.Client.TokenUsage.Description</c>.
 49    /// </summary>
 50    internal const string InstrumentDescription = "Measures number of input and output tokens used";
 51
 52    /// <summary>
 53    /// The exact explicit bucket boundaries MEAI uses. Sourced verbatim from MEAI's
 54    /// <c>OpenTelemetryConsts.GenAI.Client.TokenUsage.ExplicitBucketBoundaries</c>.
 55    /// </summary>
 156    internal static readonly int[] InstrumentBucketBoundaries =
 157    [
 158        1, 4, 16, 64, 256, 1_024, 4_096, 16_384,
 159        65_536, 262_144, 1_048_576, 4_194_304, 16_777_216, 67_108_864,
 160    ];
 61
 62    private readonly Meter _meter;
 63    private readonly Histogram<int> _tokenUsage;
 64
 465    public GenAiTokenMetrics() : this(new AgentFrameworkMetricsOptions()) { }
 66
 8967    public GenAiTokenMetrics(AgentFrameworkMetricsOptions options)
 68    {
 8969        ArgumentNullException.ThrowIfNull(options);
 70
 8971        _meter = new Meter(options.GenAiMeterName);
 8972        _tokenUsage = _meter.CreateHistogram<int>(
 8973            name: InstrumentName,
 8974            unit: InstrumentUnit,
 8975            description: InstrumentDescription,
 8976            advice: new InstrumentAdvice<int> { HistogramBucketBoundaries = InstrumentBucketBoundaries });
 8977    }
 78
 79    /// <inheritdoc />
 80    public void RecordTokenUsage(string tokenType, long tokenCount, GenAiTokenUsageTags tags)
 81    {
 3882        ArgumentException.ThrowIfNullOrEmpty(tokenType);
 83
 3684        if (tokenCount <= 0)
 285            return;
 86
 3487        var sample = (int)Math.Min(tokenCount, int.MaxValue);
 88
 3489        var tagList = new TagList
 3490        {
 3491            { "gen_ai.token.type", tokenType },
 3492            { "gen_ai.operation.name", string.IsNullOrEmpty(tags.OperationName) ? "chat" : tags.OperationName },
 3493        };
 94
 3495        if (tags.RequestModel is not null)
 496            tagList.Add("gen_ai.request.model", tags.RequestModel);
 97
 98        // gen_ai.provider.name is added UNCONDITIONALLY (even when null) to match MEAI's
 99        // OpenTelemetryChatClient.AddMetricTags exactly. Label-set parity is required for
 100        // the OpenTelemetry SDK MetricStreamIdentity to consider Needlr's measurements and
 101        // MEAI's measurements part of the same stream rather than splitting into two.
 34102        tagList.Add("gen_ai.provider.name", tags.ProviderName);
 103
 34104        if (tags.ServerAddress is not null)
 105        {
 6106            tagList.Add("server.address", tags.ServerAddress);
 6107            if (tags.ServerPort is int port)
 5108                tagList.Add("server.port", port);
 109        }
 110
 34111        if (tags.ResponseModel is not null)
 19112            tagList.Add("gen_ai.response.model", tags.ResponseModel);
 113
 34114        _tokenUsage.Record(sample, tagList);
 34115    }
 116
 117    /// <summary>Disposes the underlying <see cref="Meter"/>.</summary>
 41118    public void Dispose() => _meter.Dispose();
 119}