locai 2.1.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 (72) hide show
  1. package/README.md +928 -0
  2. package/dist/cli.js +718 -0
  3. package/dist/commands/review.js +357 -0
  4. package/dist/commands/translator.js +29 -0
  5. package/dist/config/index.js +52 -0
  6. package/dist/core/adapters/base-adapter.js +1 -0
  7. package/dist/core/adapters/factory.js +44 -0
  8. package/dist/core/adapters/json-adapter.js +20 -0
  9. package/dist/core/adapters/po-adapter.js +50 -0
  10. package/dist/core/adapters/properties-adapter.js +31 -0
  11. package/dist/core/adapters/yaml-adapter.js +24 -0
  12. package/dist/core/context-processor.js +247 -0
  13. package/dist/core/detection/framework-detector.js +56 -0
  14. package/dist/core/detection/structure-detector.js +30 -0
  15. package/dist/core/fallback-provider.js +442 -0
  16. package/dist/core/orchestrator.js +438 -0
  17. package/dist/core/pipeline/Pipeline.js +48 -0
  18. package/dist/core/pipeline/PipelineStep.js +13 -0
  19. package/dist/core/pipeline/context.js +21 -0
  20. package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
  21. package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
  22. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
  23. package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
  24. package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
  25. package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
  26. package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
  27. package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
  28. package/dist/core/pipeline/steps/TranslationStep.js +65 -0
  29. package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
  30. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
  31. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
  32. package/dist/core/provider-factory.js +139 -0
  33. package/dist/providers/base-provider.js +230 -0
  34. package/dist/providers/dashscope.js +128 -0
  35. package/dist/providers/deepseek.js +111 -0
  36. package/dist/providers/gemini.js +135 -0
  37. package/dist/providers/openai.js +203 -0
  38. package/dist/providers/xai.js +111 -0
  39. package/dist/services/embedding-provider.js +50 -0
  40. package/dist/services/translation-service.js +609 -0
  41. package/dist/services/vector-store.js +79 -0
  42. package/dist/utils/ai-context-analyzer.js +460 -0
  43. package/dist/utils/confidence-scorer.js +277 -0
  44. package/dist/utils/error-helper.js +386 -0
  45. package/dist/utils/file-manager.js +357 -0
  46. package/dist/utils/glossary-manager.js +194 -0
  47. package/dist/utils/graceful-shutdown.js +118 -0
  48. package/dist/utils/input-validator.js +663 -0
  49. package/dist/utils/logger.js +374 -0
  50. package/dist/utils/object-transformer.js +177 -0
  51. package/dist/utils/progress-tracker.js +277 -0
  52. package/dist/utils/prompt-templates.js +415 -0
  53. package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
  54. package/dist/utils/quality/base-checker.js +73 -0
  55. package/dist/utils/quality/code-block-checker.js +214 -0
  56. package/dist/utils/quality/html-tag-checker.js +64 -0
  57. package/dist/utils/quality/index.js +220 -0
  58. package/dist/utils/quality/length-checker.js +140 -0
  59. package/dist/utils/quality/markdown-checker.js +220 -0
  60. package/dist/utils/quality/placeholder-checker.js +142 -0
  61. package/dist/utils/quality/punctuation-checker.js +50 -0
  62. package/dist/utils/quality/quote-balance-checker.js +143 -0
  63. package/dist/utils/quality/special-characters-checker.js +229 -0
  64. package/dist/utils/quality/style-guide-checker.js +192 -0
  65. package/dist/utils/quality/text-sanitizer.js +120 -0
  66. package/dist/utils/quality/tone-verifier.js +91 -0
  67. package/dist/utils/rate-limiter.js +398 -0
  68. package/dist/utils/retry-helper.js +166 -0
  69. package/dist/utils/state-manager.js +162 -0
  70. package/dist/utils/statistics-manager.js +51 -0
  71. package/dist/utils/ui-manager.js +98 -0
  72. package/package.json +80 -0
