feat(server): adds billing

This commit is contained in:
2026-07-18 00:29:00 +02:00
parent 02c0b4243c
commit 2f1a3687f4
7 changed files with 1589 additions and 55 deletions
@@ -43,6 +43,20 @@ internal static class ReverseProxyEndpoint
return;
}
var billingCheck = managementStore.CheckBalanceForApiKey(auth.ApiKeyId);
if (!billingCheck.Allowed)
{
context.Response.StatusCode = StatusCodes.Status402PaymentRequired;
await context.Response.WriteAsJsonAsync(new
{
error = "Insufficient balance.",
balance = billingCheck.Balance,
currency = billingCheck.Currency,
threshold = billingCheck.Threshold
}, context.RequestAborted);
return;
}
var groupAccess = ResolveGroupAccess(auth.ApiKeyId, managementStore);
var pathTokenRemoved = TokenAuthentication.TryRemovePathToken(context.Request.Path, settings, managementStore, out var proxyPath);
@@ -76,7 +90,8 @@ internal static class ReverseProxyEndpoint
loggerFactory,
embeddingCache,
managementStore,
groupAccess);
groupAccess,
auth.ApiKeyId);
return;
}
@@ -124,7 +139,8 @@ internal static class ReverseProxyEndpoint
loggerFactory,
embeddingCache,
embeddingRequest,
managementStore);
managementStore,
auth.ApiKeyId);
}
public static async Task HandleClientAsync(
@@ -145,6 +161,20 @@ internal static class ReverseProxyEndpoint
return;
}
var billingCheck = managementStore.CheckBalanceForApiKey(auth.ApiKeyId);
if (!billingCheck.Allowed)
{
context.Response.StatusCode = StatusCodes.Status402PaymentRequired;
await context.Response.WriteAsJsonAsync(new
{
error = "Insufficient balance.",
balance = billingCheck.Balance,
currency = billingCheck.Currency,
threshold = billingCheck.Threshold
}, context.RequestAborted);
return;
}
var groupAccess = ResolveGroupAccess(auth.ApiKeyId, managementStore);
if (!groupAccess.IsClientAllowed(clientId))
{
@@ -165,7 +195,8 @@ internal static class ReverseProxyEndpoint
loggerFactory,
embeddingCache,
managementStore,
groupAccess);
groupAccess,
auth.ApiKeyId);
}
private static async Task ForwardToClientAsync(
@@ -178,7 +209,8 @@ internal static class ReverseProxyEndpoint
ILoggerFactory loggerFactory,
EmbeddingCache embeddingCache,
ManagementStore managementStore,
GroupAccess? groupAccess = null)
GroupAccess? groupAccess = null,
string? apiKeyId = null)
{
var clientAccess = managementStore.GetClientAccess(clientId);
if (clientAccess.IsDisabled)
@@ -220,7 +252,8 @@ internal static class ReverseProxyEndpoint
loggerFactory,
embeddingCache,
embeddingRequest,
managementStore);
managementStore,
apiKeyId);
}
private static bool IsRootPath(PathString path) =>
@@ -464,7 +497,8 @@ internal static class ReverseProxyEndpoint
ILoggerFactory loggerFactory,
EmbeddingCache embeddingCache,
EmbeddingCacheRequest? embeddingRequest,
ManagementStore managementStore)
ManagementStore managementStore,
string? apiKeyId = null)
{
var logger = loggerFactory.CreateLogger("ReverseLlama.Server.ReverseProxy");
var requestId = Guid.NewGuid().ToString("n");
@@ -549,13 +583,29 @@ internal static class ReverseProxyEndpoint
{
connection.RemovePending(requestId);
var completedAt = DateTimeOffset.UtcNow;
var tokenCounts = tokenCounter.CountTokens();
var cost = 0.0;
if (!string.IsNullOrWhiteSpace(apiKeyId) && tokenCounts.TotalTokens > 0)
{
var billing = managementStore.ResolveBillingForApiKey(apiKeyId);
if (billing is not null)
{
cost = managementStore.CalculateCost(billing.GroupId, requestedModel, tokenCounts.TotalTokens);
}
}
managementStore.RecordRequest(new RequestMetric(
connection.ClientId,
requestedModel,
context.Request.Method,
pathAndQuery,
statusCode ?? (context.Response.HasStarted ? context.Response.StatusCode : null),
tokenCounter.CountTokens(),
tokenCounts.PromptTokens,
tokenCounts.CompletionTokens,
tokenCounts.TotalTokens,
apiKeyId,
cost,
startedAt,
completedAt,
completedAt - startedAt));