AzureAICommunity.Agent.Middleware.TokenUsageMiddleware 1.0.0

dotnet add package AzureAICommunity.Agent.Middleware.TokenUsageMiddleware --version 1.0.0
                    
NuGet\Install-Package AzureAICommunity.Agent.Middleware.TokenUsageMiddleware -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AzureAICommunity.Agent.Middleware.TokenUsageMiddleware" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AzureAICommunity.Agent.Middleware.TokenUsageMiddleware" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="AzureAICommunity.Agent.Middleware.TokenUsageMiddleware" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AzureAICommunity.Agent.Middleware.TokenUsageMiddleware --version 1.0.0
                    
#r "nuget: AzureAICommunity.Agent.Middleware.TokenUsageMiddleware, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AzureAICommunity.Agent.Middleware.TokenUsageMiddleware@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AzureAICommunity.Agent.Middleware.TokenUsageMiddleware&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=AzureAICommunity.Agent.Middleware.TokenUsageMiddleware&version=1.0.0
                    
Install as a Cake Tool

<div align="center">

๐Ÿช™ AzureAICommunity - Agent - Token Usage Middleware

Enforce per-user token quotas and capture detailed usage metrics across every AI agent completion call.

NuGet Version NuGet Downloads License .NET

Track, throttle, and bill token consumption per user โ€” with zero friction.

Getting Started ยท Quota Stores ยท Period Keys ยท Callbacks ยท Contributing

</div>


Overview

AzureAICommunity.Agent.Middleware.TokenUsageMiddleware is a plug-and-play quota and metering layer for AI agent pipelines built on Microsoft.Extensions.AI. Before every request it checks a user's accumulated token count against a configurable limit and throws a QuotaExceededException if exhausted. After every successful completion (streaming or non-streaming) it persists the token delta to an IQuotaStore and fires an optional onUsage callback with a TokenUsageRecord.


โœจ Features

Feature
๐Ÿšฆ Pre-call quota enforcement โ€” blocks requests before the LLM is ever called
๐Ÿ“Š Post-call usage recording โ€” emits a TokenUsageRecord after every completion
๐Ÿ”€ Streaming support โ€” works with both GetResponseAsync and GetStreamingResponseAsync
๐Ÿ—“๏ธ Flexible quota periods โ€” built-in Day, Week, and Month helpers, or any custom delegate
๐Ÿ—„๏ธ Pluggable storage โ€” InMemoryQuotaStore for development; bring your own Redis/SQL backend
๐Ÿ”” Callbacks โ€” onUsage and onQuotaExceeded hooks for billing, logging, and alerting
๐Ÿ”Œ MEA integration โ€” drops directly into any Microsoft.Extensions.AI pipeline via AsBuilder().Use(...)

๐Ÿ“ฆ Installation

dotnet add package AzureAICommunity.Agent.Middleware.TokenUsageMiddleware

๐Ÿš€ Quick Start

using AzureAICommunity.Agent.Middleware.TokenUsageMiddleware;
using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient ollamaClient = new OllamaApiClient("http://localhost:11434/", "llama3.2");

var quotaStore = new InMemoryQuotaStore();

IChatClient client = ollamaClient
    .AsBuilder()
    .Use(inner => new TokenUsageMiddleware(
        inner,
        quotaStore: quotaStore,
        quotaTokens: 500,
        onUsage: async (record, ct) =>
        {
            Console.WriteLine($"[{record.UserId}] used {record.TotalTokens} tokens " +
                              $"({record.UsedTokensAfterCall}/{record.QuotaTokens} total)");
            await Task.CompletedTask;
        }))
    .Build();

var options = new ChatOptions
{
    AdditionalProperties = new() { ["user_id"] = "Vinoth" }
};

var response = await client.GetResponseAsync("What is the capital of France?", options);
Console.WriteLine(response.Message.Text);

๐Ÿ—„๏ธ Quota Stores

The middleware delegates all persistence to an IQuotaStore. Two implementations are available out of the box:

InMemoryQuotaStore

Fast, zero-dependency store backed by an in-process dictionary. Suitable for development, testing, and single-process apps where quota data does not need to survive restarts.

var quotaStore = new InMemoryQuotaStore();

Custom / Persistent Store

For production or multi-process deployments, implement IQuotaStore with any backend (Redis, SQL, Azure Table Storage, etc.):

public sealed class RedisQuotaStore : IQuotaStore
{
    public long GetUsage(string userId, string periodKey) { /* ... */ }
    public void AddUsage(string userId, string periodKey, long tokens) { /* ... */ }
}

๐Ÿ—“๏ธ Period Keys

The quota is scoped to a period key โ€” a string that resets the counter. Use the built-in PeriodKeys helpers or supply any custom delegate:

