Skip to content

Commit fb6f382

Browse files
committed
Move LRM provider backend configuration to config.json
- Add LrmBackendsConfiguration with config classes for all 10 providers - Update LrmTranslationProvider to read backend settings from config - Add ValidateBackendConfiguration() for startup validation - Fix IOptions<CloudConfiguration> to use direct singleton injection - Update config.sample.json with full backends documentation
1 parent 4798883 commit fb6f382

3 files changed

Lines changed: 372 additions & 50 deletions

File tree

cloud/deploy/config.sample.json

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,55 @@
9393
"lrmProvider": {
9494
"enabled": true,
9595
"enabledBackends": ["mymemory", "lingva"],
96-
"selectionStrategy": "priority"
96+
"selectionStrategy": "priority",
97+
"backends": {
98+
"myMemory": {
99+
"rateLimitPerMinute": 20
100+
},
101+
"lingva": {
102+
"instanceUrl": "https://lingva.ml",
103+
"rateLimitPerMinute": 30
104+
},
105+
"deepL": {
106+
"apiKey": "YOUR_DEEPL_API_KEY",
107+
"useFreeApi": false,
108+
"rateLimitPerMinute": 100
109+
},
110+
"google": {
111+
"apiKey": "YOUR_GOOGLE_CLOUD_API_KEY",
112+
"rateLimitPerMinute": 100
113+
},
114+
"openAI": {
115+
"apiKey": "YOUR_OPENAI_API_KEY",
116+
"model": "gpt-4o-mini",
117+
"rateLimitPerMinute": 60
118+
},
119+
"claude": {
120+
"apiKey": "YOUR_CLAUDE_API_KEY",
121+
"model": "claude-3-5-sonnet-20241022",
122+
"rateLimitPerMinute": 50
123+
},
124+
"azureOpenAI": {
125+
"apiKey": "YOUR_AZURE_OPENAI_API_KEY",
126+
"endpoint": "https://your-resource.openai.azure.com",
127+
"deploymentName": "gpt-4",
128+
"rateLimitPerMinute": 60
129+
},
130+
"azureTranslator": {
131+
"apiKey": "YOUR_AZURE_TRANSLATOR_API_KEY",
132+
"region": "westus",
133+
"rateLimitPerMinute": 100
134+
},
135+
"libreTranslate": {
136+
"apiKey": "",
137+
"instanceUrl": "https://libretranslate.com",
138+
"rateLimitPerMinute": 30
139+
},
140+
"ollama": {
141+
"apiUrl": "http://localhost:11434",
142+
"model": "llama3.2",
143+
"rateLimitPerMinute": 10
144+
}
145+
}
97146
}
98147
}

cloud/src/LrmCloud.Api/Services/Translation/LrmTranslationProvider.cs

Lines changed: 198 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using LocalizationManager.Core.Configuration;
44
using LocalizationManager.Core.Translation;
55
using Microsoft.EntityFrameworkCore;
6-
using Microsoft.Extensions.Options;
76

87
namespace LrmCloud.Api.Services.Translation;
98

