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
<PackageReference Include="AzureAICommunity.Agent.Middleware.TokenUsageMiddleware" Version="1.0.0" />
<PackageVersion Include="AzureAICommunity.Agent.Middleware.TokenUsageMiddleware" Version="1.0.0" />
<PackageReference Include="AzureAICommunity.Agent.Middleware.TokenUsageMiddleware" />
paket add AzureAICommunity.Agent.Middleware.TokenUsageMiddleware --version 1.0.0
#r "nuget: AzureAICommunity.Agent.Middleware.TokenUsageMiddleware, 1.0.0"
#:package AzureAICommunity.Agent.Middleware.TokenUsageMiddleware@1.0.0
#addin nuget:?package=AzureAICommunity.Agent.Middleware.TokenUsageMiddleware&version=1.0.0
#tool nuget:?package=AzureAICommunity.Agent.Middleware.TokenUsageMiddleware&version=1.0.0
<div align="center">
๐ช AzureAICommunity - Agent - Token Usage Middleware
Enforce per-user token quotas and capture detailed usage metrics across every AI agent completion call.
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.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
๐ License
MIT
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.AI (>= 10.4.1)
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 |