Files
Ngino/src/Ngino.Server/ManagementStore.cs
T
2026-07-29 15:22:28 +02:00

2580 lines
90 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite;
namespace Ngino.Server;
internal sealed class ManagementStore
{
private static readonly TimeSpan UserKeyLastUsedWriteInterval = TimeSpan.FromMinutes(1);
private readonly Dictionary<string, KeyState> _userKeysByHash = new(StringComparer.Ordinal);
private readonly Dictionary<string, KeyState> _clientKeysByHash = new(StringComparer.Ordinal);
private readonly string _connectionString = "";
private readonly string _databasePath = "";
private readonly object _lock = new();
private readonly ILogger<ManagementStore> _logger;
private bool _isAvailable;
private string? _lastError;
public ManagementStore(ServerSettings settings, ILogger<ManagementStore> logger)
{
_logger = logger;
try
{
_databasePath = ResolveDatabasePath(settings.ManagementDatabasePath);
_connectionString = new SqliteConnectionStringBuilder
{
DataSource = _databasePath,
Mode = SqliteOpenMode.ReadWriteCreate,
Pooling = true
}.ToString();
Initialize();
_isAvailable = true;
}
catch (Exception exception)
{
_lastError = exception.Message;
_logger.LogError(
exception,
"Management database is disabled because SQLite could not be initialized at {DatabasePath}.",
string.IsNullOrWhiteSpace(_databasePath) ? settings.ManagementDatabasePath : _databasePath);
}
}
public string DatabasePath => _databasePath;
public bool IsAvailable => _isAvailable;
public string? LastError => _lastError;
public bool HasUserKeys
{
get
{
if (!_isAvailable)
{
return false;
}
lock (_lock)
{
return _userKeysByHash.Count > 0;
}
}
}
public bool HasClientKeys
{
get
{
if (!_isAvailable)
{
return false;
}
lock (_lock)
{
return _clientKeysByHash.Count > 0;
}
}
}
public bool IsUserKeyValid(string userKey, bool updateLastUsed)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(userKey))
{
return false;
}
var hash = HashKey(userKey);
var now = DateTimeOffset.UtcNow;
lock (_lock)
{
if (!_userKeysByHash.TryGetValue(hash, out var key))
{
return false;
}
if (!updateLastUsed
|| key.LastUsedUtc is not null
&& now - key.LastUsedUtc.Value < UserKeyLastUsedWriteInterval)
{
return true;
}
key.LastUsedUtc = now;
try
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "UPDATE user_keys SET last_used_at_utc = $last_used_at_utc WHERE id = $id";
command.Parameters.AddWithValue("$last_used_at_utc", now.ToString("O"));
command.Parameters.AddWithValue("$id", key.Id);
command.ExecuteNonQuery();
}
catch (Exception exception) when (exception is SqliteException or IOException or UnauthorizedAccessException)
{
_logger.LogWarning(exception, "Failed to update user key last-used timestamp.");
}
return true;
}
}
public IReadOnlyList<UserKeyInfo> ListUserKeys()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
return _userKeysByHash.Values
.OrderBy(key => key.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(key => key.CreatedAtUtc)
.Select(key => new UserKeyInfo(
key.Id,
key.Name,
key.KeyPrefix,
key.CreatedAtUtc,
key.LastUsedUtc))
.ToList();
}
}
public CreatedUserKey CreateUserKey(string? name)
{
EnsureAvailable();
var key = GenerateKey();
var now = DateTimeOffset.UtcNow;
var state = new KeyState
{
Id = Guid.NewGuid().ToString("n"),
Name = string.IsNullOrWhiteSpace(name) ? "User key" : name.Trim(),
KeyHash = HashKey(key),
KeyPrefix = GetKeyPrefix(key),
CreatedAtUtc = now
};
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO user_keys (id, name, key_hash, key_prefix, created_at_utc)
VALUES ($id, $name, $key_hash, $key_prefix, $created_at_utc)
""";
command.Parameters.AddWithValue("$id", state.Id);
command.Parameters.AddWithValue("$name", state.Name);
command.Parameters.AddWithValue("$key_hash", state.KeyHash);
command.Parameters.AddWithValue("$key_prefix", state.KeyPrefix);
command.Parameters.AddWithValue("$created_at_utc", state.CreatedAtUtc.ToString("O"));
command.ExecuteNonQuery();
_userKeysByHash[state.KeyHash] = state;
}
return new CreatedUserKey(
state.Id,
state.Name,
state.KeyPrefix,
state.CreatedAtUtc,
key);
}
public bool DeleteUserKey(string id)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(id))
{
return false;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "DELETE FROM user_keys WHERE id = $id";
command.Parameters.AddWithValue("$id", id);
var deleted = command.ExecuteNonQuery() > 0;
if (deleted)
{
foreach (var pair in _userKeysByHash.Where(pair => pair.Value.Id == id).ToArray())
{
_userKeysByHash.Remove(pair.Key);
}
}
return deleted;
}
}
public bool IsClientKeyValid(string clientKey, bool updateLastUsed)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(clientKey))
{
return false;
}
var hash = HashKey(clientKey);
var now = DateTimeOffset.UtcNow;
lock (_lock)
{
if (!_clientKeysByHash.TryGetValue(hash, out var key))
{
return false;
}
if (!updateLastUsed
|| key.LastUsedUtc is not null
&& now - key.LastUsedUtc.Value < UserKeyLastUsedWriteInterval)
{
return true;
}
key.LastUsedUtc = now;
try
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "UPDATE client_keys SET last_used_at_utc = $last_used_at_utc WHERE id = $id";
command.Parameters.AddWithValue("$last_used_at_utc", now.ToString("O"));
command.Parameters.AddWithValue("$id", key.Id);
command.ExecuteNonQuery();
}
catch (Exception exception) when (exception is SqliteException or IOException or UnauthorizedAccessException)
{
_logger.LogWarning(exception, "Failed to update client key last-used timestamp.");
}
return true;
}
}
public IReadOnlyList<UserKeyInfo> ListClientKeys()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
return _clientKeysByHash.Values
.OrderBy(key => key.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(key => key.CreatedAtUtc)
.Select(key => new UserKeyInfo(
key.Id,
key.Name,
key.KeyPrefix,
key.CreatedAtUtc,
key.LastUsedUtc))
.ToList();
}
}
public CreatedUserKey CreateClientKey(string? name)
{
EnsureAvailable();
var key = GenerateKey();
var now = DateTimeOffset.UtcNow;
var state = new KeyState
{
Id = Guid.NewGuid().ToString("n"),
Name = string.IsNullOrWhiteSpace(name) ? "Client key" : name.Trim(),
KeyHash = HashKey(key),
KeyPrefix = GetKeyPrefix(key),
CreatedAtUtc = now
};
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO client_keys (id, name, key_hash, key_prefix, created_at_utc)
VALUES ($id, $name, $key_hash, $key_prefix, $created_at_utc)
""";
command.Parameters.AddWithValue("$id", state.Id);
command.Parameters.AddWithValue("$name", state.Name);
command.Parameters.AddWithValue("$key_hash", state.KeyHash);
command.Parameters.AddWithValue("$key_prefix", state.KeyPrefix);
command.Parameters.AddWithValue("$created_at_utc", state.CreatedAtUtc.ToString("O"));
command.ExecuteNonQuery();
_clientKeysByHash[state.KeyHash] = state;
}
return new CreatedUserKey(
state.Id,
state.Name,
state.KeyPrefix,
state.CreatedAtUtc,
key);
}
public bool DeleteClientKey(string id)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(id))
{
return false;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "DELETE FROM client_keys WHERE id = $id";
command.Parameters.AddWithValue("$id", id);
var deleted = command.ExecuteNonQuery() > 0;
if (deleted)
{
foreach (var pair in _clientKeysByHash.Where(pair => pair.Value.Id == id).ToArray())
{
_clientKeysByHash.Remove(pair.Key);
}
}
return deleted;
}
}
public string? GetClientKeyId(string clientKey)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(clientKey))
{
return null;
}
var hash = HashKey(clientKey);
lock (_lock)
{
return _clientKeysByHash.TryGetValue(hash, out var key) ? key.Id : null;
}
}
public ClientAccess GetClientAccess(string clientId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(clientId))
{
return ClientAccess.Enabled;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT disabled_until_utc, disabled_manually, disabled_reason
FROM client_controls
WHERE client_id = $client_id
""";
command.Parameters.AddWithValue("$client_id", clientId);
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return ClientAccess.Enabled;
}
var disabledUntil = ReadNullableDateTimeOffset(reader, 0);
var disabledManually = reader.GetInt32(1) != 0;
var reason = reader.IsDBNull(2) ? null : reader.GetString(2);
if (disabledManually)
{
return new ClientAccess(true, null, true, reason);
}
if (disabledUntil is { } until && until > DateTimeOffset.UtcNow)
{
return new ClientAccess(true, until, false, reason);
}
return ClientAccess.Enabled;
}
}
public IReadOnlyDictionary<string, ClientAccess> ListClientControls()
{
if (!_isAvailable)
{
return new Dictionary<string, ClientAccess>(StringComparer.OrdinalIgnoreCase);
}
var result = new Dictionary<string, ClientAccess>(StringComparer.OrdinalIgnoreCase);
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "SELECT client_id, disabled_until_utc, disabled_manually, disabled_reason FROM client_controls";
using var reader = command.ExecuteReader();
while (reader.Read())
{
var clientId = reader.GetString(0);
var disabledUntil = ReadNullableDateTimeOffset(reader, 1);
var disabledManually = reader.GetInt32(2) != 0;
var reason = reader.IsDBNull(3) ? null : reader.GetString(3);
result[clientId] = disabledManually
? new ClientAccess(true, null, true, reason)
: disabledUntil is { } until && until > DateTimeOffset.UtcNow
? new ClientAccess(true, until, false, reason)
: ClientAccess.Enabled;
}
}
return result;
}
public void DisableClient(string clientId, TimeSpan? duration, bool manually, string? reason)
{
EnsureAvailable();
if (string.IsNullOrWhiteSpace(clientId))
{
throw new ArgumentException("Client id is required.", nameof(clientId));
}
var now = DateTimeOffset.UtcNow;
var disabledUntil = manually ? null : now.Add(duration ?? TimeSpan.FromHours(1)).ToString("O");
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO client_controls (
client_id,
disabled_until_utc,
disabled_manually,
disabled_reason,
updated_at_utc)
VALUES (
$client_id,
$disabled_until_utc,
$disabled_manually,
$disabled_reason,
$updated_at_utc)
ON CONFLICT(client_id) DO UPDATE SET
disabled_until_utc = excluded.disabled_until_utc,
disabled_manually = excluded.disabled_manually,
disabled_reason = excluded.disabled_reason,
updated_at_utc = excluded.updated_at_utc
""";
command.Parameters.AddWithValue("$client_id", clientId);
command.Parameters.AddWithValue("$disabled_until_utc", (object?)disabledUntil ?? DBNull.Value);
command.Parameters.AddWithValue("$disabled_manually", manually ? 1 : 0);
command.Parameters.AddWithValue("$disabled_reason", string.IsNullOrWhiteSpace(reason) ? DBNull.Value : reason.Trim());
command.Parameters.AddWithValue("$updated_at_utc", now.ToString("O"));
command.ExecuteNonQuery();
}
}
public void EnableClient(string clientId)
{
EnsureAvailable();
if (string.IsNullOrWhiteSpace(clientId))
{
throw new ArgumentException("Client id is required.", nameof(clientId));
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO client_controls (
client_id,
disabled_until_utc,
disabled_manually,
disabled_reason,
updated_at_utc)
VALUES (
$client_id,
NULL,
0,
NULL,
$updated_at_utc)
ON CONFLICT(client_id) DO UPDATE SET
disabled_until_utc = NULL,
disabled_manually = 0,
disabled_reason = NULL,
updated_at_utc = excluded.updated_at_utc
""";
command.Parameters.AddWithValue("$client_id", clientId);
command.Parameters.AddWithValue("$updated_at_utc", DateTimeOffset.UtcNow.ToString("O"));
command.ExecuteNonQuery();
}
}
public void RecordRequest(RequestMetric metric)
{
if (!_isAvailable)
{
return;
}
try
{
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO request_metrics (
client_id,
model,
method,
path,
status_code,
prompt_tokens,
completion_tokens,
token_count,
user_key_id,
cost,
started_at_utc,
completed_at_utc,
duration_ms)
VALUES (
$client_id,
$model,
$method,
$path,
$status_code,
$prompt_tokens,
$completion_tokens,
$token_count,
$user_key_id,
$cost,
$started_at_utc,
$completed_at_utc,
$duration_ms)
""";
command.Parameters.AddWithValue("$client_id", metric.ClientId);
command.Parameters.AddWithValue("$model", string.IsNullOrWhiteSpace(metric.Model) ? DBNull.Value : metric.Model);
command.Parameters.AddWithValue("$method", metric.Method);
command.Parameters.AddWithValue("$path", metric.Path);
command.Parameters.AddWithValue("$status_code", metric.StatusCode is null ? DBNull.Value : metric.StatusCode.Value);
command.Parameters.AddWithValue("$prompt_tokens", metric.PromptTokens);
command.Parameters.AddWithValue("$completion_tokens", metric.CompletionTokens);
command.Parameters.AddWithValue("$token_count", metric.TokenCount);
command.Parameters.AddWithValue("$user_key_id", string.IsNullOrWhiteSpace(metric.UserKeyId) ? DBNull.Value : metric.UserKeyId);
command.Parameters.AddWithValue("$cost", metric.Cost);
command.Parameters.AddWithValue("$started_at_utc", metric.StartedAtUtc.ToString("O"));
command.Parameters.AddWithValue("$completed_at_utc", metric.CompletedAtUtc.ToString("O"));
command.Parameters.AddWithValue("$duration_ms", metric.Duration.TotalMilliseconds);
command.ExecuteNonQuery();
}
}
catch (Exception exception) when (exception is SqliteException or IOException or UnauthorizedAccessException)
{
_logger.LogWarning(exception, "Failed to record request metric for client {ClientId}.", metric.ClientId);
}
}
public IReadOnlyDictionary<string, ClientRequestStats> GetClientRequestStats()
{
if (!_isAvailable)
{
return new Dictionary<string, ClientRequestStats>(StringComparer.OrdinalIgnoreCase);
}
var now = DateTimeOffset.UtcNow;
var since10 = now.AddMinutes(-10).ToString("O");
var sinceHour = now.AddHours(-1).ToString("O");
var result = new Dictionary<string, ClientRequestStats>(StringComparer.OrdinalIgnoreCase);
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT
client_id,
COUNT(*),
SUM(CASE WHEN started_at_utc >= $since10 THEN 1 ELSE 0 END),
SUM(CASE WHEN started_at_utc >= $sinceHour THEN 1 ELSE 0 END)
FROM request_metrics
GROUP BY client_id
""";
command.Parameters.AddWithValue("$since10", since10);
command.Parameters.AddWithValue("$sinceHour", sinceHour);
using var reader = command.ExecuteReader();
while (reader.Read())
{
result[reader.GetString(0)] = new ClientRequestStats(
reader.GetInt64(1),
reader.GetInt64(2),
reader.GetInt64(3));
}
}
return result;
}
public IReadOnlyDictionary<string, ModelUsageStats> GetModelUsageStats()
{
if (!_isAvailable)
{
return new Dictionary<string, ModelUsageStats>(StringComparer.OrdinalIgnoreCase);
}
var now = DateTimeOffset.UtcNow;
var since10 = now.AddMinutes(-10).ToString("O");
var sinceHour = now.AddHours(-1).ToString("O");
var result = new Dictionary<string, ModelUsageStats>(StringComparer.OrdinalIgnoreCase);
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT
model,
COUNT(*),
SUM(CASE WHEN started_at_utc >= $since10 THEN 1 ELSE 0 END),
SUM(CASE WHEN started_at_utc >= $sinceHour THEN 1 ELSE 0 END),
SUM(CASE WHEN started_at_utc >= $since10 THEN token_count ELSE 0 END),
SUM(CASE WHEN started_at_utc >= $sinceHour THEN token_count ELSE 0 END)
FROM request_metrics
WHERE model IS NOT NULL AND model <> ''
GROUP BY model
""";
command.Parameters.AddWithValue("$since10", since10);
command.Parameters.AddWithValue("$sinceHour", sinceHour);
using var reader = command.ExecuteReader();
while (reader.Read())
{
result[reader.GetString(0)] = new ModelUsageStats(
reader.GetInt64(1),
reader.GetInt64(2),
reader.GetInt64(3),
reader.GetInt64(4),
reader.GetInt64(5));
}
}
return result;
}
public string? GetUserKeyId(string userKey)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(userKey))
{
return null;
}
var hash = HashKey(userKey);
lock (_lock)
{
return _userKeysByHash.TryGetValue(hash, out var key) ? key.Id : null;
}
}
public IReadOnlyList<GroupInfo> ListGroups()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "SELECT id, name, created_at_utc, updated_at_utc FROM groups ORDER BY name";
var result = new List<GroupInfo>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new GroupInfo(
reader.GetString(0),
reader.GetString(1),
ReadDateTimeOffset(reader.GetString(2)),
ReadDateTimeOffset(reader.GetString(3))));
}
return result;
}
}
public GroupInfo? GetGroup(string groupId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId))
{
return null;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "SELECT id, name, created_at_utc, updated_at_utc FROM groups WHERE id = $id";
command.Parameters.AddWithValue("$id", groupId);
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return null;
}
return new GroupInfo(
reader.GetString(0),
reader.GetString(1),
ReadDateTimeOffset(reader.GetString(2)),
ReadDateTimeOffset(reader.GetString(3)));
}
}
public GroupInfo CreateGroup(string? name)
{
EnsureAvailable();
var now = DateTimeOffset.UtcNow;
var id = Guid.NewGuid().ToString("n");
var groupName = string.IsNullOrWhiteSpace(name) ? "Group" : name.Trim();
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO groups (id, name, created_at_utc, updated_at_utc)
VALUES ($id, $name, $created_at_utc, $updated_at_utc)
""";
command.Parameters.AddWithValue("$id", id);
command.Parameters.AddWithValue("$name", groupName);
command.Parameters.AddWithValue("$created_at_utc", now.ToString("O"));
command.Parameters.AddWithValue("$updated_at_utc", now.ToString("O"));
command.ExecuteNonQuery();
}
return new GroupInfo(id, groupName, now, now);
}
public bool UpdateGroup(string groupId, string name)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId) || string.IsNullOrWhiteSpace(name))
{
return false;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
UPDATE groups SET name = $name, updated_at_utc = $updated_at_utc
WHERE id = $id
""";
command.Parameters.AddWithValue("$id", groupId);
command.Parameters.AddWithValue("$name", name.Trim());
command.Parameters.AddWithValue("$updated_at_utc", DateTimeOffset.UtcNow.ToString("O"));
return command.ExecuteNonQuery() > 0;
}
}
public bool DeleteGroup(string groupId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId))
{
return false;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "DELETE FROM groups WHERE id = $id";
command.Parameters.AddWithValue("$id", groupId);
return command.ExecuteNonQuery() > 0;
}
}
public IReadOnlyList<GroupClientInfo> ListGroupClients(string groupId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId))
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT id, group_id, client_id, model, client_pattern,
keepalive_instances_to_keep_alive,
keepalive_max_parallelism_per_client,
keepalive_parallelism_headroom
FROM group_members
WHERE group_id = $group_id
ORDER BY client_id, model, client_pattern
""";
command.Parameters.AddWithValue("$group_id", groupId);
return ReadGroupClientInfos(command);
}
}
public IReadOnlyList<GroupClientInfo> ListAllGroupClients()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT id, group_id, client_id, model, client_pattern,
keepalive_instances_to_keep_alive,
keepalive_max_parallelism_per_client,
keepalive_parallelism_headroom
FROM group_members
ORDER BY client_id, model, client_pattern
""";
return ReadGroupClientInfos(command);
}
}
private List<GroupClientInfo> ReadGroupClientInfos(SqliteCommand command)
{
var result = new List<GroupClientInfo>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new GroupClientInfo(
reader.GetInt64(0),
reader.GetString(1),
reader.IsDBNull(2) ? null : reader.GetString(2),
reader.IsDBNull(3) ? null : reader.GetString(3),
reader.IsDBNull(4) ? null : reader.GetString(4),
ReadKeepalivePolicy(reader, 5, 6, 7)));
}
return result;
}
public GroupClientInfo AddGroupClient(
string groupId,
string? clientId,
string? model,
string? clientPattern,
int? keepaliveInstancesToKeepAlive,
int? keepaliveMaxParallelismPerClient,
int? keepaliveParallelismHeadroom)
{
EnsureAvailable();
if (string.IsNullOrWhiteSpace(groupId))
{
throw new ArgumentException("Group id is required.", nameof(groupId));
}
if (string.IsNullOrWhiteSpace(clientId) && string.IsNullOrWhiteSpace(clientPattern))
{
throw new ArgumentException("Either client_id or client_pattern is required.");
}
if (!string.IsNullOrWhiteSpace(clientPattern))
{
try
{
_ = Regex.IsMatch("", clientPattern);
}
catch (RegexParseException ex)
{
throw new ArgumentException($"Unable to add client - invalid regex: {ex.Message}", nameof(clientPattern));
}
}
var policy = NormalizeKeepalivePolicy(
keepaliveInstancesToKeepAlive,
keepaliveMaxParallelismPerClient,
keepaliveParallelismHeadroom);
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO group_members (
group_id,
client_id,
model,
client_pattern,
keepalive_instances_to_keep_alive,
keepalive_max_parallelism_per_client,
keepalive_parallelism_headroom)
VALUES (
$group_id,
$client_id,
$model,
$client_pattern,
$keepalive_instances_to_keep_alive,
$keepalive_max_parallelism_per_client,
$keepalive_parallelism_headroom)
""";
command.Parameters.AddWithValue("$group_id", groupId);
command.Parameters.AddWithValue("$client_id", string.IsNullOrWhiteSpace(clientId) ? DBNull.Value : clientId);
command.Parameters.AddWithValue("$model", string.IsNullOrWhiteSpace(model) ? DBNull.Value : model);
command.Parameters.AddWithValue("$client_pattern", string.IsNullOrWhiteSpace(clientPattern) ? DBNull.Value : clientPattern);
command.Parameters.AddWithValue("$keepalive_instances_to_keep_alive", policy.InstancesToKeepAlive);
command.Parameters.AddWithValue("$keepalive_max_parallelism_per_client", policy.MaxParallelismPerClient);
command.Parameters.AddWithValue("$keepalive_parallelism_headroom", policy.ParallelismHeadroom);
command.ExecuteNonQuery();
using var idCommand = connection.CreateCommand();
idCommand.CommandText = "SELECT last_insert_rowid()";
var insertedId = (long)idCommand.ExecuteScalar()!;
return new GroupClientInfo(insertedId, groupId, clientId, model, clientPattern, policy);
}
}
public bool RemoveGroupClient(long memberId)
{
if (!_isAvailable)
{
return false;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "DELETE FROM group_members WHERE id = $id";
command.Parameters.AddWithValue("$id", memberId);
return command.ExecuteNonQuery() > 0;
}
}
public IReadOnlyList<string> GetUserKeyGroupIds(string userKeyId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(userKeyId))
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT g.id FROM groups g
INNER JOIN user_key_groups ukg ON g.id = ukg.group_id
WHERE ukg.user_key_id = $user_key_id
ORDER BY g.name
""";
command.Parameters.AddWithValue("$user_key_id", userKeyId);
var result = new List<string>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(reader.GetString(0));
}
return result;
}
}
public void SetUserKeyGroups(string userKeyId, IReadOnlyList<string> groupIds)
{
EnsureAvailable();
if (string.IsNullOrWhiteSpace(userKeyId))
{
throw new ArgumentException("User key id is required.", nameof(userKeyId));
}
lock (_lock)
{
using var connection = OpenConnection();
using var transaction = connection.BeginTransaction();
try
{
using (var deleteCommand = connection.CreateCommand())
{
deleteCommand.Transaction = transaction;
deleteCommand.CommandText = "DELETE FROM user_key_groups WHERE user_key_id = $user_key_id";
deleteCommand.Parameters.AddWithValue("$user_key_id", userKeyId);
deleteCommand.ExecuteNonQuery();
}
using (var insertCommand = connection.CreateCommand())
{
insertCommand.Transaction = transaction;
insertCommand.CommandText = """
INSERT INTO user_key_groups (user_key_id, group_id)
VALUES ($user_key_id, $group_id)
""";
var userKeyParam = insertCommand.Parameters.Add("$user_key_id", SqliteType.Text);
var groupParam = insertCommand.Parameters.Add("$group_id", SqliteType.Text);
userKeyParam.Value = userKeyId;
foreach (var groupId in groupIds.Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.OrdinalIgnoreCase))
{
groupParam.Value = groupId;
insertCommand.ExecuteNonQuery();
}
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
public IReadOnlyList<UserKeyGroupInfo> ListUserKeyGroups()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT uk.id, uk.name, uk.key_prefix,
GROUP_CONCAT(g.id) as group_ids,
GROUP_CONCAT(g.name) as group_names
FROM user_keys uk
LEFT JOIN user_key_groups ukg ON uk.id = ukg.user_key_id
LEFT JOIN groups g ON ukg.group_id = g.id
GROUP BY uk.id
ORDER BY uk.name
""";
var result = new List<UserKeyGroupInfo>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var keyId = reader.GetString(0);
var keyName = reader.GetString(1);
var keyPrefix = reader.GetString(2);
var groupIds = reader.IsDBNull(3)
? []
: reader.GetString(3).Split(',', StringSplitOptions.RemoveEmptyEntries);
var groupNames = reader.IsDBNull(4)
? []
: reader.GetString(4).Split(',', StringSplitOptions.RemoveEmptyEntries);
result.Add(new UserKeyGroupInfo(keyId, keyName, keyPrefix, groupIds, groupNames));
}
return result;
}
}
public GroupAccess ResolveGroupAccess(string? userKeyId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(userKeyId))
{
return GroupAccess.Unrestricted;
}
lock (_lock)
{
var groupIds = GetUserKeyGroupIdsLocked(userKeyId);
if (groupIds.Count == 0)
{
return GroupAccess.Unrestricted;
}
var clientModels = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var allClients = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT gm.client_id, gm.model, gm.client_pattern
FROM group_members gm
INNER JOIN user_key_groups ukg ON gm.group_id = ukg.group_id
WHERE ukg.user_key_id = $user_key_id
""";
command.Parameters.AddWithValue("$user_key_id", userKeyId);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var clientId = reader.IsDBNull(0) ? null : reader.GetString(0);
var model = reader.IsDBNull(1) ? null : reader.GetString(1);
var pattern = reader.IsDBNull(2) ? null : reader.GetString(2);
if (!string.IsNullOrWhiteSpace(pattern))
{
Regex? regex = null;
try
{
regex = new Regex(
pattern,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
catch (RegexParseException)
{
continue;
}
foreach (var connectedClient in _getConnectedClientIds())
{
if (regex.IsMatch(connectedClient))
{
if (!string.IsNullOrWhiteSpace(model))
{
clientModels.Add($"{connectedClient}:{model}");
}
else
{
allClients.Add(connectedClient);
}
}
}
}
else if (!string.IsNullOrWhiteSpace(clientId))
{
if (!string.IsNullOrWhiteSpace(model))
{
clientModels.Add($"{clientId}:{model}");
}
else
{
allClients.Add(clientId);
}
}
}
return new GroupAccess(clientModels, allClients);
}
}
public IReadOnlyDictionary<string, IReadOnlyList<string>> ResolveClientGroups(IReadOnlyList<string> clientIds)
{
if (!_isAvailable || clientIds.Count == 0)
{
return new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
}
var result = new Dictionary<string, SortedSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var clientId in clientIds)
{
result[clientId] = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT g.name, gm.client_id, gm.client_pattern
FROM group_members gm
INNER JOIN groups g ON gm.group_id = g.id
""";
using var reader = command.ExecuteReader();
while (reader.Read())
{
var groupName = reader.GetString(0);
var explicitClientId = reader.IsDBNull(1) ? null : reader.GetString(1);
var pattern = reader.IsDBNull(2) ? null : reader.GetString(2);
if (!string.IsNullOrWhiteSpace(explicitClientId)
&& result.TryGetValue(explicitClientId, out var explicitGroups))
{
explicitGroups.Add(groupName);
}
else if (!string.IsNullOrWhiteSpace(pattern))
{
Regex? regex = null;
try
{
regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
catch (RegexParseException)
{
continue;
}
foreach (var clientId in clientIds)
{
if (regex.IsMatch(clientId) && result.TryGetValue(clientId, out var groups))
{
groups.Add(groupName);
}
}
}
}
}
var frozen = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var (clientId, groups) in result)
{
frozen[clientId] = groups.ToList();
}
return frozen;
}
public GroupBillingInfo? GetGroupBilling(string groupId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId))
{
return null;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT group_id, currency, default_rate_per_1k, refuse_below_balance, enabled, created_at_utc, updated_at_utc
FROM group_billing WHERE group_id = $group_id
""";
command.Parameters.AddWithValue("$group_id", groupId);
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return null;
}
return new GroupBillingInfo(
reader.GetString(0),
reader.GetString(1),
reader.GetDouble(2),
reader.GetDouble(3),
reader.GetInt32(4) != 0,
ReadDateTimeOffset(reader.GetString(5)),
ReadDateTimeOffset(reader.GetString(6)));
}
}
public GroupBillingInfo UpsertGroupBilling(
string groupId,
string currency,
double defaultRatePer1k,
double refuseBelowBalance,
bool enabled)
{
EnsureAvailable();
if (string.IsNullOrWhiteSpace(groupId))
{
throw new ArgumentException("Group id is required.", nameof(groupId));
}
var now = DateTimeOffset.UtcNow;
var cur = string.IsNullOrWhiteSpace(currency) ? "EUR" : currency.Trim().ToUpperInvariant();
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO group_billing (group_id, currency, default_rate_per_1k, refuse_below_balance, enabled, created_at_utc, updated_at_utc)
VALUES ($group_id, $currency, $default_rate_per_1k, $refuse_below_balance, $enabled, $created_at_utc, $updated_at_utc)
ON CONFLICT(group_id) DO UPDATE SET
currency = excluded.currency,
default_rate_per_1k = excluded.default_rate_per_1k,
refuse_below_balance = excluded.refuse_below_balance,
enabled = excluded.enabled,
updated_at_utc = excluded.updated_at_utc
""";
command.Parameters.AddWithValue("$group_id", groupId);
command.Parameters.AddWithValue("$currency", cur);
command.Parameters.AddWithValue("$default_rate_per_1k", defaultRatePer1k);
command.Parameters.AddWithValue("$refuse_below_balance", refuseBelowBalance);
command.Parameters.AddWithValue("$enabled", enabled ? 1 : 0);
command.Parameters.AddWithValue("$created_at_utc", now.ToString("O"));
command.Parameters.AddWithValue("$updated_at_utc", now.ToString("O"));
command.ExecuteNonQuery();
}
return new GroupBillingInfo(groupId, cur, defaultRatePer1k, refuseBelowBalance, enabled, now, now);
}
public IReadOnlyList<GroupBillingRule> ListGroupBillingRules(string groupId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId))
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT id, group_id, model_regex, rate_per_1k, created_at_utc
FROM group_billing_rules WHERE group_id = $group_id ORDER BY id
""";
command.Parameters.AddWithValue("$group_id", groupId);
var result = new List<GroupBillingRule>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new GroupBillingRule(
reader.GetInt64(0),
reader.GetString(1),
reader.GetString(2),
reader.GetDouble(3),
ReadDateTimeOffset(reader.GetString(4))));
}
return result;
}
}
public GroupBillingRule AddBillingRule(string groupId, string modelRegex, double ratePer1k)
{
EnsureAvailable();
if (string.IsNullOrWhiteSpace(groupId) || string.IsNullOrWhiteSpace(modelRegex))
{
throw new ArgumentException("Group id and model regex are required.");
}
try
{
_ = Regex.IsMatch("", modelRegex);
}
catch (RegexParseException ex)
{
throw new ArgumentException($"Invalid regex: {ex.Message}", nameof(modelRegex));
}
var now = DateTimeOffset.UtcNow;
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO group_billing_rules (group_id, model_regex, rate_per_1k, created_at_utc)
VALUES ($group_id, $model_regex, $rate_per_1k, $created_at_utc)
""";
command.Parameters.AddWithValue("$group_id", groupId);
command.Parameters.AddWithValue("$model_regex", modelRegex.Trim());
command.Parameters.AddWithValue("$rate_per_1k", ratePer1k);
command.Parameters.AddWithValue("$created_at_utc", now.ToString("O"));
command.ExecuteNonQuery();
using var idCommand = connection.CreateCommand();
idCommand.CommandText = "SELECT last_insert_rowid()";
var insertedId = (long)idCommand.ExecuteScalar()!;
return new GroupBillingRule(insertedId, groupId, modelRegex.Trim(), ratePer1k, now);
}
}
public bool UpdateBillingRule(long ruleId, string modelRegex, double ratePer1k)
{
if (!_isAvailable)
{
return false;
}
try
{
_ = Regex.IsMatch("", modelRegex);
}
catch (RegexParseException ex)
{
throw new ArgumentException($"Invalid regex: {ex.Message}", nameof(modelRegex));
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
UPDATE group_billing_rules SET model_regex = $model_regex, rate_per_1k = $rate_per_1k WHERE id = $id
""";
command.Parameters.AddWithValue("$id", ruleId);
command.Parameters.AddWithValue("$model_regex", modelRegex.Trim());
command.Parameters.AddWithValue("$rate_per_1k", ratePer1k);
return command.ExecuteNonQuery() > 0;
}
}
public bool DeleteBillingRule(long ruleId)
{
if (!_isAvailable)
{
return false;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "DELETE FROM group_billing_rules WHERE id = $id";
command.Parameters.AddWithValue("$id", ruleId);
return command.ExecuteNonQuery() > 0;
}
}
public IReadOnlyList<GroupPayment> ListGroupPayments(string groupId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId))
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT id, group_id, amount, description, created_at_utc, created_by
FROM group_payments WHERE group_id = $group_id ORDER BY created_at_utc DESC
""";
command.Parameters.AddWithValue("$group_id", groupId);
var result = new List<GroupPayment>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new GroupPayment(
reader.GetInt64(0),
reader.GetString(1),
reader.GetDouble(2),
reader.IsDBNull(3) ? null : reader.GetString(3),
ReadDateTimeOffset(reader.GetString(4)),
reader.IsDBNull(5) ? null : reader.GetString(5)));
}
return result;
}
}
public GroupPayment AddPayment(string groupId, double amount, string? description, string? createdBy)
{
EnsureAvailable();
if (string.IsNullOrWhiteSpace(groupId))
{
throw new ArgumentException("Group id is required.", nameof(groupId));
}
var now = DateTimeOffset.UtcNow;
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO group_payments (group_id, amount, description, created_at_utc, created_by)
VALUES ($group_id, $amount, $description, $created_at_utc, $created_by)
""";
command.Parameters.AddWithValue("$group_id", groupId);
command.Parameters.AddWithValue("$amount", amount);
command.Parameters.AddWithValue("$description", string.IsNullOrWhiteSpace(description) ? DBNull.Value : description.Trim());
command.Parameters.AddWithValue("$created_at_utc", now.ToString("O"));
command.Parameters.AddWithValue("$created_by", string.IsNullOrWhiteSpace(createdBy) ? DBNull.Value : createdBy);
command.ExecuteNonQuery();
using var idCommand = connection.CreateCommand();
idCommand.CommandText = "SELECT last_insert_rowid()";
var insertedId = (long)idCommand.ExecuteScalar()!;
return new GroupPayment(insertedId, groupId, amount, description, now, createdBy);
}
}
public bool DeletePayment(long paymentId)
{
if (!_isAvailable)
{
return false;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "DELETE FROM group_payments WHERE id = $id";
command.Parameters.AddWithValue("$id", paymentId);
return command.ExecuteNonQuery() > 0;
}
}
public GroupBalance GetGroupBalance(string groupId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(groupId))
{
return new GroupBalance(groupId ?? "", "EUR", 0, 0, 0);
}
lock (_lock)
{
using var connection = OpenConnection();
string currency = "EUR";
using (var billingCmd = connection.CreateCommand())
{
billingCmd.CommandText = "SELECT currency FROM group_billing WHERE group_id = $group_id";
billingCmd.Parameters.AddWithValue("$group_id", groupId);
var curResult = billingCmd.ExecuteScalar();
if (curResult is not null)
{
currency = curResult.ToString() ?? "EUR";
}
}
double totalPayments = 0;
using (var payCmd = connection.CreateCommand())
{
payCmd.CommandText = "SELECT COALESCE(SUM(amount), 0) FROM group_payments WHERE group_id = $group_id";
payCmd.Parameters.AddWithValue("$group_id", groupId);
totalPayments = Convert.ToDouble(payCmd.ExecuteScalar());
}
double totalCosts = 0;
using (var costCmd = connection.CreateCommand())
{
costCmd.CommandText = """
SELECT COALESCE(SUM(rm.cost), 0)
FROM request_metrics rm
INNER JOIN user_key_groups ukg ON rm.user_key_id = ukg.user_key_id
WHERE ukg.group_id = $group_id AND rm.user_key_id IS NOT NULL
""";
costCmd.Parameters.AddWithValue("$group_id", groupId);
totalCosts = Convert.ToDouble(costCmd.ExecuteScalar());
}
return new GroupBalance(groupId, currency, totalPayments, totalCosts, totalPayments - totalCosts);
}
}
public GroupBillingInfo? ResolveBillingForUserKey(string? userKeyId)
{
if (!_isAvailable || string.IsNullOrWhiteSpace(userKeyId))
{
return null;
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT gb.group_id, gb.currency, gb.default_rate_per_1k, gb.refuse_below_balance, gb.enabled, gb.created_at_utc, gb.updated_at_utc
FROM group_billing gb
INNER JOIN user_key_groups ukg ON gb.group_id = ukg.group_id
WHERE ukg.user_key_id = $user_key_id AND gb.enabled = 1
ORDER BY gb.created_at_utc ASC
LIMIT 1
""";
command.Parameters.AddWithValue("$user_key_id", userKeyId);
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return null;
}
return new GroupBillingInfo(
reader.GetString(0),
reader.GetString(1),
reader.GetDouble(2),
reader.GetDouble(3),
reader.GetInt32(4) != 0,
ReadDateTimeOffset(reader.GetString(5)),
ReadDateTimeOffset(reader.GetString(6)));
}
}
public double CalculateCost(string groupId, string? model, int totalTokens)
{
if (totalTokens <= 0)
{
return 0;
}
lock (_lock)
{
var rules = ListGroupBillingRulesLocked(groupId);
var billing = GetGroupBillingLocked(groupId);
if (billing is null)
{
return 0;
}
var ratePer1k = billing.DefaultRatePer1k;
if (!string.IsNullOrWhiteSpace(model))
{
foreach (var rule in rules)
{
try
{
if (Regex.IsMatch(model, rule.ModelRegex, RegexOptions.IgnoreCase))
{
ratePer1k = rule.RatePer1k;
break;
}
}
catch (RegexParseException)
{
continue;
}
}
}
return (totalTokens / 1000.0) * ratePer1k;
}
}
public (bool Allowed, double Balance, string Currency, double Threshold) CheckBalanceForUserKey(string? userKeyId)
{
var billing = ResolveBillingForUserKey(userKeyId);
if (billing is null)
{
return (true, 0, "", 0);
}
var balance = GetGroupBalance(billing.GroupId);
var allowed = balance.Balance >= billing.RefuseBelowBalance;
return (allowed, balance.Balance, billing.Currency, billing.RefuseBelowBalance);
}
public IReadOnlyList<TokenStatsByModel> GetTokenStatsByModel()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT model,
COALESCE(SUM(prompt_tokens), 0),
COALESCE(SUM(completion_tokens), 0),
COALESCE(SUM(token_count), 0),
COUNT(*)
FROM request_metrics
WHERE model IS NOT NULL AND model <> ''
GROUP BY model
ORDER BY model
""";
var result = new List<TokenStatsByModel>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new TokenStatsByModel(
reader.GetString(0),
reader.GetInt64(1),
reader.GetInt64(2),
reader.GetInt64(3),
reader.GetInt64(4)));
}
return result;
}
}
public IReadOnlyList<TokenStatsByClient> GetTokenStatsByClient()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT client_id,
COALESCE(SUM(prompt_tokens), 0),
COALESCE(SUM(completion_tokens), 0),
COALESCE(SUM(token_count), 0),
COUNT(*)
FROM request_metrics
GROUP BY client_id
ORDER BY client_id
""";
var result = new List<TokenStatsByClient>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new TokenStatsByClient(
reader.GetString(0),
reader.GetInt64(1),
reader.GetInt64(2),
reader.GetInt64(3),
reader.GetInt64(4)));
}
return result;
}
}
public IReadOnlyList<TokenStatsByUserKey> GetTokenStatsByUserKey()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT rm.user_key_id, COALESCE(uk.name, 'Unknown'), COALESCE(uk.key_prefix, ''),
COALESCE(SUM(rm.prompt_tokens), 0),
COALESCE(SUM(rm.completion_tokens), 0),
COALESCE(SUM(rm.token_count), 0),
COUNT(*)
FROM request_metrics rm
LEFT JOIN user_keys uk ON rm.user_key_id = uk.id
WHERE rm.user_key_id IS NOT NULL
GROUP BY rm.user_key_id
ORDER BY uk.name
""";
var result = new List<TokenStatsByUserKey>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new TokenStatsByUserKey(
reader.GetString(0),
reader.GetString(1),
reader.GetString(2),
reader.GetInt64(3),
reader.GetInt64(4),
reader.GetInt64(5),
reader.GetInt64(6)));
}
return result;
}
}
public IReadOnlyList<TokenStatsByGroup> GetTokenStatsByGroup()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT akg.group_id, COALESCE(g.name, 'Unknown'),
COALESCE(SUM(rm.prompt_tokens), 0),
COALESCE(SUM(rm.completion_tokens), 0),
COALESCE(SUM(rm.token_count), 0),
COUNT(*)
FROM request_metrics rm
INNER JOIN user_key_groups akg ON rm.user_key_id = akg.user_key_id
INNER JOIN groups g ON akg.group_id = g.id
WHERE rm.user_key_id IS NOT NULL
GROUP BY akg.group_id
ORDER BY g.name
""";
var result = new List<TokenStatsByGroup>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new TokenStatsByGroup(
reader.GetString(0),
reader.GetString(1),
reader.GetInt64(2),
reader.GetInt64(3),
reader.GetInt64(4),
reader.GetInt64(5)));
}
return result;
}
}
public IReadOnlyList<ClientRevenue> GetClientRevenue()
{
if (!_isAvailable)
{
return [];
}
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT rm.client_id,
COALESCE(SUM(rm.cost), 0)
FROM request_metrics rm
WHERE rm.user_key_id IS NOT NULL AND rm.cost > 0
GROUP BY rm.client_id
ORDER BY rm.client_id
""";
var result = new List<ClientRevenue>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var clientId = reader.GetString(0);
var revenue = reader.GetDouble(1);
var billing = ResolveBillingForUserKeyForClientLocked(clientId);
var currency = billing?.Currency ?? "EUR";
result.Add(new ClientRevenue(clientId, revenue, currency));
}
return result;
}
}
public IReadOnlyList<GroupBillingRule> ListGroupBillingRulesLocked(string groupId)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT id, group_id, model_regex, rate_per_1k, created_at_utc
FROM group_billing_rules WHERE group_id = $group_id ORDER BY id
""";
command.Parameters.AddWithValue("$group_id", groupId);
var result = new List<GroupBillingRule>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new GroupBillingRule(
reader.GetInt64(0),
reader.GetString(1),
reader.GetString(2),
reader.GetDouble(3),
ReadDateTimeOffset(reader.GetString(4))));
}
return result;
}
private GroupBillingInfo? GetGroupBillingLocked(string groupId)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT group_id, currency, default_rate_per_1k, refuse_below_balance, enabled, created_at_utc, updated_at_utc
FROM group_billing WHERE group_id = $group_id
""";
command.Parameters.AddWithValue("$group_id", groupId);
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return null;
}
return new GroupBillingInfo(
reader.GetString(0),
reader.GetString(1),
reader.GetDouble(2),
reader.GetDouble(3),
reader.GetInt32(4) != 0,
ReadDateTimeOffset(reader.GetString(5)),
ReadDateTimeOffset(reader.GetString(6)));
}
private GroupBillingInfo? ResolveBillingForUserKeyForClientLocked(string clientId)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT gb.group_id, gb.currency, gb.default_rate_per_1k, gb.refuse_below_balance, gb.enabled, gb.created_at_utc, gb.updated_at_utc
FROM group_billing gb
INNER JOIN user_key_groups ukg ON gb.group_id = ukg.group_id
INNER JOIN request_metrics rm ON rm.user_key_id = ukg.user_key_id
WHERE rm.client_id = $client_id AND gb.enabled = 1
ORDER BY gb.created_at_utc ASC
LIMIT 1
""";
command.Parameters.AddWithValue("$client_id", clientId);
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return null;
}
return new GroupBillingInfo(
reader.GetString(0),
reader.GetString(1),
reader.GetDouble(2),
reader.GetDouble(3),
reader.GetInt32(4) != 0,
ReadDateTimeOffset(reader.GetString(5)),
ReadDateTimeOffset(reader.GetString(6)));
}
private IReadOnlyList<string> GetUserKeyGroupIdsLocked(string userKeyId)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = "SELECT group_id FROM user_key_groups WHERE user_key_id = $user_key_id";
command.Parameters.AddWithValue("$user_key_id", userKeyId);
var result = new List<string>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(reader.GetString(0));
}
return result;
}
private Func<IEnumerable<string>> _getConnectedClientIds = () => [];
public void SetConnectedClientProvider(Func<IEnumerable<string>> provider)
{
_getConnectedClientIds = provider;
}
private void Initialize()
{
var directory = Path.GetDirectoryName(_databasePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
using var connection = OpenConnection();
using (var pragma = connection.CreateCommand())
{
pragma.CommandText = "PRAGMA journal_mode=WAL";
pragma.ExecuteNonQuery();
}
using (var command = connection.CreateCommand())
{
command.CommandText = """
CREATE TABLE IF NOT EXISTS client_controls (
client_id TEXT NOT NULL PRIMARY KEY,
disabled_until_utc TEXT NULL,
disabled_manually INTEGER NOT NULL DEFAULT 0,
disabled_reason TEXT NULL,
updated_at_utc TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_keys (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL,
created_at_utc TEXT NOT NULL,
last_used_at_utc TEXT NULL
);
CREATE TABLE IF NOT EXISTS request_metrics (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
client_id TEXT NOT NULL,
model TEXT NULL,
method TEXT NOT NULL,
path TEXT NOT NULL,
status_code INTEGER NULL,
token_count INTEGER NOT NULL DEFAULT 0,
prompt_tokens INTEGER NOT NULL DEFAULT 0,
completion_tokens INTEGER NOT NULL DEFAULT 0,
user_key_id TEXT NULL,
cost REAL NOT NULL DEFAULT 0,
started_at_utc TEXT NOT NULL,
completed_at_utc TEXT NOT NULL,
duration_ms REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_request_metrics_client_started
ON request_metrics (client_id, started_at_utc);
CREATE INDEX IF NOT EXISTS idx_request_metrics_model_started
ON request_metrics (model, started_at_utc);
CREATE INDEX IF NOT EXISTS idx_request_metrics_user_key
ON request_metrics (user_key_id, started_at_utc);
CREATE TABLE IF NOT EXISTS groups (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at_utc TEXT NOT NULL,
updated_at_utc TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS group_members (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
client_id TEXT NULL,
model TEXT NULL,
client_pattern TEXT NULL,
keepalive_instances_to_keep_alive INTEGER NOT NULL DEFAULT 1,
keepalive_max_parallelism_per_client INTEGER NOT NULL DEFAULT 1,
keepalive_parallelism_headroom INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_group_members_group_id
ON group_members (group_id);
CREATE TABLE IF NOT EXISTS user_key_groups (
user_key_id TEXT NOT NULL REFERENCES user_keys(id) ON DELETE CASCADE,
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY (user_key_id, group_id)
);
CREATE TABLE IF NOT EXISTS client_keys (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL,
created_at_utc TEXT NOT NULL,
last_used_at_utc TEXT NULL
);
""";
command.ExecuteNonQuery();
using (var migrate = connection.CreateCommand())
{
migrate.CommandText = """
SELECT COUNT(*) FROM pragma_table_info('group_members') WHERE name = 'keepalive_instances_to_keep_alive'
""";
var hasKeepaliveInstancesColumn = (long)migrate.ExecuteScalar()! > 0;
if (!hasKeepaliveInstancesColumn)
{
using var addKeepaliveInstances = connection.CreateCommand();
addKeepaliveInstances.CommandText = "ALTER TABLE group_members ADD COLUMN keepalive_instances_to_keep_alive INTEGER NOT NULL DEFAULT 1";
addKeepaliveInstances.ExecuteNonQuery();
using var addKeepaliveMaxParallelism = connection.CreateCommand();
addKeepaliveMaxParallelism.CommandText = "ALTER TABLE group_members ADD COLUMN keepalive_max_parallelism_per_client INTEGER NOT NULL DEFAULT 1";
addKeepaliveMaxParallelism.ExecuteNonQuery();
using var addKeepaliveHeadroom = connection.CreateCommand();
addKeepaliveHeadroom.CommandText = "ALTER TABLE group_members ADD COLUMN keepalive_parallelism_headroom INTEGER NOT NULL DEFAULT 1";
addKeepaliveHeadroom.ExecuteNonQuery();
}
}
using (var migrate = connection.CreateCommand())
{
migrate.CommandText = """
SELECT COUNT(*) FROM pragma_table_info('request_metrics') WHERE name = 'prompt_tokens'
""";
var hasColumn = (long)migrate.ExecuteScalar()! > 0;
if (!hasColumn)
{
using var alter1 = connection.CreateCommand();
alter1.CommandText = "ALTER TABLE request_metrics ADD COLUMN prompt_tokens INTEGER NOT NULL DEFAULT 0";
alter1.ExecuteNonQuery();
using var alter2 = connection.CreateCommand();
alter2.CommandText = "ALTER TABLE request_metrics ADD COLUMN completion_tokens INTEGER NOT NULL DEFAULT 0";
alter2.ExecuteNonQuery();
using var alter3 = connection.CreateCommand();
alter3.CommandText = "ALTER TABLE request_metrics ADD COLUMN user_key_id TEXT NULL";
alter3.ExecuteNonQuery();
using var alter4 = connection.CreateCommand();
alter4.CommandText = "ALTER TABLE request_metrics ADD COLUMN cost REAL NOT NULL DEFAULT 0";
alter4.ExecuteNonQuery();
using var idx = connection.CreateCommand();
idx.CommandText = "CREATE INDEX IF NOT EXISTS idx_request_metrics_user_key ON request_metrics (user_key_id, started_at_utc)";
idx.ExecuteNonQuery();
}
else
{
using var checkCol = connection.CreateCommand();
checkCol.CommandText = "SELECT COUNT(*) FROM pragma_table_info('request_metrics') WHERE name = 'api_key_id'";
var hasOldColumn = (long)checkCol.ExecuteScalar()! > 0;
if (hasOldColumn)
{
using var rename = connection.CreateCommand();
rename.CommandText = "ALTER TABLE request_metrics RENAME COLUMN api_key_id TO user_key_id";
rename.ExecuteNonQuery();
using var idx = connection.CreateCommand();
idx.CommandText = "CREATE INDEX IF NOT EXISTS idx_request_metrics_user_key ON request_metrics (user_key_id, started_at_utc)";
idx.ExecuteNonQuery();
}
else
{
using var checkNew = connection.CreateCommand();
checkNew.CommandText = "SELECT COUNT(*) FROM pragma_table_info('request_metrics') WHERE name = 'user_key_id'";
var hasNewColumn = (long)checkNew.ExecuteScalar()! > 0;
if (!hasNewColumn)
{
using var addCol = connection.CreateCommand();
addCol.CommandText = "ALTER TABLE request_metrics ADD COLUMN user_key_id TEXT NULL";
addCol.ExecuteNonQuery();
using var idx = connection.CreateCommand();
idx.CommandText = "CREATE INDEX IF NOT EXISTS idx_request_metrics_user_key ON request_metrics (user_key_id, started_at_utc)";
idx.ExecuteNonQuery();
}
}
}
}
using (var command2 = connection.CreateCommand())
{
command2.CommandText = """
CREATE TABLE IF NOT EXISTS group_billing (
group_id TEXT NOT NULL PRIMARY KEY REFERENCES groups(id) ON DELETE CASCADE,
currency TEXT NOT NULL DEFAULT 'EUR',
default_rate_per_1k REAL NOT NULL DEFAULT 0.0,
refuse_below_balance REAL NOT NULL DEFAULT 0.0,
enabled INTEGER NOT NULL DEFAULT 0,
created_at_utc TEXT NOT NULL,
updated_at_utc TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS group_billing_rules (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
model_regex TEXT NOT NULL,
rate_per_1k REAL NOT NULL,
created_at_utc TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_group_billing_rules_group ON group_billing_rules (group_id);
CREATE TABLE IF NOT EXISTS group_payments (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
amount REAL NOT NULL,
description TEXT NULL,
created_at_utc TEXT NOT NULL,
created_by TEXT NULL
);
CREATE INDEX IF NOT EXISTS idx_group_payments_group ON group_payments (group_id);
""";
command2.ExecuteNonQuery();
}
using (var migrate = connection.CreateCommand())
{
migrate.CommandText = """
SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='api_key_groups'
""";
var hasOldTable = (long)migrate.ExecuteScalar()! > 0;
if (hasOldTable)
{
using var rename = connection.CreateCommand();
rename.CommandText = "ALTER TABLE api_key_groups RENAME TO user_key_groups";
rename.ExecuteNonQuery();
using var renameCol = connection.CreateCommand();
renameCol.CommandText = "ALTER TABLE user_key_groups RENAME COLUMN api_key_id TO user_key_id";
renameCol.ExecuteNonQuery();
}
else
{
migrate.CommandText = """
SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='user_key_groups'
""";
var hasNewTable = (long)migrate.ExecuteScalar()! > 0;
if (hasNewTable)
{
using var checkCol = connection.CreateCommand();
checkCol.CommandText = "SELECT COUNT(*) FROM pragma_table_info('user_key_groups') WHERE name = 'api_key_id'";
var hasOldCol = (long)checkCol.ExecuteScalar()! > 0;
if (hasOldCol)
{
using var renameCol = connection.CreateCommand();
renameCol.CommandText = "ALTER TABLE user_key_groups RENAME COLUMN api_key_id TO user_key_id";
renameCol.ExecuteNonQuery();
}
}
}
migrate.CommandText = """
SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='api_keys'
""";
var hasOldApiKeys = (long)migrate.ExecuteScalar()! > 0;
if (hasOldApiKeys)
{
using var rename = connection.CreateCommand();
rename.CommandText = "ALTER TABLE api_keys RENAME TO user_keys";
rename.ExecuteNonQuery();
}
}
}
using (var command = connection.CreateCommand())
{
command.CommandText = """
SELECT id, name, key_hash, key_prefix, created_at_utc, last_used_at_utc
FROM user_keys
""";
using var reader = command.ExecuteReader();
while (reader.Read())
{
var state = new KeyState
{
Id = reader.GetString(0),
Name = reader.GetString(1),
KeyHash = reader.GetString(2),
KeyPrefix = reader.GetString(3),
CreatedAtUtc = ReadDateTimeOffset(reader.GetString(4)),
LastUsedUtc = ReadNullableDateTimeOffset(reader, 5)
};
_userKeysByHash[state.KeyHash] = state;
}
}
_logger.LogInformation(
"Loaded {UserKeyCount} user key(s) from {DatabasePath}.",
_userKeysByHash.Count,
_databasePath);
using (var command = connection.CreateCommand())
{
command.CommandText = """
SELECT id, name, key_hash, key_prefix, created_at_utc, last_used_at_utc
FROM client_keys
""";
using var reader = command.ExecuteReader();
while (reader.Read())
{
var state = new KeyState
{
Id = reader.GetString(0),
Name = reader.GetString(1),
KeyHash = reader.GetString(2),
KeyPrefix = reader.GetString(3),
CreatedAtUtc = ReadDateTimeOffset(reader.GetString(4)),
LastUsedUtc = ReadNullableDateTimeOffset(reader, 5)
};
_clientKeysByHash[state.KeyHash] = state;
}
}
_logger.LogInformation(
"Loaded {ClientKeyCount} client key(s) from {DatabasePath}.",
_clientKeysByHash.Count,
_databasePath);
}
private SqliteConnection OpenConnection()
{
var connection = new SqliteConnection(_connectionString);
connection.Open();
return connection;
}
private static GroupClientKeepalivePolicy? ReadKeepalivePolicy(SqliteDataReader reader, int instancesOrdinal, int maxParallelismOrdinal, int headroomOrdinal)
{
if (reader.IsDBNull(instancesOrdinal) || reader.IsDBNull(maxParallelismOrdinal) || reader.IsDBNull(headroomOrdinal))
{
return null;
}
return NormalizeKeepalivePolicy(
reader.GetInt32(instancesOrdinal),
reader.GetInt32(maxParallelismOrdinal),
reader.GetInt32(headroomOrdinal));
}
private static GroupClientKeepalivePolicy NormalizeKeepalivePolicy(
int? instancesToKeepAlive,
int? maxParallelismPerClient,
int? parallelismHeadroom)
{
return new GroupClientKeepalivePolicy(
Math.Max(1, instancesToKeepAlive ?? GroupClientKeepalivePolicy.Default.InstancesToKeepAlive),
Math.Max(1, maxParallelismPerClient ?? GroupClientKeepalivePolicy.Default.MaxParallelismPerClient),
Math.Max(1, parallelismHeadroom ?? GroupClientKeepalivePolicy.Default.ParallelismHeadroom));
}
private void EnsureAvailable()
{
if (!_isAvailable)
{
throw new InvalidOperationException(_lastError ?? "The management database is not available.");
}
}
private static DateTimeOffset? ReadNullableDateTimeOffset(SqliteDataReader reader, int ordinal) =>
reader.IsDBNull(ordinal) ? null : ReadDateTimeOffset(reader.GetString(ordinal));
private static DateTimeOffset ReadDateTimeOffset(string value) =>
DateTimeOffset.TryParse(value, out var parsed) ? parsed : DateTimeOffset.MinValue;
private static string GenerateKey()
{
var bytes = RandomNumberGenerator.GetBytes(32);
return $"rl_{Base64UrlEncode(bytes)}";
}
private static string HashKey(string key) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))).ToLowerInvariant();
private static string GetKeyPrefix(string key) =>
key.Length <= 12 ? key : key[..12];
private static string Base64UrlEncode(byte[] bytes) =>
Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
private static string ResolveDatabasePath(string? configuredPath)
{
if (!string.IsNullOrWhiteSpace(configuredPath))
{
var expanded = Environment.ExpandEnvironmentVariables(configuredPath);
return Path.IsPathRooted(expanded)
? expanded
: Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, expanded));
}
return Path.Combine(AppContext.BaseDirectory, "App_Data", "management.sqlite");
}
private sealed class KeyState
{
public string Id { get; init; } = "";
public string Name { get; init; } = "";
public string KeyHash { get; init; } = "";
public string KeyPrefix { get; init; } = "";
public DateTimeOffset CreatedAtUtc { get; init; }
public DateTimeOffset? LastUsedUtc { get; set; }
}
}
internal sealed record ClientAccess(
bool IsDisabled,
DateTimeOffset? DisabledUntilUtc,
bool DisabledManually,
string? DisabledReason)
{
public static ClientAccess Enabled { get; } = new(false, null, false, null);
}
internal sealed record UserKeyInfo(
string Id,
string Name,
string KeyPrefix,
DateTimeOffset CreatedAtUtc,
DateTimeOffset? LastUsedUtc);
internal sealed record CreatedUserKey(
string Id,
string Name,
string KeyPrefix,
DateTimeOffset CreatedAtUtc,
string Key);
internal sealed record RequestMetric(
string ClientId,
string? Model,
string Method,
string Path,
int? StatusCode,
int PromptTokens,
int CompletionTokens,
int TokenCount,
string? UserKeyId,
double Cost,
DateTimeOffset StartedAtUtc,
DateTimeOffset CompletedAtUtc,
TimeSpan Duration);
internal sealed record ClientRequestStats(
long Total,
long Last10Minutes,
long LastHour);
internal sealed record ModelUsageStats(
long TotalRequests,
long RequestsLast10Minutes,
long RequestsLastHour,
long TokensLast10Minutes,
long TokensLastHour);
internal sealed record GroupBillingInfo(
string GroupId,
string Currency,
double DefaultRatePer1k,
double RefuseBelowBalance,
bool Enabled,
DateTimeOffset CreatedAtUtc,
DateTimeOffset UpdatedAtUtc);
internal sealed record GroupBillingRule(
long Id,
string GroupId,
string ModelRegex,
double RatePer1k,
DateTimeOffset CreatedAtUtc);
internal sealed record GroupPayment(
long Id,
string GroupId,
double Amount,
string? Description,
DateTimeOffset CreatedAtUtc,
string? CreatedBy);
internal sealed record GroupBalance(
string GroupId,
string Currency,
double TotalPayments,
double TotalCosts,
double Balance);
internal sealed record TokenStatsByModel(
string Model,
long PromptTokens,
long CompletionTokens,
long TotalTokens,
long Requests);
internal sealed record TokenStatsByClient(
string ClientId,
long PromptTokens,
long CompletionTokens,
long TotalTokens,
long Requests);
internal sealed record TokenStatsByUserKey(
string UserKeyId,
string UserKeyName,
string UserKeyPrefix,
long PromptTokens,
long CompletionTokens,
long TotalTokens,
long Requests);
internal sealed record TokenStatsByGroup(
string GroupId,
string GroupName,
long PromptTokens,
long CompletionTokens,
long TotalTokens,
long Requests);
internal sealed record ClientRevenue(
string ClientId,
double Revenue,
string Currency);
internal sealed record GroupInfo(
string Id,
string Name,
DateTimeOffset CreatedAtUtc,
DateTimeOffset UpdatedAtUtc);
internal sealed record GroupClientInfo(
long Id,
string GroupId,
string? ClientId,
string? Model,
string? ClientPattern,
GroupClientKeepalivePolicy? KeepalivePolicy = null);
internal sealed record GroupClientKeepalivePolicy(
int InstancesToKeepAlive,
int MaxParallelismPerClient,
int ParallelismHeadroom)
{
public static GroupClientKeepalivePolicy Default { get; } = new(1, 1, 1);
}
internal sealed record UserKeyGroupInfo(
string UserKeyId,
string UserKeyName,
string UserKeyPrefix,
IReadOnlyList<string> GroupIds,
IReadOnlyList<string> GroupNames);
internal sealed class GroupAccess
{
public static GroupAccess Unrestricted { get; } = new(new HashSet<string>(), new HashSet<string>(), isUnrestricted: true);
public static GroupAccess Empty { get; } = new(new HashSet<string>(), new HashSet<string>());
public IReadOnlySet<string> ClientModels { get; }
public IReadOnlySet<string> AllClients { get; }
public bool IsUnrestricted { get; }
public GroupAccess(IReadOnlySet<string> clientModels, IReadOnlySet<string> allClients, bool isUnrestricted = false)
{
ClientModels = clientModels;
AllClients = allClients;
IsUnrestricted = isUnrestricted;
}
public bool IsClientAllowed(string clientId) =>
IsUnrestricted || AllClients.Contains(clientId);
public bool IsClientModelAllowed(string clientId, string model) =>
IsUnrestricted
|| AllClients.Contains(clientId)
|| ClientModels.Contains($"{clientId}:{model}");
}