feat(ai): enforce local-first routing

Keep external providers behind server consent, task, and prompt-cost gates while persisting actual provider provenance.
This commit is contained in:
cesnimda
2026-08-09 12:30:11 +02:00
parent c3f4a57195
commit 5eb9b3cb96
29 changed files with 967 additions and 145 deletions
+29
View File
@@ -14,6 +14,8 @@ public sealed class AiWorkspaceTests
public string? Next = "## Result\nGenerated suggestion.";
public int Calls;
public string? LastInstruction;
public string? ActualProvider;
public string? FallbackReason;
// The source text the module assembled — what the prompt actually saw.
public string? LastText;
public Task<string?> SummarizeSectionAsync(string instruction, string text, int maxLength = 180, int minLength = 40)
@@ -24,6 +26,17 @@ public sealed class AiWorkspaceTests
return Task.FromResult(Next);
}
public Task<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30) => Task.FromResult(Next);
public async Task<AiGenerationResult?> GenerateSectionWithMetadataAsync(
string instruction,
string text,
int maxLength = 180,
int minLength = 40,
CancellationToken cancellationToken = default)
{
var generated = await SummarizeSectionAsync(instruction, text, maxLength, minLength);
return generated is null ? null : new AiGenerationResult(generated, ActualProvider, "test-model", FallbackReason,
FallbackReason is null ? "local_primary" : "external_fallback");
}
public Task<AiTextExtractionResult?> ExtractTextAsync(Stream stream, string fileName, string? contentType = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task RunProbeAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<AiServiceMetrics> GetMetricsAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
@@ -72,6 +85,22 @@ public sealed class AiWorkspaceTests
Assert.Single(await db.AiInteractions.IgnoreQueryFilters().Where(x => x.JobApplicationId == jobId).ToListAsync());
}
[Fact]
public async Task Generate_records_the_actual_provider_and_fallback_metadata()
{
var (db, svc, ai) = New("user-1");
await using var _ = db;
ai.ActualProvider = "groq";
ai.FallbackReason = "local_circuit_open";
var jobId = await SeedJobAsync(db, "user-1");
var result = await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("job-analysis"), "configured-label", default);
Assert.Equal("groq", result!.Provider);
Assert.Contains("local_circuit_open", result.ResultJson);
Assert.Contains("test-model", result.ResultJson);
}
[Fact]
public async Task Cover_letter_normalizes_an_unknown_mode_and_labels_the_title()
{