@@ -16,9 +15,15 @@ public interface ILrmTranslationProvider
1615
{
1716
/// <summary>
1817
/// Translate text using LRM managed backends.
18+
/// Note: Usage tracking is handled by CloudTranslationService, not here.
1919
/// </summary>
20+
/// <param name="billableUserId">The user whose quota to check (org owner for org projects).</param>
21+
/// <param name="sourceText">Text to translate.</param>
22+
/// <param name="sourceLanguage">Source language code.</param>
23+
/// <param name="targetLanguage">Target language code.</param>
24+
/// <param name="context">Optional context for AI providers.</param>
2025
Task<LrmTranslationResult> TranslateAsync(
21-
int userId,
26+
int billableUserId,
2227
string sourceText,
2328
string sourceLanguage,
2429
string targetLanguage,
@@ -27,17 +32,20 @@ Task<LrmTranslationResult> TranslateAsync(
2732
/// <summary>
2833
/// Check if LRM provider is available (enabled and user has chars remaining).
2934
/// </summary>
30-
Task<(bool Available, string? Reason)> IsAvailableAsync(int userId);
35+
/// <param name="billableUserId">The user whose quota to check (org owner for org projects).</param>
36+
Task<(bool Available, string? Reason)> IsAvailableAsync(int billableUserId);
3137

3238
/// <summary>
3339
/// Check if user has sufficient LRM chars for the given text.
3440
/// </summary>
35-
Task<bool> HasSufficientCharsAsync(int userId, int charCount);
41+
/// <param name="billableUserId">The user whose quota to check (org owner for org projects).</param>
42+
Task<bool> HasSufficientCharsAsync(int billableUserId, int charCount);
3643

3744
/// <summary>
3845
/// Get the remaining LRM chars for a user.
3946
/// </summary>
40-
Task<int> GetRemainingCharsAsync(int userId);
47+
/// <param name="billableUserId">The user whose quota to check (org owner for org projects).</param>
48+
Task<int> GetRemainingCharsAsync(int billableUserId);
4149
}
4250

4351
public class LrmTranslationResult
@@ -62,18 +70,78 @@ public class LrmTranslationProvider : ILrmTranslationProvider
6270

6371
public LrmTranslationProvider(
6472
AppDbContext db,
65-
IOptions<CloudConfiguration> config,
73+
CloudConfiguration config,
6674
ILogger<LrmTranslationProvider> logger,
6775
IServiceProvider serviceProvider)
6876
{
6977
_db = db;
70-
_config = config.Value;
78+
_config = config;
7179
_logger = logger;
7280
_serviceProvider = serviceProvider;
81+
82+
ValidateBackendConfiguration();
83+
}
84+
85+
/// <summary>
86+
/// Validate that enabled backends have required configuration.
87+
/// Logs warnings for misconfigured backends on startup.
88+
/// </summary>
89+
private void ValidateBackendConfiguration()
90+
{
91+
if (!_config.LrmProvider.Enabled)
92+
{
93+
_logger.LogInformation("LRM provider is disabled");
94+
return;
95+
}
96+
97+
var backends = _config.LrmProvider.Backends;
98+
var enabled = _config.LrmProvider.EnabledBackends;
99+
100+
if (!enabled.Any())
101+
{
102+
_logger.LogWarning("LRM provider is enabled but no backends are configured in EnabledBackends");
103+
return;
104+
}
105+
106+
foreach (var backend in enabled)
107+
{
108+
var (isConfigured, issue) = backend.ToLowerInvariant() switch
109+
{
110+
"mymemory" => (true, null), // Free, always works
111+
"lingva" => (true, null), // Free, always works
112+
"deepl" => (!string.IsNullOrEmpty(backends.DeepL?.ApiKey), "missing ApiKey"),
113+
"google" => (!string.IsNullOrEmpty(backends.Google?.ApiKey), "missing ApiKey"),
114+
"openai" => (!string.IsNullOrEmpty(backends.OpenAI?.ApiKey), "missing ApiKey"),
115+
"claude" => (!string.IsNullOrEmpty(backends.Claude?.ApiKey), "missing ApiKey"),
116+
"azureopenai" => (
117+
!string.IsNullOrEmpty(backends.AzureOpenAI?.ApiKey) && !string.IsNullOrEmpty(backends.AzureOpenAI?.Endpoint),
118+
"missing ApiKey or Endpoint"),
119+
"azuretranslator" => (!string.IsNullOrEmpty(backends.AzureTranslator?.ApiKey), "missing ApiKey"),
120+
"libretranslate" => (true, null), // API key optional
121+
"ollama" => (true, null), // Local, no key needed
122+
_ => (false, "unknown backend")
123+
};
124+
125+
if (!isConfigured)
126+
{
127+
_logger.LogWarning(
128+
"LRM backend '{Backend}' is enabled but not properly configured: {Issue}. " +
129+
"Configure it in LrmProvider.Backends section of config.json",
130+
backend, issue);
131+
}
132+
else
133+
{
134+
_logger.LogDebug("LRM backend '{Backend}' is configured and ready", backend);
135+
}
136+
}
137+
138+
_logger.LogInformation(
139+
"LRM provider initialized with {Count} backend(s): [{Backends}]",
140+
enabled.Count, string.Join(", ", enabled));
73141
}
74142

75143
public async Task<LrmTranslationResult> TranslateAsync(
76-
int userId,
144+
int billableUserId,
77145
string sourceText,
78146
string sourceLanguage,
79147
string targetLanguage,
@@ -88,8 +156,8 @@ public async Task<LrmTranslationResult> TranslateAsync(
88156
return result;
89157
}
90158

91-
// Check user's remaining chars
92-
var remaining = await GetRemainingCharsAsync(userId);
159+
// Check billable user's remaining chars
160+
var remaining = await GetRemainingCharsAsync(billableUserId);
93161
if (remaining < sourceText.Length)
94162
{
95163
result.Error = $"Insufficient LRM translation quota. Need {sourceText.Length} chars, have {remaining} remaining.";
@@ -133,26 +201,24 @@ public async Task<LrmTranslationResult> TranslateAsync(
133201
result.TranslatedText = response.TranslatedText;
134202
result.FromCache = response.FromCache;
135203

136-
// Track usage (decrement user's char balance)
137-
await TrackLrmUsageAsync(userId, sourceText.Length);
138-
204+
// Note: Usage tracking is handled by CloudTranslationService, not here
139205
_logger.LogDebug(
140-
"LRM translation via {Backend}: {Chars} chars for user {UserId}",
141-
backend, sourceText.Length, userId);
206+
"LRM translation via {Backend}: {Chars} chars (billable to user {BillableUserId})",
207+
backend, sourceText.Length, billableUserId);
142208

143209
return result; // Success - return immediately
144210
}
145211
catch (Exception ex)
146212
{
147-
_logger.LogWarning(ex, "LRM backend {Backend} failed, user {UserId}, trying next", backend, userId);
213+
_logger.LogWarning(ex, "LRM backend {Backend} failed (billable user {BillableUserId}), trying next", backend, billableUserId);
148214
errors.Add($"{backend}: {ex.Message}");
149215
// Continue to next backend
150216
}
151217
}
152218

153219
// All backends failed
154220
result.Error = $"All LRM backends failed: {string.Join("; ", errors)}";
155-
_logger.LogError("All LRM backends failed for user {UserId}: {Errors}", userId, result.Error);
221+
_logger.LogError("All LRM backends failed (billable user {BillableUserId}): {Errors}", billableUserId, result.Error);
156222

157223
return result;
158224
}
@@ -267,49 +333,132 @@ private void ApplyPlatformConfig(ConfigurationModel config, string backend)
267333
config.Translation.ApiKeys ??= new TranslationApiKeys();
268334
config.Translation.AIProviders ??= new AIProviderConfiguration();
269335

270-
// Platform API keys from master secret (if configured)
271-
// These are the keys WE own for the LRM service
272-
var masterSecret = _config.ApiKeyMasterSecret;
336+
var backends = _config.LrmProvider.Backends;
273337

274338
switch (backend.ToLowerInvariant())
275339
{
340+
case "mymemory":
341+
var mm = backends.MyMemory ?? new LrmMyMemoryConfig();
342+
config.Translation.AIProviders.MyMemory = new MyMemorySettings
343+
{
344+
RateLimitPerMinute = mm.RateLimitPerMinute
345+
};
346+
break;
347+
276348
case "lingva":
277-
// Lingva is free, no API key needed
278-
config.Translation.AIProviders.Lingva = new LingvaSettings();
349+
var lingva = backends.Lingva ?? new LrmLingvaConfig();
350+
config.Translation.AIProviders.Lingva = new LingvaSettings
351+
{
352+
InstanceUrl = lingva.InstanceUrl,
353+
RateLimitPerMinute = lingva.RateLimitPerMinute
354+
};
279355
break;
280356

281-
case "mymemory":
282-
// MyMemory is free, no API key needed
283-
config.Translation.AIProviders.MyMemory = new MyMemorySettings();
357+
case "deepl":
358+
var deepl = backends.DeepL;
359+
if (string.IsNullOrEmpty(deepl?.ApiKey))
360+
{
361+
_logger.LogWarning("DeepL backend enabled but no API key configured in LrmProvider.Backends.DeepL");
362+
break;
363+
}
364+
config.Translation.ApiKeys.DeepL = deepl.ApiKey;
284365
break;
285366

286-
// Future: Add platform-owned API keys for paid backends
287-
// case "deepl":
288-
// config.Translation.ApiKeys.DeepL = GetPlatformApiKey("deepl");
289-
// break;
290-
}
291-
}
367+
case "google":
368+
var google = backends.Google;
369+
if (string.IsNullOrEmpty(google?.ApiKey))
370+
{
371+
_logger.LogWarning("Google backend enabled but no API key configured in LrmProvider.Backends.Google");
372+
break;
373+
}
374+
config.Translation.ApiKeys.Google = google.ApiKey;
375+
break;
292376