@@ -0,0 +1,67 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ import { log } from "../../../utils/logger.js";
3
+ class ConfidenceCheckStep extends PipelineStep {
4
+ confidenceSettings;
5
+ debug;
6
+ constructor(confidenceSettings, debug = false) {
7
+ super();
8
+ this.confidenceSettings = confidenceSettings;
9
+ this.debug = debug;
10
+ }
11
+ async execute(context, next) {
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ if (!context.confidence || !this.confidenceSettings.enabled) {
14
+ await next();
15
+ return;
16
+ }
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ const confidence = context.confidence;
19
+ const result = context.result;
20
+ result.confidence = confidence;
21
+ // Auto-approve if above threshold
22
+ if (this.confidenceSettings.autoApproveThreshold !== undefined &&
23
+ confidence.score >= this.confidenceSettings.autoApproveThreshold) {
24
+ result.autoApproved = true;
25
+ if (this.confidenceSettings.autoApprovedCount !== undefined) {
26
+ this.confidenceSettings.autoApprovedCount++;
27
+ }
28
+ if (this.debug) {
29
+ log(`Auto-approved: ${context.key} (score: ${confidence.score.toFixed(3)})`, true);
30
+ }
31
+ }
32
+ // Auto-reject if below threshold
33
+ else if (this.confidenceSettings.rejectThreshold !== undefined &&
34
+ confidence.score < this.confidenceSettings.rejectThreshold) {
35
+ result.rejected = true;
36
+ result.rejectionReason = `Quality score too low: ${confidence.score.toFixed(3)}`;
37
+ if (this.confidenceSettings.rejectedCount !== undefined) {
38
+ this.confidenceSettings.rejectedCount++;
39
+ }
40
+ if (this.debug) {
41
+ log(`Auto-rejected: ${context.key} (score: ${confidence.score.toFixed(3)})`, true);
42
+ }
43
+ // Keep original text for rejected translations
44
+ result.translated = context.sourceText;
45
+ }
46
+ // Add to review queue if below review threshold
47
+ else if (this.confidenceSettings.saveReviewQueue &&
48
+ this.confidenceSettings.reviewThreshold !== undefined &&
49
+ confidence.score < this.confidenceSettings.reviewThreshold) {
50
+ result.needsReview = true;
51
+ if (this.confidenceSettings.reviewQueue) {
52
+ this.confidenceSettings.reviewQueue.push({
53
+ key: context.key,
54
+ source: context.sourceText,
55
+ translation: context.translatedText,
56
+ confidence,
57
+ language: context.targetLang,
58
+ sourceLang: context.options.source,
59
+ category: context.meta?.category || "general",
60
+ timestamp: new Date().toISOString(),
61
+ });
62
+ }
63
+ }
64
+ await next();
65
+ }
66
+ }
67
+ export default ConfidenceCheckStep;
@@ -0,0 +1,17 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ class GlossaryPostStep extends PipelineStep {
3
+ glossaryManager;
4
+ constructor(glossaryManager) {
5
+ super();
6
+ this.glossaryManager = glossaryManager;
7
+ }
8
+ async execute(context, next) {
9
+ if (!this.glossaryManager || !context.termMap || !context.translatedText) {
10
+ await next();
11
+ return;
12
+ }
13
+ context.translatedText = this.glossaryManager.restoreTerms(context.translatedText, context.termMap);
14
+ await next();
15
+ }
16
+ }
17
+ export default GlossaryPostStep;
@@ -0,0 +1,20 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ class GlossaryPreStep extends PipelineStep {
3
+ glossaryManager;
4
+ constructor(glossaryManager) {
5
+ super();
6
+ this.glossaryManager = glossaryManager;
7
+ }
8
+ async execute(context, next) {
9
+ if (!this.glossaryManager) {
10
+ context.protectedText = context.sourceText;
11
+ await next();
12
+ return;
13
+ }
14
+ const { protectedText, termMap } = this.glossaryManager.protectTerms(context.sourceText, context.sourceLang, context.targetLang);
15
+ context.protectedText = protectedText;
16
+ context.termMap = termMap;
17
+ await next();
18
+ }
19
+ }
20
+ export default GlossaryPreStep;
@@ -0,0 +1,26 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ class InputValidationStep extends PipelineStep {
3
+ maxKeyLength;
4
+ constructor(maxKeyLength = 10000) {
5
+ super();
6
+ this.maxKeyLength = maxKeyLength;
7
+ }
8
+ async execute(context, next) {
9
+ if (typeof context.sourceText !== "string") {
10
+ context.result.error = "Invalid input type: sourceText must be a string";
11
+ context.result.success = false;
12
+ // Stop pipeline, return error
13
+ return;
14
+ }
15
+ if (context.key.length > this.maxKeyLength) {
16
+ context.result.key = context.key.substring(0, 100) + "...";
17
+ context.result.translated = context.sourceText;
18
+ context.result.error = `Key exceeds maximum length of ${this.maxKeyLength} characters`;
19
+ context.result.success = false;
20
+ // Stop pipeline
21
+ return;
22
+ }
23
+ await next();
24
+ }
25
+ }
26
+ export default InputValidationStep;
@@ -0,0 +1,48 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ import { FileManager } from "../../../utils/file-manager.js";
3
+ class QualityCheckStep extends PipelineStep {
4
+ qualityChecker;
5
+ debug;
6
+ constructor(qualityChecker, debug = false) {
7
+ super();
8
+ this.qualityChecker = qualityChecker;
9
+ this.debug = debug;
10
+ }
11
+ async execute(context, next) {
12
+ if (!context.translatedText) {
13
+ await next();
14
+ return;
15
+ }
16
+ let translated = context.translatedText;
17
+ // 1. Quality Checks & Auto-fixing
18
+ if (this.qualityChecker && this.qualityChecker.rules?.enabled !== false) {
19
+ const qualityResult = this.qualityChecker.validateAndFix(context.sourceText, context.translatedText);
20
+ translated = qualityResult.fixedText;
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ context.qualityResult = qualityResult;
23
+ }
24
+ // 2. JSON Validity Check
25
+ const jsonValidation = FileManager.validateTranslationValue(context.key, translated);
26
+ if (!jsonValidation.valid) {
27
+ if (this.debug) {
28
+ console.warn(`JSON validation warning for key "${context.key}": ${jsonValidation.error}`);
29
+ }
30
+ // Attempt simple quote fix if possible (logic moved from Orchestrator)
31
+ // Note: Since quoteBalanceChecker was inside Orchestrator, we might need to rely on QualityChecker if it has that logic.
32
+ // Assuming QualityChecker handles most, but Orchestrator had a specific extra check.
33
+ // For now, we trust QualityChecker which was passed in.
34
+ }
35
+ context.translatedText = translated;
36
+ // Populate the final result
37
+ context.result = {
38
+ key: context.key,
39
+ translated: context.translatedText,
40
+ context: context.meta,
41
+ success: true,
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ qualityChecks: context.qualityResult,
44
+ };
45
+ await next();
46
+ }
47
+ }
48
+ export default QualityCheckStep;
@@ -0,0 +1,58 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ import { ToneVerifier } from "../../../utils/quality/tone-verifier.js";
3
+ export class ToneCheckStep extends PipelineStep {
4
+ verifier;
5
+ enabled;
6
+ debug;
7
+ constructor(config = {}) {
8
+ super();
9
+ this.verifier = new ToneVerifier();
10
+ this.enabled = config.enabled || false;
11
+ this.debug = config.debug || false;
12
+ }
13
+ async execute(context, next) {
14
+ // Only run if enabled and we have a translation
15
+ if (!this.enabled || !context.translatedText || !context.options?.styleGuide?.toneOfVoice) {
16
+ await next();
17
+ return;
18
+ }
19
+ try {
20
+ if (this.debug) {
21
+ console.log(`[ToneCheck] Verifying "${context.key}" against tone: ${context.options.styleGuide.toneOfVoice}`);
22
+ }
23
+ const styleGuide = context.options.styleGuide;
24
+ const provider = styleGuide.toneProvider;
25
+ const options = styleGuide.analysisOptions;
26
+ const result = await this.verifier.verify(context.sourceText, context.translatedText, context.sourceLang, context.targetLang, styleGuide.toneOfVoice, provider, options);
27
+ if (!result.passed) {
28
+ if (this.debug) {
29
+ console.warn(`[ToneCheck] Failed: Score ${result.score}/10. Reason: ${result.reasoning}`);
30
+ }
31
+ // Mark in context that tone validation failed
32
+ // We can define a standard structure for quality warnings
33
+ const qualityResult = context.qualityResult || { issues: [] };
34
+ qualityResult.issues.push({
35
+ type: "tone_mismatch",
36
+ message: result.reasoning,
37
+ severity: "warning",
38
+ suggestion: result.suggestions,
39
+ score: result.score,
40
+ });
41
+ context.qualityResult = qualityResult;
42
+ // Optionally, if we want to enforce REJECTION, we could clear translatedText
43
+ // For now, per plan, we are adding warnings/feedback loop potential.
44
+ // context.translatedText = null; // Uncomment to strict enforce
45
+ }
46
+ else {
47
+ if (this.debug) {
48
+ console.log(`[ToneCheck] Passed: Score ${result.score}/10`);
49
+ }
50
+ }
51
+ }
52
+ catch (error) {
53
+ console.error("[ToneCheck] Error executing verification:", error);
54
+ }
55
+ await next();
56
+ }
57
+ }
58
+ export default ToneCheckStep;
@@ -0,0 +1,65 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ import ProviderFactory from "../../provider-factory.js";
3
+ class TranslationStep extends PipelineStep {
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ options;
6
+ confidenceSettings;
7
+ constructor(
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ options = {}, confidenceSettings = {}) {
10
+ super();
11
+ this.options = options;
12
+ this.confidenceSettings = confidenceSettings;
13
+ }
14
+ async execute(context, next) {
15
+ try {
16
+ const provider = this._getProvider();
17
+ // Setup translation options
18
+ const translationOpts = {
19
+ ...this.options,
20
+ detectedContext: {
21
+ ...context.meta,
22
+ existingTranslation: context.existingTranslation || null,
23
+ },
24
+ };
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
41
+ }
42
+ // Continue to next steps (restore glossary, quality check, etc.)
43
+ await next();
44
+ }
45
+ catch (error) {
46
+ console.error(`Translation error - key "${context.key}":`, error);
47
+ context.result.error = error.message;
48
+ context.result.success = false;
49
+ context.result.translated = context.sourceText; // Fallback to source
50
+ // Stop pipeline or proceed?
51
+ // Usually if translation fails, we stop quality checks but we might want to return the error result.
52
+ // Since we set context.result, returning here is fine.
53
+ return;
54
+ }
55
+ }
56
+ _getProvider() {
57
+ const provider = ProviderFactory.getProvider(this.options.apiProvider, this.options.useFallback !== false, this.options);
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ if (!provider || typeof provider.translate !== "function") {
60
+ throw new Error(`Translation provider not available or invalid: ${this.options.apiProvider}`);
61
+ }
62
+ return provider;
63
+ }
64
+ }
65
+ export default TranslationStep;
@@ -0,0 +1,84 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ export class VectorCacheReadStep extends PipelineStep {
3
+ vectorStore;
4
+ embeddingProvider;
5
+ stats;
6
+ options;
7
+ constructor(vectorStore, embeddingProvider, stats, options = {}) {
8
+ super();
9
+ this.vectorStore = vectorStore;
10
+ this.embeddingProvider = embeddingProvider;
11
+ this.stats = stats;
12
+ this.options = options;
13
+ }
14
+ async execute(context, next) {
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) {
20
+ await next();
21
+ return;
22
+ }
23
+ try {
24
+ // Generate embedding for source text
25
+ // Optimization: If text is very short/garbage, maybe skip?
26
+ // For now, process everything reasonable.
27
+ const embedding = await this.embeddingProvider.getEmbedding(context.sourceText);
28
+ if (embedding.length === 0) {
29
+ await next();
30
+ return;
31
+ }
32
+ // Store embedding in context for Write step to reuse (save API call)
33
+ context.meta = context.meta || {};
34
+ context.meta.embedding = embedding;
35
+ const results = await this.vectorStore.search(embedding, context.targetLang, 1);
36
+ const bestMatch = results[0];
37
+ if (bestMatch) {
38
+ const similarity = bestMatch.score;
39
+ const exactThreshold = this.options.exactMatchThreshold || 0.98;
40
+ const contextThreshold = this.options.similarityThreshold || 0.85;
41
+ if (similarity >= exactThreshold) {
42
+ // "Exact" Vector Match - Treat as cache hit
43
+ this.stats.hits++;
44
+ context.result = {
45
+ key: context.key,
46
+ translated: bestMatch.item.translation,
47
+ success: true,
48
+ fromCache: true, // From "Vector" cache
49
+ context: {
50
+ matchType: "vector-exact",
51
+ similarity: similarity,
52
+ },
53
+ meta: {
54
+ vectorMatch: true,
55
+ similarity: similarity,
56
+ },
57
+ };
58
+ // Short-circuit
59
+ return;
60
+ }
61
+ else if (similarity >= contextThreshold) {
62
+ // Context Match - Provide to LLM
63
+ this.stats.contextUsed++;
64
+ context.meta.similarTranslation = {
65
+ source: bestMatch.item.text,
66
+ target: bestMatch.item.translation,
67
+ similarity: similarity,
68
+ };
69
+ }
70
+ else {
71
+ this.stats.misses++;
72
+ }
73
+ }
74
+ else {
75
+ this.stats.misses++;
76
+ }
77
+ }
78
+ catch (error) {
79
+ console.warn(`Vector cache read failed: ${error.message}`);
80
+ // Continue pipeline on error
81
+ }
82
+ await next();
83
+ }
84
+ }
@@ -0,0 +1,42 @@
1
+ import PipelineStep from "../PipelineStep.js";
2
+ export class VectorCacheWriteStep extends PipelineStep {
3
+ vectorStore;
4
+ embeddingProvider;
5
+ options;
6
+ constructor(vectorStore, embeddingProvider, options = {}) {
7
+ super();
8
+ this.vectorStore = vectorStore;
9
+ this.embeddingProvider = embeddingProvider;
10
+ this.options = options;
11
+ }
12
+ async execute(context, next) {
13
+ // Execute next steps first (which might be nothing as this is usually last)
14
+ await next();
15
+ // Check conditions to save:
16
+ // 1. Enabled
17
+ // 2. Translation successful
18
+ // 3. Not from cache (exact or vector)
19
+ if (!this.options.enabled ||
20
+ !context.result?.success ||
21
+ context.result?.fromCache ||
22
+ !context.translatedText) {
23
+ return;
24
+ }
25
+ try {
26
+ // Get embedding: reuse if generated in read step, else generate
27
+ let embedding = context.meta?.embedding;
28
+ if (!embedding) {
29
+ embedding = await this.embeddingProvider.getEmbedding(context.sourceText);
30
+ }
31
+ if (embedding && embedding.length > 0) {
32
+ await this.vectorStore.addItem(context.sourceText, context.translatedText, context.sourceLang, context.targetLang, embedding, {
33
+ category: context.meta?.category,
34
+ key: context.key,
35
+ });
36
+ }
37
+ }
38
+ catch (error) {
39
+ console.warn(`Vector cache write failed: ${error.message}`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ToneCheckStep } from "../ToneCheckStep.js";
3
+ // Mock ToneVerifier
4
+ vi.mock("../../../../utils/quality/tone-verifier.js", () => {
5
+ return {
6
+ ToneVerifier: class {
7
+ verify = vi.fn();
8
+ },
9
+ };
10
+ });
11
+ describe("ToneCheckStep", () => {
12
+ let step;
13
+ let mockContext;
14
+ let mockNext;
15
+ let mockVerifier;
16
+ beforeEach(() => {
17
+ // Reset mocks
18
+ vi.clearAllMocks();
19
+ step = new ToneCheckStep({ enabled: true });
20
+ mockContext = {
21
+ translatedText: "Sanal Makine",
22
+ sourceText: "Virtual Machine",
23
+ sourceLang: "en",
24
+ targetLang: "tr",
25
+ options: {
26
+ styleGuide: {
27
+ toneOfVoice: "tech",
28
+ },
29
+ },
30
+ qualityResult: {
31
+ issues: [],
32
+ },
33
+ };
34
+ mockNext = vi.fn().mockResolvedValue(undefined);
35
+ // Get the mock instance
36
+ // @ts-ignore
37
+ mockVerifier = step.verifier;
38
+ });
39
+ it("should skip verification if disabled", async () => {
40
+ const disabledStep = new ToneCheckStep({ enabled: false });
41
+ await disabledStep.execute(mockContext, mockNext);
42
+ expect(mockVerifier.verify).not.toHaveBeenCalled();
43
+ expect(mockNext).toHaveBeenCalled();
44
+ });
45
+ it("should skip verification if no translated text", async () => {
46
+ mockContext.translatedText = "";
47
+ await step.execute(mockContext, mockNext);
48
+ expect(mockVerifier.verify).not.toHaveBeenCalled();
49
+ expect(mockNext).toHaveBeenCalled();
50
+ });
51
+ it("should skip verification if no tone defined", async () => {
52
+ mockContext.options.styleGuide.toneOfVoice = undefined;
53
+ await step.execute(mockContext, mockNext);
54
+ expect(mockVerifier.verify).not.toHaveBeenCalled();
55
+ expect(mockNext).toHaveBeenCalled();
56
+ });
57
+ it("should pass configured provider to verifier", async () => {
58
+ mockContext.options.styleGuide.toneProvider = "gemini";
59
+ mockVerifier.verify.mockResolvedValue({ passed: true, score: 10 });
60
+ await step.execute(mockContext, mockNext);
61
+ expect(mockVerifier.verify).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(String), expect.any(String), expect.any(String), "gemini", undefined);
62
+ });
63
+ it("should pass configured toneProvider and analysis options to verifier", async () => {
64
+ mockContext.options.styleGuide.toneProvider = "deepseek";
65
+ mockContext.options.styleGuide.analysisOptions = { temperature: 0.7 };
66
+ mockVerifier.verify.mockResolvedValue({ passed: true, score: 10 });
67
+ await step.execute(mockContext, mockNext);
68
+ expect(mockVerifier.verify).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(String), expect.any(String), expect.any(String), "deepseek", { temperature: 0.7 });
69
+ });
70
+ it("should verify and add warning on failure", async () => {
71
+ mockVerifier.verify.mockResolvedValue({
72
+ passed: false,
73
+ score: 5,
74
+ reasoning: "Too simple",
75
+ });
76
+ await step.execute(mockContext, mockNext);
77
+ expect(mockVerifier.verify).toHaveBeenCalledWith("Virtual Machine", "Sanal Makine", "en", "tr", "tech", undefined, // Default provider
78
+ undefined // Default options
79
+ );
80
+ expect(mockContext.qualityResult.issues.length).toBe(1);
81
+ expect(mockContext.qualityResult.issues[0].message).toContain("Too simple");
82
+ expect(mockNext).toHaveBeenCalled();
83
+ });
84
+ it("should not add warning on success", async () => {
85
+ mockVerifier.verify.mockResolvedValue({
86
+ passed: true,
87
+ score: 9,
88
+ });
89
+ await step.execute(mockContext, mockNext);
90
+ expect(mockContext.qualityResult.issues.length).toBe(0);
91
+ expect(mockNext).toHaveBeenCalled();
92
+ });
93
+ it("should handle verifier errors gracefully", async () => {
94
+ mockVerifier.verify.mockRejectedValue(new Error("Network error"));
95
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
96
+ await step.execute(mockContext, mockNext);
97
+ expect(mockNext).toHaveBeenCalled();
98
+ consoleSpy.mockRestore();
99
+ });
100
+ });
@@ -0,0 +1,139 @@
1
+ import * as deepseekProvider from "../providers/deepseek.js";
2
+ import * as geminiProvider from "../providers/gemini.js";
3
+ import * as openaiProvider from "../providers/openai.js";
4
+ import * as dashscopeProvider from "../providers/dashscope.js";
5
+ import * as xaiProvider from "../providers/xai.js";
6
+ import FallbackProvider from "./fallback-provider.js";
7
+ import rateLimiter from "../utils/rate-limiter.js";
8
+ /**
9
+ * Factory for creating and managing AI providers.
10
+ * Handles provider instantiation, configuration, and fallback chains.
11
+ */
12
+ class ProviderFactory {
13
+ /**
14
+ * Get provider instance with intelligent fallback support.
15
+ * @param {string} providerName - Name of the primary provider.
16
+ * @param {boolean} [useFallback=true] - Whether to use fallback chain on failure.
17
+ * @param {Object} [config=null] - Optional configuration overrides.
18
+ * @returns {AIProvider} - Provider instance or wrapped FallbackProvider.
19
+ * @throws {Error} If provider is not found or configured.
20
+ */
21
+ static getProvider(providerName, useFallback = true, config = null) {
22
+ const providers = {
23
+ dashscope: dashscopeProvider,
24
+ xai: xaiProvider,
25
+ openai: openaiProvider,
26
+ deepseek: deepseekProvider,
27
+ gemini: geminiProvider,
28
+ };
29
+ const normalizedProviderName = (providerName || "").toLowerCase();
30
+ if (!useFallback) {
31
+ const selected = providers[normalizedProviderName];
32
+ if (!selected) {
33
+ throw new Error(`Provider ${providerName} not found or not configured`);
34
+ }
35
+ if (!this.isProviderConfigured(normalizedProviderName)) {
36
+ throw new Error(`Provider ${providerName} is not configured. Missing API key.`);
37
+ }
38
+ const wrappedProvider = {
39
+ async translate(text, sourceLang, targetLang, options = {}) {
40
+ const priority = text.length < 100 ? 1 : 0;
41
+ return rateLimiter.enqueue(normalizedProviderName, () => selected.translate(text, sourceLang, targetLang, options), priority);
42
+ },
43
+ };
44
+ if (selected.analyze) {
45
+ wrappedProvider.analyze = (prompt, options = {}) => {
46
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
47
+ return rateLimiter.enqueue(normalizedProviderName, () => selected.analyze(prompt, options));
48
+ };
49
+ }
50
+ if (selected.chat) {
51
+ wrappedProvider.chat = (messages, options = {}) => {
52
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
53
+ return rateLimiter.enqueue(normalizedProviderName, () => selected.chat(messages, options));
54
+ };
55
+ }
56
+ return wrappedProvider;
57
+ }
58
+ const allProviders = [];
59
+ const availableProviderNames = this.getAvailableProviders();
60
+ if (normalizedProviderName &&
61
+ providers[normalizedProviderName] &&
62
+ availableProviderNames.includes(normalizedProviderName)) {
63
+ allProviders.push({
64
+ name: normalizedProviderName,
65
+ implementation: providers[normalizedProviderName],
66
+ });
67
+ }
68
+ else if (normalizedProviderName) {
69
+ console.warn(`Provider '${providerName}' not found or not configured, using default provider chain`);
70
+ }
71
+ let fallbackOrder = availableProviderNames;
72
+ if (config?.fallbackOrder && Array.isArray(config.fallbackOrder)) {
73
+ fallbackOrder = config.fallbackOrder
74
+ .filter((name) => availableProviderNames.includes(name.toLowerCase()))
75
+ .map((name) => name.toLowerCase());
76
+ const remainingProviders = availableProviderNames.filter((name) => !fallbackOrder.includes(name));
77
+ fallbackOrder = [...fallbackOrder, ...remainingProviders];
78
+ }
79
+ for (const name of fallbackOrder) {
80
+ if (!allProviders.some((p) => p.name === name) && providers[name]) {
81
+ allProviders.push({ name, implementation: providers[name] });
82
+ }
83
+ }
84
+ if (allProviders.length === 0) {
85
+ throw new Error("No valid providers found for fallback chain. Please check your API keys.");
86
+ }
87
+ if (process.env.DEBUG) {
88
+ const safeProviderNames = allProviders.map((p) => p.name // FallbackProvider logic expects name property on wrapped object? No, p is ProviderWrapper {name, implementation}
89
+ );
90
+ console.log(`Provider fallback chain: ${safeProviderNames.join(" → ")}`);
91
+ }
92
+ return new FallbackProvider(allProviders);
93
+ }
94
+ /**
95
+ * Get list of available (configured) providers.
96
+ * @returns {string[]} - Array of provider names with valid API keys.
97
+ */
98
+ static getAvailableProviders() {
99
+ const providers = {
100
+ dashscope: process.env.DASHSCOPE_API_KEY,
101
+ xai: process.env.XAI_API_KEY,
102
+ openai: process.env.OPENAI_API_KEY,
103
+ deepseek: process.env.DEEPSEEK_API_KEY,
104
+ gemini: process.env.GEMINI_API_KEY,
105
+ };
106
+ return Object.entries(providers)
107
+ .filter(([, key]) => !!key)
108
+ .map(([name]) => name);
109
+ }
110
+ /**
111
+ * Validate that at least one provider is configured.
112
+ * @returns {string[]} - Array of available providers.
113
+ * @throws {Error} If no providers are configured.
114
+ */
115
+ static validateProviders() {
116
+ const available = this.getAvailableProviders();
117
+ if (available.length === 0) {
118
+ throw new Error("No API providers configured. Please set at least one API key.");
119
+ }
120
+ return available;
121
+ }
122
+ /**
123
+ * Check if a specific provider is configured.
124
+ * @param {string} providerName - Name of the provider.
125
+ * @returns {boolean} - True if provider has an API key configured.
126
+ */
127
+ static isProviderConfigured(providerName) {
128
+ const envVarMap = {
129
+ dashscope: "DASHSCOPE_API_KEY",
130
+ xai: "XAI_API_KEY",
131
+ openai: "OPENAI_API_KEY",
132
+ deepseek: "DEEPSEEK_API_KEY",
133
+ gemini: "GEMINI_API_KEY",
134
+ };
135
+ const envKey = envVarMap[providerName.toLowerCase()];
136
+ return !!(envKey && process.env[envKey]);
137
+ }
138
+ }
139
+ export default ProviderFactory;