locai 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +23 -25
  2. package/dist/cli/helpers.d.ts +34 -2
  3. package/dist/cli/helpers.js +96 -37
  4. package/dist/cli.js +40 -39
  5. package/dist/commands/review.d.ts +33 -8
  6. package/dist/commands/review.js +89 -22
  7. package/dist/commands/translator.d.ts +4 -2
  8. package/dist/commands/translator.js +1 -1
  9. package/dist/config/index.d.ts +53 -66
  10. package/dist/config/index.js +2 -1
  11. package/dist/config/setup.d.ts +3 -2
  12. package/dist/config/setup.js +5 -3
  13. package/dist/core/adapters/android-adapter.d.ts +3 -2
  14. package/dist/core/adapters/android-adapter.js +12 -13
  15. package/dist/core/adapters/base-adapter.d.ts +13 -2
  16. package/dist/core/adapters/ios-adapter.d.ts +3 -2
  17. package/dist/core/adapters/json-adapter.d.ts +4 -3
  18. package/dist/core/adapters/json-adapter.js +1 -3
  19. package/dist/core/adapters/po-adapter.d.ts +3 -2
  20. package/dist/core/adapters/po-adapter.js +4 -5
  21. package/dist/core/adapters/properties-adapter.d.ts +3 -2
  22. package/dist/core/adapters/properties-adapter.js +2 -4
  23. package/dist/core/adapters/yaml-adapter.d.ts +4 -3
  24. package/dist/core/adapters/yaml-adapter.js +1 -3
  25. package/dist/core/context-processor.d.ts +21 -6
  26. package/dist/core/context-processor.js +2 -7
  27. package/dist/core/detection/framework-detector.js +1 -1
  28. package/dist/core/detection/structure-detector.js +1 -1
  29. package/dist/core/fallback-provider.d.ts +3 -4
  30. package/dist/core/fallback-provider.js +15 -21
  31. package/dist/core/orchestrator.d.ts +98 -26
  32. package/dist/core/orchestrator.js +56 -116
  33. package/dist/core/pipeline/Pipeline.d.ts +2 -2
  34. package/dist/core/pipeline/Pipeline.js +1 -3
  35. package/dist/core/pipeline/context.d.ts +69 -13
  36. package/dist/core/pipeline/context.js +1 -7
  37. package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
  38. package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
  39. package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
  40. package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
  41. package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
  42. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
  43. package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
  44. package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
  45. package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
  46. package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
  47. package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
  48. package/dist/core/pipeline/steps/TranslationStep.js +21 -23
  49. package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
  50. package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
  51. package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
  52. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
  53. package/dist/core/provider-factory.d.ts +15 -9
  54. package/dist/core/provider-factory.js +42 -32
  55. package/dist/providers/anthropic.d.ts +5 -4
  56. package/dist/providers/anthropic.js +3 -2
  57. package/dist/providers/api-types.d.ts +74 -0
  58. package/dist/providers/api-types.js +5 -0
  59. package/dist/providers/base-provider.d.ts +27 -31
  60. package/dist/providers/base-provider.js +22 -46
  61. package/dist/providers/dashscope.js +0 -2
  62. package/dist/providers/deepseek.js +0 -1
  63. package/dist/providers/gemini.js +0 -1
  64. package/dist/providers/openai.d.ts +3 -3
  65. package/dist/providers/openai.js +0 -1
  66. package/dist/providers/xai.js +0 -1
  67. package/dist/services/embedding-provider.js +4 -2
  68. package/dist/services/language-processor.d.ts +5 -3
  69. package/dist/services/language-processor.js +38 -43
  70. package/dist/services/source-analyzer.d.ts +24 -0
  71. package/dist/services/source-analyzer.js +161 -68
  72. package/dist/services/translation-service.d.ts +31 -13
  73. package/dist/services/translation-service.js +116 -71
  74. package/dist/services/vector-store.d.ts +15 -2
  75. package/dist/services/vector-store.js +14 -10
  76. package/dist/types/index.d.ts +31 -33
  77. package/dist/utils/ai-context-analyzer.d.ts +12 -3
  78. package/dist/utils/ai-context-analyzer.js +18 -10
  79. package/dist/utils/confidence-scorer.d.ts +16 -19
  80. package/dist/utils/confidence-scorer.js +36 -125
  81. package/dist/utils/error-helper.d.ts +27 -6
  82. package/dist/utils/error-helper.js +10 -9
  83. package/dist/utils/file-manager.d.ts +8 -7
  84. package/dist/utils/file-manager.js +22 -23
  85. package/dist/utils/glossary-manager.d.ts +22 -3
  86. package/dist/utils/glossary-manager.js +47 -28
  87. package/dist/utils/graceful-shutdown.d.ts +7 -1
  88. package/dist/utils/graceful-shutdown.js +1 -2
  89. package/dist/utils/input-validator.d.ts +53 -66
  90. package/dist/utils/input-validator.js +100 -151
  91. package/dist/utils/locale-paths.d.ts +7 -0
  92. package/dist/utils/locale-paths.js +16 -0
  93. package/dist/utils/logger.d.ts +8 -8
  94. package/dist/utils/logger.js +7 -15
  95. package/dist/utils/object-transformer.d.ts +20 -5
  96. package/dist/utils/object-transformer.js +20 -28
  97. package/dist/utils/progress-tracker.js +2 -3
  98. package/dist/utils/prompt-templates.d.ts +45 -5
  99. package/dist/utils/prompt-templates.js +61 -5
  100. package/dist/utils/quality/base-checker.d.ts +40 -18
  101. package/dist/utils/quality/base-checker.js +1 -2
  102. package/dist/utils/quality/code-block-checker.d.ts +13 -5
  103. package/dist/utils/quality/index.d.ts +2 -1
  104. package/dist/utils/quality/index.js +8 -27
  105. package/dist/utils/quality/length-checker.d.ts +35 -5
  106. package/dist/utils/quality/length-checker.js +2 -3
  107. package/dist/utils/quality/markdown-checker.d.ts +14 -7
  108. package/dist/utils/quality/special-characters-checker.d.ts +18 -1
  109. package/dist/utils/quality/special-characters-checker.js +0 -1
  110. package/dist/utils/quality/style-guide-checker.d.ts +1 -1
  111. package/dist/utils/quality/style-guide-checker.js +0 -2
  112. package/dist/utils/quality/tone-verifier.js +1 -1
  113. package/dist/utils/rate-limiter.d.ts +17 -1
  114. package/dist/utils/rate-limiter.js +12 -11
  115. package/dist/utils/retry-helper.d.ts +21 -3
  116. package/dist/utils/retry-helper.js +11 -3
  117. package/dist/utils/review-queue.d.ts +45 -0
  118. package/dist/utils/review-queue.js +53 -0
  119. package/dist/utils/state-manager.d.ts +12 -3
  120. package/dist/utils/state-manager.js +32 -7
  121. package/dist/utils/statistics-manager.d.ts +11 -1
  122. package/dist/utils/statistics-manager.js +8 -0
  123. package/dist/utils/ui-manager.d.ts +2 -1
  124. package/dist/utils/ui-manager.js +15 -1
  125. package/package.json +22 -17
  126. package/dist/commands/interactive-review.d.ts +0 -9
  127. package/dist/commands/interactive-review.js +0 -109
  128. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
  129. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
  130. package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
  131. package/dist/utils/quality/__tests__/tone-verifier.test.js +0 -91