293-
private async Task TrackLrmUsageAsync(int userId, int charsUsed)
294-
{
295-
var user = await _db.Users.FindAsync(userId);
296-
if (user == null) return;
377+
case "openai":
378+
var openai = backends.OpenAI;
379+
if (string.IsNullOrEmpty(openai?.ApiKey))
380+
{
381+
_logger.LogWarning("OpenAI backend enabled but no API key configured in LrmProvider.Backends.OpenAI");
382+
break;
383+
}
384+
config.Translation.ApiKeys.OpenAI = openai.ApiKey;
385+
config.Translation.AIProviders.OpenAI = new OpenAISettings
386+
{
387+
Model = openai.Model,
388+
CustomSystemPrompt = openai.CustomSystemPrompt,
389+
RateLimitPerMinute = openai.RateLimitPerMinute
390+
};
391+
break;
297392

298-
user.TranslationCharsUsed += charsUsed;
299-
user.UpdatedAt = DateTime.UtcNow;
393+
case "claude":
394+
var claude = backends.Claude;
395+
if (string.IsNullOrEmpty(claude?.ApiKey))
396+
{
397+
_logger.LogWarning("Claude backend enabled but no API key configured in LrmProvider.Backends.Claude");
398+
break;
399+
}
400+
config.Translation.ApiKeys.Claude = claude.ApiKey;
401+
config.Translation.AIProviders.Claude = new ClaudeSettings
402+
{
403+
Model = claude.Model,
404+
CustomSystemPrompt = claude.CustomSystemPrompt,
405+
RateLimitPerMinute = claude.RateLimitPerMinute
406+
};
407+
break;
300408

301-
// Reset usage if reset date has passed
302-
if (user.TranslationCharsResetAt.HasValue && user.TranslationCharsResetAt < DateTime.UtcNow)
303-
{
304-
user.TranslationCharsUsed = charsUsed; // Reset and add current
305-
user.TranslationCharsResetAt = DateTime.UtcNow.AddMonths(1);
306-
}
307-
else if (!user.TranslationCharsResetAt.HasValue)
308-
{
309-
// Set initial reset date
310-
user.TranslationCharsResetAt = DateTime.UtcNow.AddMonths(1);
311-
}
409+
case "azureopenai":
410+
var azureOai = backends.AzureOpenAI;
411+
if (string.IsNullOrEmpty(azureOai?.ApiKey) || string.IsNullOrEmpty(azureOai?.Endpoint))
412+
{
413+
_logger.LogWarning("AzureOpenAI backend enabled but missing ApiKey or Endpoint in LrmProvider.Backends.AzureOpenAI");
414+
break;
415+
}
416+
config.Translation.ApiKeys.AzureOpenAI = azureOai.ApiKey;
417+
config.Translation.AIProviders.AzureOpenAI = new AzureOpenAISettings
418+
{
419+
Endpoint = azureOai.Endpoint,
420+
DeploymentName = azureOai.DeploymentName,
421+
CustomSystemPrompt = azureOai.CustomSystemPrompt,
422+
RateLimitPerMinute = azureOai.RateLimitPerMinute
423+
};
424+
break;
425+
426+
case "azuretranslator":
427+
var azureTr = backends.AzureTranslator;
428+
if (string.IsNullOrEmpty(azureTr?.ApiKey))
429+
{
430+
_logger.LogWarning("AzureTranslator backend enabled but no API key configured in LrmProvider.Backends.AzureTranslator");
431+
break;
432+
}
433+
config.Translation.ApiKeys.AzureTranslator = azureTr.ApiKey;
434+
config.Translation.AIProviders.AzureTranslator = new AzureTranslatorSettings
435+
{
436+
Region = azureTr.Region,
437+
Endpoint = azureTr.Endpoint,
438+
RateLimitPerMinute = azureTr.RateLimitPerMinute
439+
};
440+
break;
312441

313-
await _db.SaveChangesAsync();
442+
case "libretranslate":
443+
var libre = backends.LibreTranslate ?? new LrmLibreTranslateConfig();
444+
config.Translation.ApiKeys.LibreTranslate = libre.ApiKey;
445+
break;
446+
447+
case "ollama":
448+
var ollama = backends.Ollama ?? new LrmOllamaConfig();
449+
config.Translation.AIProviders.Ollama = new OllamaSettings
450+
{
451+
ApiUrl = ollama.ApiUrl,
452+
Model = ollama.Model,
453+
CustomSystemPrompt = ollama.CustomSystemPrompt,
454+
RateLimitPerMinute = ollama.RateLimitPerMinute
455+
};
456+
break;
457+
458+
default:
459+
_logger.LogWarning("Unknown LRM backend: {Backend}", backend);
460+
break;
461+
}
314462
}
463+
315464
}

0 commit comments

Comments
 (0)