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.
- package/README.md +23 -25
- package/dist/cli/helpers.d.ts +34 -2
- package/dist/cli/helpers.js +96 -37
- package/dist/cli.js +40 -39
- package/dist/commands/review.d.ts +33 -8
- package/dist/commands/review.js +89 -22
- package/dist/commands/translator.d.ts +4 -2
- package/dist/commands/translator.js +1 -1
- package/dist/config/index.d.ts +53 -66
- package/dist/config/index.js +2 -1
- package/dist/config/setup.d.ts +3 -2
- package/dist/config/setup.js +5 -3
- package/dist/core/adapters/android-adapter.d.ts +3 -2
- package/dist/core/adapters/android-adapter.js +12 -13
- package/dist/core/adapters/base-adapter.d.ts +13 -2
- package/dist/core/adapters/ios-adapter.d.ts +3 -2
- package/dist/core/adapters/json-adapter.d.ts +4 -3
- package/dist/core/adapters/json-adapter.js +1 -3
- package/dist/core/adapters/po-adapter.d.ts +3 -2
- package/dist/core/adapters/po-adapter.js +4 -5
- package/dist/core/adapters/properties-adapter.d.ts +3 -2
- package/dist/core/adapters/properties-adapter.js +2 -4
- package/dist/core/adapters/yaml-adapter.d.ts +4 -3
- package/dist/core/adapters/yaml-adapter.js +1 -3
- package/dist/core/context-processor.d.ts +21 -6
- package/dist/core/context-processor.js +2 -7
- package/dist/core/detection/framework-detector.js +1 -1
- package/dist/core/detection/structure-detector.js +1 -1
- package/dist/core/fallback-provider.d.ts +3 -4
- package/dist/core/fallback-provider.js +15 -21
- package/dist/core/orchestrator.d.ts +98 -26
- package/dist/core/orchestrator.js +56 -116
- package/dist/core/pipeline/Pipeline.d.ts +2 -2
- package/dist/core/pipeline/Pipeline.js +1 -3
- package/dist/core/pipeline/context.d.ts +69 -13
- package/dist/core/pipeline/context.js +1 -7
- package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
- package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
- package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
- package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
- package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
- package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
- package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
- package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
- package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
- package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
- package/dist/core/pipeline/steps/TranslationStep.js +21 -23
- package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
- package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
- package/dist/core/provider-factory.d.ts +15 -9
- package/dist/core/provider-factory.js +42 -32
- package/dist/providers/anthropic.d.ts +5 -4
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/api-types.d.ts +74 -0
- package/dist/providers/api-types.js +5 -0
- package/dist/providers/base-provider.d.ts +27 -31
- package/dist/providers/base-provider.js +22 -46
- package/dist/providers/dashscope.js +0 -2
- package/dist/providers/deepseek.js +0 -1
- package/dist/providers/gemini.js +0 -1
- package/dist/providers/openai.d.ts +3 -3
- package/dist/providers/openai.js +0 -1
- package/dist/providers/xai.js +0 -1
- package/dist/services/embedding-provider.js +4 -2
- package/dist/services/language-processor.d.ts +5 -3
- package/dist/services/language-processor.js +38 -43
- package/dist/services/source-analyzer.d.ts +24 -0
- package/dist/services/source-analyzer.js +161 -68
- package/dist/services/translation-service.d.ts +31 -13
- package/dist/services/translation-service.js +116 -71
- package/dist/services/vector-store.d.ts +15 -2
- package/dist/services/vector-store.js +14 -10
- package/dist/types/index.d.ts +31 -33
- package/dist/utils/ai-context-analyzer.d.ts +12 -3
- package/dist/utils/ai-context-analyzer.js +18 -10
- package/dist/utils/confidence-scorer.d.ts +16 -19
- package/dist/utils/confidence-scorer.js +36 -125
- package/dist/utils/error-helper.d.ts +27 -6
- package/dist/utils/error-helper.js +10 -9
- package/dist/utils/file-manager.d.ts +8 -7
- package/dist/utils/file-manager.js +22 -23
- package/dist/utils/glossary-manager.d.ts +22 -3
- package/dist/utils/glossary-manager.js +47 -28
- package/dist/utils/graceful-shutdown.d.ts +7 -1
- package/dist/utils/graceful-shutdown.js +1 -2
- package/dist/utils/input-validator.d.ts +53 -66
- package/dist/utils/input-validator.js +100 -151
- package/dist/utils/locale-paths.d.ts +7 -0
- package/dist/utils/locale-paths.js +16 -0
- package/dist/utils/logger.d.ts +8 -8
- package/dist/utils/logger.js +7 -15
- package/dist/utils/object-transformer.d.ts +20 -5
- package/dist/utils/object-transformer.js +20 -28
- package/dist/utils/progress-tracker.js +2 -3
- package/dist/utils/prompt-templates.d.ts +45 -5
- package/dist/utils/prompt-templates.js +61 -5
- package/dist/utils/quality/base-checker.d.ts +40 -18
- package/dist/utils/quality/base-checker.js +1 -2
- package/dist/utils/quality/code-block-checker.d.ts +13 -5
- package/dist/utils/quality/index.d.ts +2 -1
- package/dist/utils/quality/index.js +8 -27
- package/dist/utils/quality/length-checker.d.ts +35 -5
- package/dist/utils/quality/length-checker.js +2 -3
- package/dist/utils/quality/markdown-checker.d.ts +14 -7
- package/dist/utils/quality/special-characters-checker.d.ts +18 -1
- package/dist/utils/quality/special-characters-checker.js +0 -1
- package/dist/utils/quality/style-guide-checker.d.ts +1 -1
- package/dist/utils/quality/style-guide-checker.js +0 -2
- package/dist/utils/quality/tone-verifier.js +1 -1
- package/dist/utils/rate-limiter.d.ts +17 -1
- package/dist/utils/rate-limiter.js +12 -11
- package/dist/utils/retry-helper.d.ts +21 -3
- package/dist/utils/retry-helper.js +11 -3
- package/dist/utils/review-queue.d.ts +45 -0
- package/dist/utils/review-queue.js +53 -0
- package/dist/utils/state-manager.d.ts +12 -3
- package/dist/utils/state-manager.js +32 -7
- package/dist/utils/statistics-manager.d.ts +11 -1
- package/dist/utils/statistics-manager.js +8 -0
- package/dist/utils/ui-manager.d.ts +2 -1
- package/dist/utils/ui-manager.js +15 -1
- package/package.json +22 -17
- package/dist/commands/interactive-review.d.ts +0 -9
- package/dist/commands/interactive-review.js +0 -109
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
- package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
- 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
|
-
//
|
|
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
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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?:
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* @
|
|
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
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
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
|
|
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:
|
|
20
|
-
extractTranslation(response:
|
|
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:
|
|
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 {};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
44
|
-
chat(
|
|
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:
|
|
49
|
-
|
|
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:
|
|
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;
|