save progress

This commit is contained in:
StellaOps Bot
2026-01-03 00:47:24 +02:00
parent 3f197814c5
commit ca578801fd
319 changed files with 32478 additions and 2202 deletions

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -83,6 +84,11 @@ public sealed class LlmInferenceCacheOptions
/// </summary>
public int MaxContentLength { get; set; } = 100_000;
/// <summary>
/// Maximum number of cache entries (0 = unlimited).
/// </summary>
public int MaxEntries { get; set; }
/// <summary>
/// Whether to use sliding expiration.
/// </summary>
@@ -194,7 +200,8 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
{
if (_cache.TryGetValue(key, out var entry))
{
if (entry.ExpiresAt > _timeProvider.GetUtcNow())
var now = _timeProvider.GetUtcNow();
if (entry.ExpiresAt > now)
{
Interlocked.Increment(ref _hits);
Interlocked.Add(ref _tokensSaved, entry.Result.OutputTokens ?? 0);
@@ -202,7 +209,8 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
// Update access time for sliding expiration
if (_options.SlidingExpiration)
{
entry.AccessedAt = _timeProvider.GetUtcNow();
entry.AccessedAt = now;
entry.ExpiresAt = ApplySlidingExpiration(entry, now);
}
_logger.LogDebug("Cache hit for key {Key}", key);
@@ -246,6 +254,11 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
var key = ComputeCacheKey(request, providerId);
var ttl = result.Deterministic ? _options.DefaultTtl : _options.ShortTtl;
ttl = ClampTtl(ttl);
if (ttl <= TimeSpan.Zero)
{
return Task.CompletedTask;
}
var now = _timeProvider.GetUtcNow();
var entry = new CacheEntry
@@ -253,12 +266,14 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
Result = result,
CreatedAt = now,
AccessedAt = now,
ExpiresAt = now.Add(ttl)
ExpiresAt = now.Add(ttl),
Ttl = ttl
};
lock (_lock)
{
_cache[key] = entry;
EnforceCacheLimit();
}
Interlocked.Increment(ref _sets);
@@ -328,7 +343,7 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
// Include temperature and max tokens in key
sb.Append(':');
sb.Append(request.Temperature.ToString("F2"));
sb.Append(request.Temperature.ToString("F2", CultureInfo.InvariantCulture));
sb.Append(':');
sb.Append(request.MaxTokens);
@@ -372,12 +387,75 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
_cleanupTimer.Dispose();
}
private TimeSpan ClampTtl(TimeSpan ttl)
{
if (ttl <= TimeSpan.Zero)
{
return TimeSpan.Zero;
}
if (_options.MaxTtl > TimeSpan.Zero && ttl > _options.MaxTtl)
{
return _options.MaxTtl;
}
return ttl;
}
private DateTimeOffset ApplySlidingExpiration(CacheEntry entry, DateTimeOffset now)
{
if (entry.Ttl <= TimeSpan.Zero)
{
return entry.ExpiresAt;
}
var proposed = now.Add(entry.Ttl);
if (_options.MaxTtl > TimeSpan.Zero)
{
var maxAllowed = entry.CreatedAt.Add(_options.MaxTtl);
if (proposed > maxAllowed)
{
return maxAllowed;
}
}
return proposed;
}
private void EnforceCacheLimit()
{
if (_options.MaxEntries <= 0)
{
return;
}
var removeCount = _cache.Count - _options.MaxEntries;
if (removeCount <= 0)
{
return;
}
var keysToRemove = _cache
.OrderBy(entry => entry.Value.AccessedAt)
.ThenBy(entry => entry.Value.CreatedAt)
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
.Take(removeCount)
.Select(entry => entry.Key)
.ToList();
foreach (var key in keysToRemove)
{
_cache.Remove(key);
}
}
private sealed class CacheEntry
{
public required LlmCompletionResult Result { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset AccessedAt { get; set; }
public DateTimeOffset ExpiresAt { get; init; }
public DateTimeOffset ExpiresAt { get; set; }
public TimeSpan Ttl { get; init; }
}
}

View File

@@ -1,6 +1,8 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace StellaOps.AdvisoryAI.Inference;
@@ -142,6 +144,13 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
private readonly TimeProvider _clock;
public SignedModelBundleManager(TimeProvider? clock = null)
{
_clock = clock ?? TimeProvider.System;
}
public async Task<SigningResult> SignBundleAsync(
string bundlePath,
IModelBundleSigner signer,
@@ -166,11 +175,14 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
var manifestBytes = await File.ReadAllBytesAsync(manifestPath, cancellationToken);
var manifestDigest = ComputeSha256(manifestBytes);
var signedAt = _clock.GetUtcNow();
var signedAtValue = signedAt.ToString("O", CultureInfo.InvariantCulture);
// Create the payload (manifest digest + metadata)
var payload = new
{
manifest_digest = manifestDigest,
signed_at = DateTime.UtcNow.ToString("o"),
signed_at = signedAtValue,
bundle_path = Path.GetFileName(bundlePath)
};
var payloadJson = JsonSerializer.Serialize(payload, JsonOptions);
@@ -182,7 +194,7 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
var signature = await signer.SignAsync(pae, cancellationToken);
var signatureBase64 = Convert.ToBase64String(signature);
var signatureId = $"{signer.CryptoScheme}-{DateTime.UtcNow:yyyyMMddHHmmss}-{manifestDigest[..8]}";
var signatureId = $"{signer.CryptoScheme}-{signedAt.UtcDateTime:yyyyMMddHHmmss}-{manifestDigest[..8]}";
// Create DSSE envelope
var envelope = new ModelBundleSignatureEnvelope
@@ -205,13 +217,13 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
await File.WriteAllTextAsync(envelopePath, envelopeJson, cancellationToken);
// Update manifest with signature info
var manifest = await File.ReadAllTextAsync(manifestPath, cancellationToken);
var manifestObj = JsonSerializer.Deserialize<Dictionary<string, object>>(manifest);
if (manifestObj != null)
var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken);
var manifestNode = JsonNode.Parse(manifestJson);
if (manifestNode is JsonObject manifestObject)
{
manifestObj["signature_id"] = signatureId;
manifestObj["crypto_scheme"] = signer.CryptoScheme;
var updatedManifest = JsonSerializer.Serialize(manifestObj, JsonOptions);
manifestObject["signature_id"] = signatureId;
manifestObject["crypto_scheme"] = signer.CryptoScheme;
var updatedManifest = manifestObject.ToJsonString(JsonOptions);
await File.WriteAllTextAsync(manifestPath, updatedManifest, cancellationToken);
}

View File

@@ -5,7 +5,7 @@
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />

View File

@@ -0,0 +1,10 @@
# Advisory AI Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0017-M | DONE | Maintainability audit for StellaOps.AdvisoryAI. |
| AUDIT-0017-T | DONE | Test coverage audit for StellaOps.AdvisoryAI. |
| AUDIT-0017-A | DONE | Pending approval for changes. |