Helper Example output Usage
PeriodKeys.Month "2026-04" Monthly quota (default)
PeriodKeys.Week "2026-W15" Weekly quota
PeriodKeys.Day "2026-04-14" Daily quota
// Weekly quota
var client = ollamaClient.AsBuilder()
    .Use(inner => new TokenUsageMiddleware(
        inner,
        quotaStore: quotaStore,
        quotaTokens: 1000,
        periodKeyFn: PeriodKeys.Week))
    .Build();

// Custom period (e.g. hourly)
var client = ollamaClient.AsBuilder()
    .Use(inner => new TokenUsageMiddleware(
        inner,
        quotaStore: quotaStore,
        quotaTokens: 200,
        periodKeyFn: () => DateTimeOffset.UtcNow.ToString("yyyy-MM-dd-HH")))
    .Build();

๐Ÿ”” Callbacks

onUsage โ€” Post-completion metrics

Fires after every successful completion with an immutable TokenUsageRecord snapshot:

.Use(inner => new TokenUsageMiddleware(
    inner,
    quotaStore: quotaStore,
    quotaTokens: 500,
    onUsage: async (record, ct) =>
    {
        // Forward to a billing system, database, or telemetry sink
        Console.WriteLine(
            $"User={record.UserId} Period={record.PeriodKey} Model={record.Model} " +
            $"Input={record.InputTokens} Output={record.OutputTokens} Total={record.TotalTokens} " +
            $"Used={record.UsedTokensAfterCall}/{record.QuotaTokens} Streaming={record.IsStreaming}");
        await Task.CompletedTask;
    }))

onQuotaExceeded โ€” Pre-exception hook

Fires when the quota check fails, before QuotaExceededException is thrown, giving you a chance to log or alert:

.Use(inner => new TokenUsageMiddleware(
    inner,
    quotaStore: quotaStore,
    quotaTokens: 500,
    onQuotaExceeded: async (info, ct) =>
    {
        Console.WriteLine(
            $"[QUOTA] User={info.UserId} has used {info.UsedTokens}/{info.QuotaTokens} tokens " +
            $"in period {info.PeriodKey}. Request blocked.");
        await Task.CompletedTask;
    }))

๐Ÿง‘โ€๐Ÿ’ป Custom User Identification

By default the middleware reads the "user_id" key from ChatOptions.AdditionalProperties, falling back to "anonymous". Supply a custom userIdGetter delegate to integrate with your own identity system:

.Use(inner => new TokenUsageMiddleware(
    inner,
    quotaStore: quotaStore,
    quotaTokens: 500,
    userIdGetter: (messages, options) =>
    {
        // e.g. extract from a JWT claim stored in AdditionalProperties
        return options?.AdditionalProperties?.TryGetValue("sub", out var sub) == true
            ? sub?.ToString() ?? "anonymous"
            : "anonymous";
    }))

โš™๏ธ How It Works

1. Intercept     โ†’  middleware captures the incoming request
2. Quota check   โ†’  GetUsage(userId, periodKey) compared against quotaTokens
3. Block         โ†’  if used >= quota: fire onQuotaExceeded, throw QuotaExceededException
4. Delegate      โ†’  forward to the inner IChatClient (LLM is called)
5. Record usage  โ†’  AddUsage(userId, periodKey, totalTokens)
6. Emit record   โ†’  fire onUsage callback with a TokenUsageRecord snapshot
7. Return        โ†’  response (or stream) is returned to the caller

๐Ÿ“„ Constructor Reference

public TokenUsageMiddleware(
    IChatClient inner,                                                  // Inner chat client to delegate to
    IQuotaStore quotaStore,                                             // Per-user token storage backend
    long quotaTokens,                                                   // Maximum tokens per user per period (> 0)
    Func<TokenUsageRecord, CancellationToken, Task>? onUsage = null,   // Post-completion callback
    Func<QuotaExceededInfo, CancellationToken, Task>? onQuotaExceeded = null, // Pre-exception callback
    Func<IEnumerable<ChatMessage>, ChatOptions?, string>? userIdGetter = null, // User ID extractor
    Func<string>? periodKeyFn = null                                    // Period key factory (default: PeriodKeys.Month)
)

๐Ÿ“‹ Type Reference

Type Description
TokenUsageMiddleware The main DelegatingChatClient middleware
IQuotaStore Interface for per-user, per-period token storage
InMemoryQuotaStore In-process dictionary-backed quota store
TokenUsageRecord Immutable usage snapshot passed to onUsage
QuotaExceededException Thrown when a user's quota is exhausted
QuotaExceededInfo Context passed to onQuotaExceeded before the exception is thrown
PeriodKeys Static helpers: Month(), Week(), Day()

๐Ÿค Contributing

Contributions are welcome! Please open an issue to discuss what you'd like to change before submitting a pull request.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -m 'Add my feature')
  4. Push to the branch (git push origin feature/my-feature)
  5. Open a Pull Request

๐Ÿ“„ License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 180 4/15/2026