33using LocalizationManager . Core . Configuration ;
44using LocalizationManager . Core . Translation ;
55using Microsoft . EntityFrameworkCore ;
6- using Microsoft . Extensions . Options ;
76
87namespace 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
4351public 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