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.
- package/README.md +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- package/package.json +80 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for translation providers
|
|
3
|
+
*/
|
|
4
|
+
import ErrorHelper from "../utils/error-helper.js";
|
|
5
|
+
import ConfidenceScorer from "../utils/confidence-scorer.js";
|
|
6
|
+
class BaseProvider {
|
|
7
|
+
name;
|
|
8
|
+
config;
|
|
9
|
+
defaultModel;
|
|
10
|
+
defaultTemperature;
|
|
11
|
+
defaultMaxTokens;
|
|
12
|
+
commonHeaders;
|
|
13
|
+
constructor(name, config = {}) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.defaultModel = config.model;
|
|
17
|
+
this.defaultTemperature = config.temperature || 0.3;
|
|
18
|
+
this.defaultMaxTokens = config.maxTokens || 2000;
|
|
19
|
+
this.commonHeaders = {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"User-Agent": "locai/1.0",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async translate(text, sourceLang, targetLang, _options = {}) {
|
|
25
|
+
throw new Error(`translate method must be implemented by ${this.name} provider`);
|
|
26
|
+
}
|
|
27
|
+
async analyze(prompt, options = {}) {
|
|
28
|
+
throw new Error(`analyze method must be implemented by ${this.name} provider if supported`);
|
|
29
|
+
}
|
|
30
|
+
async chat(messages, options = {}) {
|
|
31
|
+
throw new Error(`chat method must be implemented by ${this.name} provider if supported`);
|
|
32
|
+
}
|
|
33
|
+
getApiKey() {
|
|
34
|
+
throw new Error(`getApiKey method must be implemented by ${this.name} provider`);
|
|
35
|
+
}
|
|
36
|
+
getEndpoint() {
|
|
37
|
+
throw new Error(`getEndpoint method must be implemented by ${this.name} provider`);
|
|
38
|
+
}
|
|
39
|
+
validateRequest(text, sourceLang, targetLang) {
|
|
40
|
+
if (!text || typeof text !== "string") {
|
|
41
|
+
throw new Error("Text must be a non-empty string");
|
|
42
|
+
}
|
|
43
|
+
if (!sourceLang || !targetLang) {
|
|
44
|
+
throw new Error("Source and target languages are required");
|
|
45
|
+
}
|
|
46
|
+
if (text.length > 10000) {
|
|
47
|
+
throw new Error("Text too long for translation (max 10000 characters)");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
validateResponse(response, providerName) {
|
|
51
|
+
if (!response) {
|
|
52
|
+
throw new Error(`No response from ${providerName} API`);
|
|
53
|
+
}
|
|
54
|
+
if (!response.data) {
|
|
55
|
+
throw new Error(`Invalid response format from ${providerName}`);
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
|
60
|
+
handleApiError(error, providerName) {
|
|
61
|
+
if (error.response) {
|
|
62
|
+
const status = error.response.status;
|
|
63
|
+
const apiMessage = error.response.data?.error?.message || error.response.data?.message || null;
|
|
64
|
+
// Rate limit error
|
|
65
|
+
if (status === 429) {
|
|
66
|
+
const retryAfter = error.response.headers?.["retry-after"] || 60;
|
|
67
|
+
const limit = error.response.headers?.["x-ratelimit-limit"];
|
|
68
|
+
const remaining = error.response.headers?.["x-ratelimit-remaining"];
|
|
69
|
+
throw ErrorHelper.rateLimitError(providerName, {
|
|
70
|
+
limit,
|
|
71
|
+
current: limit ? limit - remaining : null,
|
|
72
|
+
retryAfter,
|
|
73
|
+
statusCode: status,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Server error (5xx)
|
|
77
|
+
if (status >= 500) {
|
|
78
|
+
throw ErrorHelper.serverError(providerName, status, apiMessage);
|
|
79
|
+
}
|
|
80
|
+
// Authentication error (401, 403)
|
|
81
|
+
if (status === 401 || status === 403) {
|
|
82
|
+
throw ErrorHelper.authError(providerName, status, apiMessage);
|
|
83
|
+
}
|
|
84
|
+
// Other API errors
|
|
85
|
+
const apiError = ErrorHelper.createError("API_RESPONSE_ERROR", {
|
|
86
|
+
provider: providerName,
|
|
87
|
+
statusCode: status,
|
|
88
|
+
apiMessage,
|
|
89
|
+
});
|
|
90
|
+
apiError.message = `${providerName} API error (${status})${apiMessage ? `: ${apiMessage}` : ""}`;
|
|
91
|
+
throw apiError;
|
|
92
|
+
}
|
|
93
|
+
// Network errors
|
|
94
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
95
|
+
throw ErrorHelper.networkError(providerName);
|
|
96
|
+
}
|
|
97
|
+
// Timeout errors
|
|
98
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
|
99
|
+
throw ErrorHelper.timeoutError(providerName, error.config?.timeout);
|
|
100
|
+
}
|
|
101
|
+
// Unknown errors - preserve original for debugging
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
const unknownError = new Error(`${providerName}: ${error.message}`);
|
|
104
|
+
unknownError.code = "ERR_UNKNOWN";
|
|
105
|
+
unknownError.originalError = error;
|
|
106
|
+
throw unknownError;
|
|
107
|
+
}
|
|
108
|
+
generatePrompt(text, sourceLang, targetLang, options = {}) {
|
|
109
|
+
const context = options.detectedContext;
|
|
110
|
+
let prompt = `Translate the following text from ${sourceLang} to ${targetLang}`;
|
|
111
|
+
if (context?.category && context.category !== "general") {
|
|
112
|
+
const categoryPrompts = {
|
|
113
|
+
technical: ". Preserve technical terms and variable names exactly as they appear.",
|
|
114
|
+
defi: ". Keep DeFi and cryptocurrency terms in English.",
|
|
115
|
+
marketing: ". Use persuasive and engaging language appropriate for marketing.",
|
|
116
|
+
legal: ". Maintain formal tone and precise legal terminology.",
|
|
117
|
+
ui: ". Keep UI terms consistent and clear for user interface elements.",
|
|
118
|
+
};
|
|
119
|
+
prompt += categoryPrompts[context.category] || "";
|
|
120
|
+
}
|
|
121
|
+
if (options.lengthControl?.mode === "strict") {
|
|
122
|
+
prompt += " Keep the translation length similar to the original.";
|
|
123
|
+
}
|
|
124
|
+
const placeholderRegex = /\{[^}]+\}/g;
|
|
125
|
+
const hasPlaceholders = placeholderRegex.test(text);
|
|
126
|
+
if (hasPlaceholders) {
|
|
127
|
+
prompt +=
|
|
128
|
+
"\n\nCRITICAL: This text contains placeholders like {variable}. You MUST preserve them EXACTLY as they appear. Do NOT translate the placeholder names, do NOT modify the curly braces, and do NOT add any text around them.";
|
|
129
|
+
}
|
|
130
|
+
prompt += `\n\nText to translate: "${text}"`;
|
|
131
|
+
prompt += "\n\nProvide only the translation without explanations or quotes.";
|
|
132
|
+
return prompt;
|
|
133
|
+
}
|
|
134
|
+
getConfig(options = {}) {
|
|
135
|
+
const apiKey = this.getApiKey();
|
|
136
|
+
if (!apiKey) {
|
|
137
|
+
throw new Error(`API key not configured for ${this.name} provider`);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
model: options.model || this.defaultModel,
|
|
141
|
+
temperature: options.temperature ?? this.defaultTemperature,
|
|
142
|
+
maxTokens: options.maxTokens || this.defaultMaxTokens,
|
|
143
|
+
max_tokens: options.maxTokens || this.defaultMaxTokens, // Backward compatibility
|
|
144
|
+
apiKey: apiKey,
|
|
145
|
+
endpoint: this.getEndpoint(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
|
149
|
+
extractTranslation(response, providerName) {
|
|
150
|
+
// Check for error response first
|
|
151
|
+
if (response.error) {
|
|
152
|
+
const errorMsg = response.error.message || response.error.error || JSON.stringify(response.error);
|
|
153
|
+
throw new Error(`api: ${providerName} - ${errorMsg}`);
|
|
154
|
+
}
|
|
155
|
+
let translation = null;
|
|
156
|
+
if (response.choices && response.choices[0]?.message?.content) {
|
|
157
|
+
translation = response.choices[0].message.content.trim();
|
|
158
|
+
}
|
|
159
|
+
else if (response.choices && response.choices[0]?.text) {
|
|
160
|
+
translation = response.choices[0].text.trim();
|
|
161
|
+
}
|
|
162
|
+
else if (response.choices && response.choices[0]?.delta?.content) {
|
|
163
|
+
translation = response.choices[0].delta.content.trim();
|
|
164
|
+
}
|
|
165
|
+
else if (response.candidates && response.candidates[0]?.content?.parts?.[0]?.text) {
|
|
166
|
+
translation = response.candidates[0].content.parts[0].text.trim();
|
|
167
|
+
}
|
|
168
|
+
else if (response.output && response.output.text) {
|
|
169
|
+
translation = response.output.text.trim();
|
|
170
|
+
}
|
|
171
|
+
else if (response.text) {
|
|
172
|
+
translation = response.text.trim();
|
|
173
|
+
}
|
|
174
|
+
else if (response.content) {
|
|
175
|
+
translation = response.content.trim();
|
|
176
|
+
}
|
|
177
|
+
else if (response.data &&
|
|
178
|
+
response.data.choices &&
|
|
179
|
+
response.data.choices[0]?.message?.content) {
|
|
180
|
+
translation = response.data.choices[0].message.content.trim();
|
|
181
|
+
}
|
|
182
|
+
if (!translation) {
|
|
183
|
+
console.error(`Unable to extract translation from ${providerName} response. Response structure:`, {
|
|
184
|
+
keys: Object.keys(response),
|
|
185
|
+
choices: response.choices ? `Array(${response.choices.length})` : "undefined",
|
|
186
|
+
choicesStructure: response.choices && response.choices[0]
|
|
187
|
+
? Object.keys(response.choices[0])
|
|
188
|
+
: "undefined",
|
|
189
|
+
});
|
|
190
|
+
throw new Error(`Unable to extract translation from ${providerName} response`);
|
|
191
|
+
}
|
|
192
|
+
return translation;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Extract translation with confidence score
|
|
196
|
+
*/
|
|
197
|
+
extractTranslationWithConfidence(
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
|
199
|
+
response, providerName, sourceText, sourceLang, targetLang, category = "general") {
|
|
200
|
+
const translation = this.extractTranslation(response, providerName);
|
|
201
|
+
// Extract AI confidence from response
|
|
202
|
+
const aiConfidence = ConfidenceScorer.extractAIConfidence(response, providerName);
|
|
203
|
+
// Calculate comprehensive confidence score
|
|
204
|
+
const confidenceResult = ConfidenceScorer.calculateConfidence({
|
|
205
|
+
aiConfidence,
|
|
206
|
+
sourceText,
|
|
207
|
+
translation,
|
|
208
|
+
sourceLang,
|
|
209
|
+
targetLang,
|
|
210
|
+
provider: providerName,
|
|
211
|
+
category,
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
translation,
|
|
215
|
+
confidence: confidenceResult,
|
|
216
|
+
rawResponse: response,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
sanitizeTranslation(translation) {
|
|
220
|
+
if (!translation || typeof translation !== "string") {
|
|
221
|
+
throw new Error("Invalid translation format");
|
|
222
|
+
}
|
|
223
|
+
return translation
|
|
224
|
+
.replace(/^["']|["']$/g, "")
|
|
225
|
+
.replace(/^\s*Translation:\s*/i, "")
|
|
226
|
+
.replace(/^\s*Result:\s*/i, "")
|
|
227
|
+
.trim();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export default BaseProvider;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import BaseProvider from "./base-provider.js";
|
|
3
|
+
import { getPrompt, getAnalysisPrompt } from "../utils/prompt-templates.js";
|
|
4
|
+
import RetryHelper from "../utils/retry-helper.js";
|
|
5
|
+
/**
|
|
6
|
+
* Provider implementation for Alibaba DashScope (Qwen) models.
|
|
7
|
+
*/
|
|
8
|
+
class DashScopeProvider extends BaseProvider {
|
|
9
|
+
client;
|
|
10
|
+
generationClient;
|
|
11
|
+
/**
|
|
12
|
+
* Create a new DashScopeProvider instance.
|
|
13
|
+
* @param {ProviderConfig} config - Provider configuration.
|
|
14
|
+
*/
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super("dashscope", config);
|
|
17
|
+
this.client = axios.create({
|
|
18
|
+
baseURL: "https://dashscope-intl.aliyuncs.com",
|
|
19
|
+
headers: {
|
|
20
|
+
...this.commonHeaders,
|
|
21
|
+
Authorization: `Bearer ${this.getApiKey()}`,
|
|
22
|
+
},
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
maxRedirects: 0,
|
|
25
|
+
validateStatus: (status) => status < 500,
|
|
26
|
+
});
|
|
27
|
+
// Generation client for analysis
|
|
28
|
+
this.generationClient = axios.create({
|
|
29
|
+
baseURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc",
|
|
30
|
+
headers: {
|
|
31
|
+
...this.commonHeaders,
|
|
32
|
+
Authorization: `Bearer ${this.getApiKey()}`,
|
|
33
|
+
},
|
|
34
|
+
timeout: 30000,
|
|
35
|
+
maxRedirects: 0,
|
|
36
|
+
validateStatus: (status) => status < 500,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
getApiKey() {
|
|
40
|
+
return process.env.DASHSCOPE_API_KEY;
|
|
41
|
+
}
|
|
42
|
+
getEndpoint() {
|
|
43
|
+
return "/compatible-mode/v1/chat/completions";
|
|
44
|
+
}
|
|
45
|
+
getGenerationEndpoint() {
|
|
46
|
+
return "/text-generation/generation";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Translate text using DashScope.
|
|
50
|
+
*/
|
|
51
|
+
async translate(text, sourceLang, targetLang, options = {}) {
|
|
52
|
+
this.validateRequest(text, sourceLang, targetLang);
|
|
53
|
+
const config = this.getConfig(options.apiConfig?.dashscope);
|
|
54
|
+
const promptData = getPrompt("dashscope", sourceLang, targetLang, text, options);
|
|
55
|
+
return RetryHelper.withRetry(async () => {
|
|
56
|
+
try {
|
|
57
|
+
const response = await this.client.post(this.getEndpoint(), {
|
|
58
|
+
model: config.model || "qwen-plus",
|
|
59
|
+
...promptData,
|
|
60
|
+
temperature: config.temperature || 0.3,
|
|
61
|
+
max_tokens: config.maxTokens || 2000,
|
|
62
|
+
});
|
|
63
|
+
this.validateResponse(response, this.name);
|
|
64
|
+
const translation = this.extractTranslation(response.data, this.name);
|
|
65
|
+
return this.sanitizeTranslation(translation);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.handleApiError(error, this.name);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}, {
|
|
72
|
+
maxRetries: options.retryOptions?.maxRetries || 2,
|
|
73
|
+
initialDelay: options.retryOptions?.initialDelay || 1000,
|
|
74
|
+
context: "DashScope Provider",
|
|
75
|
+
logContext: {
|
|
76
|
+
source: sourceLang,
|
|
77
|
+
target: targetLang,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async analyze(prompt, options = {}) {
|
|
82
|
+
const config = this.getConfig({
|
|
83
|
+
model: options.model || "qwen-plus",
|
|
84
|
+
temperature: options.temperature || 0.2,
|
|
85
|
+
maxTokens: options.maxTokens || 1000,
|
|
86
|
+
});
|
|
87
|
+
const promptData = getAnalysisPrompt("dashscope", prompt, {
|
|
88
|
+
...options,
|
|
89
|
+
...config,
|
|
90
|
+
});
|
|
91
|
+
return RetryHelper.withRetry(async () => {
|
|
92
|
+
try {
|
|
93
|
+
const response = await this.generationClient.post(this.getGenerationEndpoint(), {
|
|
94
|
+
...promptData,
|
|
95
|
+
});
|
|
96
|
+
if (!response.data?.output?.text) {
|
|
97
|
+
throw new Error("Invalid response format from DashScope API");
|
|
98
|
+
}
|
|
99
|
+
const result = response.data.output.text.trim();
|
|
100
|
+
return this.sanitizeTranslation(result);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.handleApiError(error, this.name);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}, {
|
|
107
|
+
maxRetries: options.maxRetries || 2,
|
|
108
|
+
initialDelay: options.initialDelay || 1000,
|
|
109
|
+
context: "DashScope Provider Analysis",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Lazy singleton - created on first use
|
|
114
|
+
let dashscopeProvider = null;
|
|
115
|
+
function getProvider() {
|
|
116
|
+
if (!dashscopeProvider) {
|
|
117
|
+
dashscopeProvider = new DashScopeProvider();
|
|
118
|
+
}
|
|
119
|
+
return dashscopeProvider;
|
|
120
|
+
}
|
|
121
|
+
// Export both class and legacy functions
|
|
122
|
+
async function translate(text, sourceLang, targetLang, options = {}) {
|
|
123
|
+
return getProvider().translate(text, sourceLang, targetLang, options);
|
|
124
|
+
}
|
|
125
|
+
async function analyze(prompt, options = {}) {
|
|
126
|
+
return getProvider().analyze(prompt, options);
|
|
127
|
+
}
|
|
128
|
+
export { translate, analyze, DashScopeProvider };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import BaseProvider from "./base-provider.js";
|
|
3
|
+
import { getPrompt, getAnalysisPrompt } from "../utils/prompt-templates.js";
|
|
4
|
+
import RetryHelper from "../utils/retry-helper.js";
|
|
5
|
+
/**
|
|
6
|
+
* Provider implementation for DeepSeek API.
|
|
7
|
+
*/
|
|
8
|
+
class DeepSeekProvider extends BaseProvider {
|
|
9
|
+
client;
|
|
10
|
+
/**
|
|
11
|
+
* Create a new DeepSeekProvider instance.
|
|
12
|
+
* @param {ProviderConfig} config - Provider configuration.
|
|
13
|
+
*/
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
super("deepseek", config);
|
|
16
|
+
this.client = axios.create({
|
|
17
|
+
baseURL: "https://api.deepseek.com/v1",
|
|
18
|
+
headers: {
|
|
19
|
+
...this.commonHeaders,
|
|
20
|
+
Authorization: `Bearer ${this.getApiKey()}`,
|
|
21
|
+
},
|
|
22
|
+
timeout: 45000,
|
|
23
|
+
maxRedirects: 0,
|
|
24
|
+
validateStatus: (status) => status < 500,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
getApiKey() {
|
|
28
|
+
return process.env.DEEPSEEK_API_KEY;
|
|
29
|
+
}
|
|
30
|
+
getEndpoint() {
|
|
31
|
+
return "/chat/completions";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Translate text using DeepSeek.
|
|
35
|
+
*/
|
|
36
|
+
async translate(text, sourceLang, targetLang, options = {}) {
|
|
37
|
+
this.validateRequest(text, sourceLang, targetLang);
|
|
38
|
+
const config = this.getConfig(options.apiConfig?.deepseek);
|
|
39
|
+
const promptData = getPrompt("deepseek", sourceLang, targetLang, text, options);
|
|
40
|
+
return RetryHelper.withRetry(async () => {
|
|
41
|
+
try {
|
|
42
|
+
const response = await this.client.post(this.getEndpoint(), {
|
|
43
|
+
model: config.model || "deepseek-chat",
|
|
44
|
+
...promptData,
|
|
45
|
+
temperature: config.temperature || 0.3,
|
|
46
|
+
max_tokens: config.maxTokens || 2000,
|
|
47
|
+
});
|
|
48
|
+
this.validateResponse(response, this.name);
|
|
49
|
+
const translation = this.extractTranslation(response.data, this.name);
|
|
50
|
+
return this.sanitizeTranslation(translation);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.handleApiError(error, this.name);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}, {
|
|
57
|
+
maxRetries: options.retryOptions?.maxRetries || 3,
|
|
58
|
+
initialDelay: options.retryOptions?.initialDelay || 2000,
|
|
59
|
+
context: "DeepSeek Provider",
|
|
60
|
+
logContext: {
|
|
61
|
+
source: sourceLang,
|
|
62
|
+
target: targetLang,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async analyze(prompt, options = {}) {
|
|
67
|
+
const config = this.getConfig({
|
|
68
|
+
model: options.model || "deepseek-chat",
|
|
69
|
+
temperature: options.temperature || 0.2,
|
|
70
|
+
maxTokens: options.maxTokens || 1000,
|
|
71
|
+
});
|
|
72
|
+
const promptData = getAnalysisPrompt("deepseek", prompt, options);
|
|
73
|
+
return RetryHelper.withRetry(async () => {
|
|
74
|
+
try {
|
|
75
|
+
const response = await this.client.post(this.getEndpoint(), {
|
|
76
|
+
model: config.model,
|
|
77
|
+
...promptData,
|
|
78
|
+
temperature: config.temperature,
|
|
79
|
+
max_tokens: config.maxTokens,
|
|
80
|
+
});
|
|
81
|
+
this.validateResponse(response, this.name);
|
|
82
|
+
const result = this.extractTranslation(response.data, this.name);
|
|
83
|
+
return this.sanitizeTranslation(result);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.handleApiError(error, this.name);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}, {
|
|
90
|
+
maxRetries: options.maxRetries || 2,
|
|
91
|
+
initialDelay: options.initialDelay || 1000,
|
|
92
|
+
context: "DeepSeek Provider Analysis",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Lazy singleton - created on first use
|
|
97
|
+
let deepseekProvider = null;
|
|
98
|
+
function getProvider() {
|
|
99
|
+
if (!deepseekProvider) {
|
|
100
|
+
deepseekProvider = new DeepSeekProvider();
|
|
101
|
+
}
|
|
102
|
+
return deepseekProvider;
|
|
103
|
+
}
|
|
104
|
+
// Export both class and legacy functions
|
|
105
|
+
async function translate(text, sourceLang, targetLang, options = {}) {
|
|
106
|
+
return getProvider().translate(text, sourceLang, targetLang, options);
|
|
107
|
+
}
|
|
108
|
+
async function analyze(prompt, options = {}) {
|
|
109
|
+
return getProvider().analyze(prompt, options);
|
|
110
|
+
}
|
|
111
|
+
export { translate, analyze, DeepSeekProvider };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import BaseProvider from "./base-provider.js";
|
|
3
|
+
import { getPrompt, getAnalysisPrompt } from "../utils/prompt-templates.js";
|
|
4
|
+
import RetryHelper from "../utils/retry-helper.js";
|
|
5
|
+
/**
|
|
6
|
+
* Provider implementation for Google Gemini models.
|
|
7
|
+
*/
|
|
8
|
+
class GeminiProvider extends BaseProvider {
|
|
9
|
+
client;
|
|
10
|
+
/**
|
|
11
|
+
* Create a new GeminiProvider instance.
|
|
12
|
+
* @param {ProviderConfig} config - Provider configuration.
|
|
13
|
+
*/
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
super("gemini", config);
|
|
16
|
+
this.client = axios.create({
|
|
17
|
+
baseURL: "https://generativelanguage.googleapis.com/v1beta",
|
|
18
|
+
headers: {
|
|
19
|
+
...this.commonHeaders,
|
|
20
|
+
},
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
maxRedirects: 0,
|
|
23
|
+
validateStatus: (status) => status < 500,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
getApiKey() {
|
|
27
|
+
return process.env.GEMINI_API_KEY;
|
|
28
|
+
}
|
|
29
|
+
getEndpoint(model) {
|
|
30
|
+
return `/models/${model || "gemini-2.0-flash-exp"}:generateContent`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Translate text using Gemini.
|
|
34
|
+
*/
|
|
35
|
+
async translate(text, sourceLang, targetLang, options = {}) {
|
|
36
|
+
this.validateRequest(text, sourceLang, targetLang);
|
|
37
|
+
const config = this.getConfig(options.apiConfig?.gemini);
|
|
38
|
+
const model = config.model || "gemini-2.0-flash-exp";
|
|
39
|
+
const apiKey = this.getApiKey();
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
throw new Error("GEMINI_API_KEY environment variable not found");
|
|
42
|
+
}
|
|
43
|
+
const promptData = getPrompt("gemini", sourceLang, targetLang, text, options);
|
|
44
|
+
return RetryHelper.withRetry(async () => {
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.client.post(this.getEndpoint(model), {
|
|
47
|
+
...promptData,
|
|
48
|
+
generationConfig: {
|
|
49
|
+
temperature: config.temperature || 0.3,
|
|
50
|
+
maxOutputTokens: config.maxTokens || 2048,
|
|
51
|
+
},
|
|
52
|
+
}, {
|
|
53
|
+
params: { key: apiKey },
|
|
54
|
+
});
|
|
55
|
+
if (!response.data?.candidates || response.data.candidates.length === 0) {
|
|
56
|
+
throw new Error("Failed to get translation candidate from Gemini API");
|
|
57
|
+
}
|
|
58
|
+
if (!response.data.candidates[0]?.content?.parts?.[0]?.text) {
|
|
59
|
+
throw new Error("Invalid response format from Gemini API");
|
|
60
|
+
}
|
|
61
|
+
const translation = response.data.candidates[0].content.parts[0].text.trim();
|
|
62
|
+
return this.sanitizeTranslation(translation);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
this.handleApiError(error, this.name);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}, {
|
|
69
|
+
maxRetries: options.retryOptions?.maxRetries || 2,
|
|
70
|
+
initialDelay: options.retryOptions?.initialDelay || 1000,
|
|
71
|
+
context: "Gemini Provider",
|
|
72
|
+
logContext: {
|
|
73
|
+
source: sourceLang,
|
|
74
|
+
target: targetLang,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async analyze(prompt, options = {}) {
|
|
79
|
+
const config = this.getConfig({
|
|
80
|
+
model: options.model || "gemini-2.0-flash-exp",
|
|
81
|
+
temperature: options.temperature || 0.2,
|
|
82
|
+
maxTokens: options.maxTokens || 1000,
|
|
83
|
+
});
|
|
84
|
+
const apiKey = this.getApiKey();
|
|
85
|
+
if (!apiKey) {
|
|
86
|
+
throw new Error("GEMINI_API_KEY environment variable not found");
|
|
87
|
+
}
|
|
88
|
+
const promptData = getAnalysisPrompt("gemini", prompt, options);
|
|
89
|
+
return RetryHelper.withRetry(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.client.post(this.getEndpoint(config.model), {
|
|
92
|
+
...promptData,
|
|
93
|
+
generationConfig: {
|
|
94
|
+
temperature: config.temperature,
|
|
95
|
+
maxOutputTokens: config.maxTokens,
|
|
96
|
+
},
|
|
97
|
+
}, {
|
|
98
|
+
params: { key: apiKey },
|
|
99
|
+
});
|
|
100
|
+
if (!response.data?.candidates || response.data.candidates.length === 0) {
|
|
101
|
+
throw new Error("Failed to get analysis result from Gemini API");
|
|
102
|
+
}
|
|
103
|
+
if (!response.data.candidates[0]?.content?.parts?.[0]?.text) {
|
|
104
|
+
throw new Error("Invalid response format from Gemini API");
|
|
105
|
+
}
|
|
106
|
+
const result = response.data.candidates[0].content.parts[0].text.trim();
|
|
107
|
+
return this.sanitizeTranslation(result);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.handleApiError(error, this.name);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}, {
|
|
114
|
+
maxRetries: options.maxRetries || 2,
|
|
115
|
+
initialDelay: options.initialDelay || 1000,
|
|
116
|
+
context: "Gemini Provider Analysis",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Lazy singleton - created on first use
|
|
121
|
+
let geminiProvider = null;
|
|
122
|
+
function getProvider() {
|
|
123
|
+
if (!geminiProvider) {
|
|
124
|
+
geminiProvider = new GeminiProvider();
|
|
125
|
+
}
|
|
126
|
+
return geminiProvider;
|
|
127
|
+
}
|
|
128
|
+
// Export both class and legacy functions
|
|
129
|
+
async function translate(text, sourceLang, targetLang, options = {}) {
|
|
130
|
+
return getProvider().translate(text, sourceLang, targetLang, options);
|
|
131
|
+
}
|
|
132
|
+
async function analyze(prompt, options = {}) {
|
|
133
|
+
return getProvider().analyze(prompt, options);
|
|
134
|
+
}
|
|
135
|
+
export { translate, analyze, GeminiProvider };
|