@@ -14,26 +14,15 @@ export default class ContextEnrichmentStep extends PipelineStep {
14
14
  await this.analyzer.ensureInitialized();
15
15
  const codeContext = this.analyzer.getContext(context.key);
16
16
  if (codeContext) {
17
- // Enrich the context meta
17
+ // TranslationStep forwards meta to the provider as `detectedContext`,
18
+ // and the prompt templates render `code` (see prompt-templates.ts).
18
19
  context.meta = {
19
20
  ...context.meta,
20
21
  code: codeContext,
21
22
  };
22
- // Also append to the AI prompt context if it exists
23
- // (Assuming existing translation prompt uses contextData)
24
- context.contextData = {
25
- ...(context.contextData || {}),
26
- visualContext: {
27
- component: codeContext.component,
28
- file: codeContext.filePath,
29
- comments: codeContext.comments,
30
- props: codeContext.props,
31
- snippet: codeContext.usageSnippet,
32
- },
33
- };
34
23
  }
35
24
  }
36
- catch (error) {
25
+ catch {
37
26
  // Non-blocking error
38
27
  // console.warn(`Context enrichment failed for ${context.key}`, error);
39
28
  }
@@ -4,7 +4,7 @@ import QualityChecker from "../../../utils/quality/index.js";
4
4
  declare class QualityCheckStep extends PipelineStep {
5
5
  private qualityChecker;
6
6
  private debug;
7
- constructor(qualityChecker: QualityChecker | any, debug?: boolean);
7
+ constructor(qualityChecker: QualityChecker | null, debug?: boolean);
8
8
  execute(context: TranslationContext, next: () => Promise<void>): Promise<void>;
9
9
  }
10
10
  export default QualityCheckStep;
@@ -18,7 +18,6 @@ class QualityCheckStep extends PipelineStep {
18
18
  if (this.qualityChecker && this.qualityChecker.rules?.enabled !== false) {
19
19
  const qualityResult = this.qualityChecker.validateAndFix(context.sourceText, context.translatedText);
20
20
  translated = qualityResult.fixedText;
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
21
  context.qualityResult = qualityResult;
23
22
  }
24
23
  // 2. JSON Validity Check
@@ -39,7 +38,6 @@ class QualityCheckStep extends PipelineStep {
39
38
  translated: context.translatedText,
40
39
  context: context.meta,
41
40
  success: true,
42
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
41
  qualityChecks: context.qualityResult,
44
42
  };
45
43
  await next();
@@ -23,7 +23,7 @@ export class ToneCheckStep extends PipelineStep {
23
23
  const styleGuide = context.options.styleGuide;
24
24
  const provider = styleGuide.toneProvider;
25
25
  const options = styleGuide.analysisOptions;
26
- const result = await this.verifier.verify(context.sourceText, context.translatedText, context.sourceLang, context.targetLang, styleGuide.toneOfVoice, provider, options);
26
+ const result = await this.verifier.verify(context.sourceText, context.translatedText, context.sourceLang, context.targetLang, context.options.styleGuide.toneOfVoice, provider, options);
27
27
  if (!result.passed) {
28
28
  if (this.debug) {
29
29
  console.warn(`[ToneCheck] Failed: Score ${result.score}/10. Reason: ${result.reasoning}`);
@@ -1,21 +1,31 @@
1
1
  import PipelineStep from "../PipelineStep.js";
2
2
  import { AIProvider } from "../../provider-factory.js";
3
3
  import { TranslationContext } from "../context.js";
4
+ import type { TranslateOptions } from "../../../providers/base-provider.js";
5
+ import type { ReviewQueueItem, TranslationRef } from "../../../utils/review-queue.js";
4
6
  export interface ConfidenceSettings {
5
7
  enabled: boolean;
6
8
  autoApproveThreshold: number;
7
9
  rejectThreshold: number;
8
10
  reviewThreshold: number;
9
11
  saveReviewQueue: boolean;
10
- reviewQueue: any[];
12
+ /** Translations scored below reviewThreshold in this run, rejected ones included. */
13
+ reviewQueue: ReviewQueueItem[];
14
+ /** Every translation scored in this run. */
15
+ scored: TranslationRef[];
11
16
  autoApprovedCount: number;
12
17
  rejectedCount: number;
13
- [key: string]: any;
18
+ }
19
+ /** Options TranslationStep reads; the whole object is also forwarded to the provider. */
20
+ export interface TranslationStepOptions extends TranslateOptions {
21
+ apiProvider?: string;
22
+ useFallback?: boolean;
23
+ fallbackOrder?: string[];
14
24
  }
15
25
  declare class TranslationStep extends PipelineStep {
16
26
  private options;
17
27
  private confidenceSettings;
18
- constructor(options?: any, confidenceSettings?: Partial<ConfidenceSettings>);
28
+ constructor(options?: TranslationStepOptions, confidenceSettings?: Partial<ConfidenceSettings>);
19
29
  execute(context: TranslationContext, next: () => Promise<void>): Promise<void>;
20
30
  _getProvider(): AIProvider;
21
31
  }
@@ -1,12 +1,10 @@
1
1
  import PipelineStep from "../PipelineStep.js";
2
2
  import ProviderFactory from "../../provider-factory.js";
3
+ import ConfidenceScorer from "../../../utils/confidence-scorer.js";
3
4
  class TranslationStep extends PipelineStep {
4
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
5
  options;
6
6
  confidenceSettings;
7
- constructor(
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- options = {}, confidenceSettings = {}) {
7
+ constructor(options = {}, confidenceSettings = {}) {
10
8
  super();
11
9
  this.options = options;
12
10
  this.confidenceSettings = confidenceSettings;
@@ -14,6 +12,7 @@ class TranslationStep extends PipelineStep {
14
12
  async execute(context, next) {
15
13
  try {
16
14
  const provider = this._getProvider();
15
+ const text = context.protectedText || context.sourceText;
17
16
  // Setup translation options
18
17
  const translationOpts = {
19
18
  ...this.options,
@@ -22,29 +21,29 @@ class TranslationStep extends PipelineStep {
22
21
  existingTranslation: context.existingTranslation || null,
23
22
  },
24
23
  };
25
- // Handling Confidence Scoring if enabled
26
- if (this.confidenceSettings.enabled &&
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- typeof provider.extractTranslationWithConfidence === "function") {
29
- const rawResponse = await provider.translate(context.protectedText || context.sourceText, context.sourceLang, context.targetLang, { ...translationOpts, returnRawResponse: true });
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- const result = provider.extractTranslationWithConfidence(rawResponse,
32
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
- provider.name || "unknown", context.protectedText || context.sourceText, context.sourceLang, context.targetLang, context.meta?.category);
34
- context.translatedText = result.translation;
35
- context.confidence = result.confidence;
36
- }
37
- else {
38
- // Standard translation
39
- context.translatedText = await provider.translate(context.protectedText || context.sourceText, context.sourceLang, context.targetLang, translationOpts);
40
- context.confidence = undefined; // Use undefined instead of null for optional typings often
24
+ context.translatedText = await provider.translate(text, context.sourceLang, context.targetLang, translationOpts);
25
+ // Score what the provider returned, before glossary restoration and quality fixes
26
+ if (this.confidenceSettings.enabled) {
27
+ context.confidence = ConfidenceScorer.calculateConfidence({
28
+ sourceText: text,
29
+ translation: context.translatedText,
30
+ sourceLang: context.sourceLang,
31
+ targetLang: context.targetLang,
32
+ provider: this.options.apiProvider?.toLowerCase() || "unknown",
33
+ category: context.meta?.category,
34
+ });
41
35
  }
42
36
  // Continue to next steps (restore glossary, quality check, etc.)
43
37
  await next();
44
38
  }
45
39
  catch (error) {
46
- console.error(`Translation error - key "${context.key}":`, error);
47
- context.result.error = error.message;
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ console.error(`Translation error - key "${context.key}": ${message}`);
42
+ // The stack is for debugging LocAI itself; show it only with --debug
43
+ if (process.env.DEBUG === "true" && error instanceof Error && error.stack) {
44
+ console.error(error.stack);
45
+ }
46
+ context.result.error = message;
48
47
  context.result.success = false;
49
48
  context.result.translated = context.sourceText; // Fallback to source
50
49
  // Stop pipeline or proceed?
@@ -55,7 +54,6 @@ class TranslationStep extends PipelineStep {
55
54
  }
56
55
  _getProvider() {
57
56
  const provider = ProviderFactory.getProvider(this.options.apiProvider, this.options.useFallback !== false, this.options);
58
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
57
  if (!provider || typeof provider.translate !== "function") {
60
58
  throw new Error(`Translation provider not available or invalid: ${this.options.apiProvider}`);
61
59
  }
@@ -7,11 +7,19 @@ export interface VectorMemoryStats {
7
7
  contextUsed: number;
8
8
  misses: number;
9
9
  }
10
+ /** Vector memory settings read by the vector cache steps. */
11
+ export interface VectorCacheOptions {
12
+ enabled?: boolean;
13
+ /** Similarity at which a stored translation is reused as-is (default 0.98). */
14
+ exactMatchThreshold?: number;
15
+ /** Similarity at which a stored translation is offered to the provider as reference (default 0.85). */
16
+ similarityThreshold?: number;
17
+ }
10
18
  export declare class VectorCacheReadStep extends PipelineStep {
11
19
  private vectorStore;
12
20
  private embeddingProvider;
13
21
  private stats;
14
22
  private options;
15
- constructor(vectorStore: VectorStore, embeddingProvider: EmbeddingProvider, stats: VectorMemoryStats, options?: any);
23
+ constructor(vectorStore: VectorStore, embeddingProvider: EmbeddingProvider, stats: VectorMemoryStats, options?: VectorCacheOptions);
16
24
  execute(context: TranslationContext, next: () => Promise<void>): Promise<void>;
17
25
  }
@@ -13,10 +13,7 @@ export class VectorCacheReadStep extends PipelineStep {
13
13
  }
14
14
  async execute(context, next) {
15
15
  // Skip if disabled, or already has result (Exact Cache hit)
16
- if (!this.options.enabled ||
17
- context.result?.success ||
18
- context.result?.fromCache ||
19
- context.options?.skipCache) {
16
+ if (!this.options.enabled || context.result?.success || context.result?.fromCache) {
20
17
  await next();
21
18
  return;
22
19
  }
@@ -76,7 +73,8 @@ export class VectorCacheReadStep extends PipelineStep {
76
73
  }
77
74
  }
78
75
  catch (error) {
79
- console.warn(`Vector cache read failed: ${error.message}`);
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ console.warn(`Vector cache read failed: ${message}`);
80
78
  // Continue pipeline on error
81
79
  }
82
80
  await next();
@@ -2,10 +2,11 @@ import PipelineStep from "../PipelineStep.js";
2
2
  import { TranslationContext } from "../context.js";
3
3
  import { VectorStore } from "../../../services/vector-store.js";
4
4
  import { EmbeddingProvider } from "../../../services/embedding-provider.js";
5
+ import { VectorCacheOptions } from "./VectorCacheReadStep.js";
5
6
  export declare class VectorCacheWriteStep extends PipelineStep {
6
7
  private vectorStore;
7
8
  private embeddingProvider;
8
9
  private options;
9
- constructor(vectorStore: VectorStore, embeddingProvider: EmbeddingProvider, options?: any);
10
+ constructor(vectorStore: VectorStore, embeddingProvider: EmbeddingProvider, options?: VectorCacheOptions);
10
11
  execute(context: TranslationContext, next: () => Promise<void>): Promise<void>;
11
12
  }
@@ -36,7 +36,8 @@ export class VectorCacheWriteStep extends PipelineStep {
36
36
  }
37
37
  }
38
38
  catch (error) {
39
- console.warn(`Vector cache write failed: ${error.message}`);
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ console.warn(`Vector cache write failed: ${message}`);
40
41
  }
41
42
  }
42
43
  }
@@ -1,8 +1,12 @@
1
- import { ProviderConfig, TranslateOptions } from "../providers/base-provider.js";
1
+ import { ChatMessage, ProviderConfig, TranslateOptions } from "../providers/base-provider.js";
2
2
  export interface AIProvider {
3
3
  translate(text: string, sourceLang: string, targetLang: string, options?: TranslateOptions): Promise<string>;
4
4
  analyze?(prompt: string, options?: ProviderConfig): Promise<string>;
5
- chat?(messages: any[], options?: ProviderConfig): Promise<string>;
5
+ chat?(messages: ChatMessage[], options?: ProviderConfig): Promise<string>;
6
+ }
7
+ /** No configured provider can translate (no API key); thrown before any per-key work. */
8
+ export declare class NoProviderError extends Error {
9
+ constructor(message: string);
6
10
  }
7
11
  /**
8
12
  * Factory for creating and managing AI providers.
@@ -11,26 +15,28 @@ export interface AIProvider {
11
15
  declare class ProviderFactory {
12
16
  /**
13
17
  * Get provider instance with intelligent fallback support.
14
- * @param {string} providerName - Name of the primary provider.
18
+ * @param {string} [providerName] - Name of the primary provider; omitted → default fallback chain.
15
19
  * @param {boolean} [useFallback=true] - Whether to use fallback chain on failure.
16
20
  * @param {Object} [config=null] - Optional configuration overrides.
17
21
  * @returns {AIProvider} - Provider instance or wrapped FallbackProvider.
18
22
  * @throws {Error} If provider is not found or configured.
19
23
  */
20
- static getProvider(providerName: string, useFallback?: boolean, config?: {
24
+ static getProvider(providerName: string | undefined, useFallback?: boolean, config?: {
21
25
  fallbackOrder?: string[];
22
26
  } | null): AIProvider;
23
27
  /**
24
28
  * Get list of available (configured) providers.
25
- * @returns {string[]} - Array of provider names with valid API keys.
29
+ * @returns {string[]} - Array of provider names with valid API keys, in default chain order.
26
30
  */
27
31
  static getAvailableProviders(): string[];
28
32
  /**
29
- * Validate that at least one provider is configured.
30
- * @returns {string[]} - Array of available providers.
31
- * @throws {Error} If no providers are configured.
33
+ * Fail fast when no provider can translate: with fallback, no provider has an API key;
34
+ * without it, the configured provider has none.
35
+ * @param {string} [providerName] - The configured `apiProvider`.
36
+ * @param {boolean} [useFallback=true] - Whether other providers may be used.
37
+ * @throws {NoProviderError} If no usable provider is configured.
32
38
  */
33
- static validateProviders(): string[];
39
+ static assertProviderAvailable(providerName?: string, useFallback?: boolean): void;
34
40
  /**
35
41
  * Check if a specific provider is configured.
36
42
  * @param {string} providerName - Name of the provider.
@@ -6,6 +6,22 @@ import * as dashscopeProvider from "../providers/dashscope.js";
6
6
  import * as xaiProvider from "../providers/xai.js";
7
7
  import FallbackProvider from "./fallback-provider.js";
8
8
  import rateLimiter from "../utils/rate-limiter.js";
9
+ /** API key environment variable per provider, in the default fallback-chain order. */
10
+ const PROVIDER_API_KEYS = {
11
+ dashscope: "DASHSCOPE_API_KEY",
12
+ xai: "XAI_API_KEY",
13
+ openai: "OPENAI_API_KEY",
14
+ anthropic: "ANTHROPIC_API_KEY",
15
+ deepseek: "DEEPSEEK_API_KEY",
16
+ gemini: "GEMINI_API_KEY",
17
+ };
18
+ /** No configured provider can translate (no API key); thrown before any per-key work. */
19
+ export class NoProviderError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "NoProviderError";
23
+ }
24
+ }
9
25
  /**
10
26
  * Factory for creating and managing AI providers.
11
27
  * Handles provider instantiation, configuration, and fallback chains.
@@ -13,7 +29,7 @@ import rateLimiter from "../utils/rate-limiter.js";
13
29
  class ProviderFactory {
14
30
  /**
15
31
  * Get provider instance with intelligent fallback support.
16
- * @param {string} providerName - Name of the primary provider.
32
+ * @param {string} [providerName] - Name of the primary provider; omitted → default fallback chain.
17
33
  * @param {boolean} [useFallback=true] - Whether to use fallback chain on failure.
18
34
  * @param {Object} [config=null] - Optional configuration overrides.
19
35
  * @returns {AIProvider} - Provider instance or wrapped FallbackProvider.
@@ -45,13 +61,11 @@ class ProviderFactory {
45
61
  };
46
62
  if (selected.analyze) {
47
63
  wrappedProvider.analyze = (prompt, options = {}) => {
48
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
49
64
  return rateLimiter.enqueue(normalizedProviderName, () => selected.analyze(prompt, options));
50
65
  };
51
66
  }
52
67
  if (selected.chat) {
53
68
  wrappedProvider.chat = (messages, options = {}) => {
54
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
55
69
  return rateLimiter.enqueue(normalizedProviderName, () => selected.chat(messages, options));
56
70
  };
57
71
  }
@@ -95,32 +109,36 @@ class ProviderFactory {
95
109
  }
96
110
  /**
97
111
  * Get list of available (configured) providers.
98
- * @returns {string[]} - Array of provider names with valid API keys.
112
+ * @returns {string[]} - Array of provider names with valid API keys, in default chain order.
99
113
  */
100
114
  static getAvailableProviders() {
101
- const providers = {
102
- dashscope: process.env.DASHSCOPE_API_KEY,
103
- xai: process.env.XAI_API_KEY,
104
- openai: process.env.OPENAI_API_KEY,
105
- anthropic: process.env.ANTHROPIC_API_KEY,
106
- deepseek: process.env.DEEPSEEK_API_KEY,
107
- gemini: process.env.GEMINI_API_KEY,
108
- };
109
- return Object.entries(providers)
110
- .filter(([, key]) => !!key)
111
- .map(([name]) => name);
115
+ return Object.keys(PROVIDER_API_KEYS).filter((name) => this.isProviderConfigured(name));
112
116
  }
113
117
  /**
114
- * Validate that at least one provider is configured.
115
- * @returns {string[]} - Array of available providers.
116
- * @throws {Error} If no providers are configured.
118
+ * Fail fast when no provider can translate: with fallback, no provider has an API key;
119
+ * without it, the configured provider has none.
120
+ * @param {string} [providerName] - The configured `apiProvider`.
121
+ * @param {boolean} [useFallback=true] - Whether other providers may be used.
122
+ * @throws {NoProviderError} If no usable provider is configured.
117
123
  */
118
- static validateProviders() {
119
- const available = this.getAvailableProviders();
120
- if (available.length === 0) {
121
- throw new Error("No API providers configured. Please set at least one API key.");
124
+ static assertProviderAvailable(providerName, useFallback = true) {
125
+ const name = (providerName || "").toLowerCase();
126
+ if (!useFallback) {
127
+ const envKey = PROVIDER_API_KEYS[name];
128
+ if (!envKey) {
129
+ throw new NoProviderError(`Unknown provider "${providerName}" and useFallback is false. ` +
130
+ `Supported providers: ${Object.keys(PROVIDER_API_KEYS).join(", ")}.`);
131
+ }
132
+ if (!this.isProviderConfigured(name)) {
133
+ throw new NoProviderError(`No API key for provider "${name}" (useFallback is false, so no other ` +
134
+ `provider is tried). Set ${envKey} in the environment, .env or .env.local.`);
135
+ }
136
+ return;
137
+ }
138
+ if (this.getAvailableProviders().length === 0) {
139
+ throw new NoProviderError("No AI provider has an API key, so nothing can be translated. Set at least one of " +
140
+ `${Object.values(PROVIDER_API_KEYS).join(", ")} in the environment, .env or .env.local.`);
122
141
  }
123
- return available;
124
142
  }
125
143
  /**
126
144
  * Check if a specific provider is configured.
@@ -128,15 +146,7 @@ class ProviderFactory {
128
146
  * @returns {boolean} - True if provider has an API key configured.
129
147
  */
130
148
  static isProviderConfigured(providerName) {
131
- const envVarMap = {
132
- dashscope: "DASHSCOPE_API_KEY",
133
- xai: "XAI_API_KEY",
134
- openai: "OPENAI_API_KEY",
135
- anthropic: "ANTHROPIC_API_KEY",
136
- deepseek: "DEEPSEEK_API_KEY",
137
- gemini: "GEMINI_API_KEY",
138
- };
139
- const envKey = envVarMap[providerName.toLowerCase()];
149
+ const envKey = PROVIDER_API_KEYS[providerName.toLowerCase()];
140
150
  return !!(envKey && process.env[envKey]);
141
151
  }
142
152
  }
@@ -1,4 +1,5 @@
1
- import BaseProvider, { ProviderConfig, TranslateOptions } from "./base-provider.js";
1
+ import BaseProvider, { ChatMessage, ProviderConfig, TranslateOptions } from "./base-provider.js";
2
+ import type { AnthropicMessagesResponse, ProviderResponseBody } from "./api-types.js";
2
3
  /**
3
4
  * Provider implementation for Anthropic (Claude) models.
4
5
  */
@@ -16,10 +17,10 @@ declare class AnthropicProvider extends BaseProvider {
16
17
  */
17
18
  translate(text: string, sourceLang: string, targetLang: string, options?: TranslateOptions): Promise<string>;
18
19
  analyze(prompt: string, options?: ProviderConfig): Promise<string>;
19
- chat(messages: any[], options?: ProviderConfig): Promise<string>;
20
- extractTranslation(response: any, providerName: string): string;
20
+ chat(messages: ChatMessage[], options?: ProviderConfig): Promise<string>;
21
+ extractTranslation(response: AnthropicMessagesResponse | ProviderResponseBody, providerName: string): string;
21
22
  }
22
23
  declare function translate(text: string, sourceLang: string, targetLang: string, options?: TranslateOptions): Promise<string>;
23
24
  declare function analyze(prompt: string, options?: ProviderConfig): Promise<string>;
24
- declare function chat(messages: any[], options?: ProviderConfig): Promise<string>;
25
+ declare function chat(messages: ChatMessage[], options?: ProviderConfig): Promise<string>;
25
26
  export { translate, analyze, chat, AnthropicProvider };
@@ -22,7 +22,6 @@ class AnthropicProvider extends BaseProvider {
22
22
  },
23
23
  timeout: config.timeoutMs || 60000,
24
24
  maxRedirects: 0,
25
- validateStatus: (status) => status < 500,
26
25
  });
27
26
  }
28
27
  getApiKey() {
@@ -153,7 +152,9 @@ class AnthropicProvider extends BaseProvider {
153
152
  return textBlock.text.trim();
154
153
  }
155
154
  }
156
- // Fallback to base implementation for other structures (though unlikely for Anthropic)
155
+ // Fallback to base implementation for other structures (though unlikely for Anthropic).
156
+ // The base only reads a string `content`, so a reply without a text block ends in its
157
+ // "Unable to extract translation" error.
157
158
  return super.extractTranslation(response, providerName);
158
159
  }
159
160
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Response bodies of the provider HTTP APIs, limited to the fields LocAI reads.
3
+ * Every field is optional: bodies come from the network and are checked before use.
4
+ */
5
+ /** Error object some APIs return in a 2xx/4xx body instead of a result. */
6
+ export interface ApiErrorBody {
7
+ message?: string;
8
+ error?: string;
9
+ }
10
+ interface ApiErrorEnvelope {
11
+ error?: ApiErrorBody;
12
+ }
13
+ /** One choice of an OpenAI-compatible chat completion (OpenAI, DeepSeek, xAI, DashScope compatible mode). */
14
+ export interface ChatCompletionChoice {
15
+ message?: {
16
+ content?: string | null;
17
+ };
18
+ /** Legacy completions API. */
19
+ text?: string;
20
+ /** Streaming chunk. */
21
+ delta?: {
22
+ content?: string;
23
+ };
24
+ finish_reason?: string;
25
+ }
26
+ export interface ChatCompletionResponse extends ApiErrorEnvelope {
27
+ choices?: ChatCompletionChoice[];
28
+ }
29
+ /** Google Gemini `models/{model}:generateContent`. */
30
+ export interface GeminiGenerateContentResponse extends ApiErrorEnvelope {
31
+ candidates?: Array<{
32
+ content?: {
33
+ parts?: Array<{
34
+ text?: string;
35
+ }>;
36
+ };
37
+ finishReason?: string;
38
+ }>;
39
+ }
40
+ /** Alibaba DashScope native `text-generation/generation`. */
41
+ export interface DashScopeGenerationResponse extends ApiErrorEnvelope {
42
+ output?: {
43
+ text?: string;
44
+ };
45
+ }
46
+ /** One block of an Anthropic Messages reply; only `type: "text"` blocks carry `text`. */
47
+ export interface AnthropicContentBlock {
48
+ type: string;
49
+ text?: string;
50
+ }
51
+ /** Anthropic `/v1/messages`. */
52
+ export interface AnthropicMessagesResponse extends ApiErrorEnvelope {
53
+ content?: AnthropicContentBlock[];
54
+ }
55
+ /** OpenAI `/v1/embeddings`. */
56
+ export interface EmbeddingResponse {
57
+ data?: Array<{
58
+ index: number;
59
+ embedding: number[];
60
+ }>;
61
+ }
62
+ /**
63
+ * Every body shape the generic `BaseProvider.extractTranslation` understands:
64
+ * the OpenAI-compatible, Gemini and DashScope responses, plain `text` / `content`
65
+ * strings, and an un-unwrapped Axios response whose `data` is a chat completion.
66
+ * `content` may also be an Anthropic block array; only a string `content` is read here,
67
+ * text blocks are extracted by `AnthropicProvider`.
68
+ */
69
+ export interface ProviderResponseBody extends ChatCompletionResponse, GeminiGenerateContentResponse, DashScopeGenerationResponse {
70
+ text?: string;
71
+ content?: string | AnthropicContentBlock[];
72
+ data?: ChatCompletionResponse;
73
+ }
74
+ export {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Response bodies of the provider HTTP APIs, limited to the fields LocAI reads.
3
+ * Every field is optional: bodies come from the network and are checked before use.
4
+ */
5
+ export {};
@@ -1,35 +1,33 @@
1
+ import type { PromptOptions } from "../utils/prompt-templates.js";
2
+ import type { ProviderResponseBody } from "./api-types.js";
3
+ /**
4
+ * Model settings for one provider: constructor config, `apiConfig.<provider>` entries,
5
+ * and per-call options of `analyze()` / `chat()` / `embed()`.
6
+ */
1
7
  export interface ProviderConfig {
2
8
  model?: string;
3
9
  temperature?: number;
4
10
  maxTokens?: number;
5
- [key: string]: any;
11
+ /** HTTP timeout, read once by the provider constructor. */
12
+ timeoutMs?: number;
13
+ /** Retry overrides for `analyze()` / `chat()` / `embed()`. */
14
+ maxRetries?: number;
15
+ initialDelay?: number;
16
+ /** OpenAI `chat()`: request a JSON object response. */
17
+ json?: boolean;
6
18
  }
7
- export interface TranslateOptions {
8
- detectedContext?: {
9
- category?: string;
10
- [key: string]: any;
11
- };
12
- lengthControl?: {
13
- mode?: string;
14
- [key: string]: any;
15
- };
16
- apiConfig?: Record<string, any>;
19
+ /** Options of `translate()`; forwarded to the prompt templates as well. */
20
+ export interface TranslateOptions extends PromptOptions {
21
+ /** Per-provider overrides, keyed by provider name. */
22
+ apiConfig?: Record<string, ProviderConfig>;
17
23
  retryOptions?: {
18
24
  maxRetries?: number;
19
25
  initialDelay?: number;
20
- [key: string]: any;
21
26
  };
22
- [key: string]: any;
23
- }
24
- export interface ConfidenceResult {
25
- score: number;
26
- metrics?: any;
27
- [key: string]: any;
28
27
  }
29
- export interface TranslationResult {
30
- translation: string;
31
- confidence: ConfidenceResult;
32
- rawResponse: any;
28
+ export interface ChatMessage {
29
+ role: "system" | "user" | "assistant";
30
+ content: string;
33
31
  }
34
32
  declare class BaseProvider {
35
33
  name: string;
@@ -40,13 +38,15 @@ declare class BaseProvider {
40
38
  commonHeaders: Record<string, string>;
41
39
  constructor(name: string, config?: ProviderConfig);
42
40
  translate(text: string, sourceLang: string, targetLang: string, _options?: TranslateOptions): Promise<string>;
43
- analyze(prompt: string, options?: any): Promise<string>;
44
- chat(messages: any[], options?: any): Promise<string>;
41
+ analyze(_prompt: string, _options?: ProviderConfig): Promise<string>;
42
+ chat(_messages: ChatMessage[], _options?: ProviderConfig): Promise<string>;
45
43
  getApiKey(): string | undefined;
46
44
  getEndpoint(): string;
47
45
  validateRequest(text: string, sourceLang: string, targetLang: string): void;
48
- validateResponse(response: any, providerName: string): boolean;
49
- handleApiError(error: any, providerName: string): never;
46
+ validateResponse(response: {
47
+ data?: unknown;
48
+ } | null | undefined, providerName: string): boolean;
49
+ handleApiError(error: unknown, providerName: string): never;
50
50
  generatePrompt(text: string, sourceLang: string, targetLang: string, options?: TranslateOptions): string;
51
51
  getConfig(options?: ProviderConfig): {
52
52
  model: string | undefined;
@@ -56,11 +56,7 @@ declare class BaseProvider {
56
56
  apiKey: string;
57
57
  endpoint: string;
58
58
  };
59
- extractTranslation(response: any, providerName: string): string;
60
- /**
61
- * Extract translation with confidence score
62
- */
63
- extractTranslationWithConfidence(response: any, providerName: string, sourceText: string, sourceLang: string, targetLang: string, category?: string): TranslationResult;
59
+ extractTranslation(response: ProviderResponseBody, providerName: string): string;
64
60
  sanitizeTranslation(translation: string): string;
65
61
  }
66
62
  export default BaseProvider;