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
+23 -3
View File
@@ -134,12 +134,23 @@ public sealed class AiWorkspaceService : IAiWorkspaceService
};
var prompt = $"{instruction} {Guardrail}";
var result = await _ai.SummarizeSectionAsync(prompt, source, max, 120);
AiGenerationResult? generation;
try
{
generation = await _ai.GenerateSectionWithMetadataAsync(prompt, source, max, 120, ct);
}
catch (AiGenerationException)
{
throw new AiUnavailableException("The AI service could not generate this right now. Please try again in a moment.");
}
var result = generation?.Text;
if (string.IsNullOrWhiteSpace(result))
{
throw new AiUnavailableException("The AI service could not generate this right now. Please try again in a moment.");
}
var actualProvider = string.IsNullOrWhiteSpace(generation?.Provider) ? provider : generation.Provider;
var interaction = new AiInteraction
{
OwnerUserId = ownerUserId,
@@ -147,8 +158,17 @@ public sealed class AiWorkspaceService : IAiWorkspaceService
Module = module,
Mode = mode,
Title = title,
Provider = string.IsNullOrWhiteSpace(provider) ? "ai-service" : provider,
ResultJson = JsonSerializer.Serialize(new { text = result.Trim() }, Json),
Provider = string.IsNullOrWhiteSpace(actualProvider) ? "ai-service" : actualProvider,
ResultJson = JsonSerializer.Serialize(new
{
text = result.Trim(),
meta = new
{
model = generation?.Model,
fallbackReason = generation?.FallbackReason,
routeReason = generation?.RouteReason,
},
}, Json),
InputCharacterCount = prompt.Length + source.Length,
OutputCharacterCount = result.Trim().Length,
EstimatedTokenCount = EstimateTokens(prompt.Length + source.Length + result.Trim().Length),