@djangocfg/llm 2.1.164
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 +181 -0
- package/dist/index.cjs +1164 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +164 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.mjs +1128 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers/index.cjs +317 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +30 -0
- package/dist/providers/index.d.ts +30 -0
- package/dist/providers/index.mjs +304 -0
- package/dist/providers/index.mjs.map +1 -0
- package/dist/sdkrouter-D8GMBmTi.d.ts +171 -0
- package/dist/sdkrouter-hlQlVd0v.d.cts +171 -0
- package/dist/text-utils-DoYqMIr6.d.ts +289 -0
- package/dist/text-utils-VXWN-8Oq.d.cts +289 -0
- package/dist/translator/index.cjs +794 -0
- package/dist/translator/index.cjs.map +1 -0
- package/dist/translator/index.d.cts +24 -0
- package/dist/translator/index.d.ts +24 -0
- package/dist/translator/index.mjs +769 -0
- package/dist/translator/index.mjs.map +1 -0
- package/dist/types-D6lazgm1.d.cts +59 -0
- package/dist/types-D6lazgm1.d.ts +59 -0
- package/package.json +82 -0
- package/src/client.ts +119 -0
- package/src/index.ts +70 -0
- package/src/providers/anthropic.ts +98 -0
- package/src/providers/base.ts +90 -0
- package/src/providers/index.ts +15 -0
- package/src/providers/openai.ts +73 -0
- package/src/providers/sdkrouter.ts +279 -0
- package/src/translator/cache.ts +237 -0
- package/src/translator/index.ts +55 -0
- package/src/translator/json-translator.ts +408 -0
- package/src/translator/prompts.ts +90 -0
- package/src/translator/text-utils.ts +148 -0
- package/src/translator/types.ts +112 -0
- package/src/translator/validator.ts +181 -0
- package/src/types.ts +85 -0
- package/src/utils/env.ts +67 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/json.ts +44 -0
- package/src/utils/schema.ts +153 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
7
|
+
|
|
8
|
+
// src/translator/text-utils.ts
|
|
9
|
+
function isTechnicalContent(text) {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (!trimmed) return true;
|
|
12
|
+
if (/^(https?:\/\/|\/\/|www\.)/i.test(trimmed)) return true;
|
|
13
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return true;
|
|
14
|
+
if (/^\/[a-zA-Z]/.test(trimmed)) return true;
|
|
15
|
+
if (/\.(js|ts|tsx|jsx|json|css|scss|html|md|py|go|rs)$/i.test(trimmed)) return true;
|
|
16
|
+
if (/^[\d.,]+%?$/.test(trimmed)) return true;
|
|
17
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(trimmed)) return true;
|
|
18
|
+
if (/^(\{[^}]+\}|\{\{[^}]+\}\}|%[sd]|\$\d+)$/.test(trimmed)) return true;
|
|
19
|
+
if (/^[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+$/.test(trimmed)) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function containsCJK(text) {
|
|
23
|
+
return /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(text);
|
|
24
|
+
}
|
|
25
|
+
function containsCyrillic(text) {
|
|
26
|
+
return /[\u0400-\u04ff]/.test(text);
|
|
27
|
+
}
|
|
28
|
+
function containsArabic(text) {
|
|
29
|
+
return /[\u0600-\u06ff]/.test(text);
|
|
30
|
+
}
|
|
31
|
+
function detectScript(text) {
|
|
32
|
+
if (containsCJK(text)) return "cjk";
|
|
33
|
+
if (containsCyrillic(text)) return "cyrillic";
|
|
34
|
+
if (containsArabic(text)) return "arabic";
|
|
35
|
+
if (/[a-zA-Z]/.test(text)) return "latin";
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
function extractPlaceholders(text) {
|
|
39
|
+
const patterns = [
|
|
40
|
+
/\{[^}]+\}/g,
|
|
41
|
+
// {name}
|
|
42
|
+
/\{\{[^}]+\}\}/g,
|
|
43
|
+
// {{name}}
|
|
44
|
+
/%[sd]/g,
|
|
45
|
+
// %s, %d
|
|
46
|
+
/\$\d+/g
|
|
47
|
+
// $1, $2
|
|
48
|
+
];
|
|
49
|
+
const placeholders = [];
|
|
50
|
+
for (const pattern of patterns) {
|
|
51
|
+
const matches = text.match(pattern);
|
|
52
|
+
if (matches) {
|
|
53
|
+
placeholders.push(...matches);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...new Set(placeholders)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/translator/validator.ts
|
|
60
|
+
function validateJsonKeys(original, translated, path = "") {
|
|
61
|
+
const errors = [];
|
|
62
|
+
if (typeof original !== typeof translated) {
|
|
63
|
+
errors.push(`Type mismatch at ${path || "root"}: expected ${typeof original}, got ${typeof translated}`);
|
|
64
|
+
return { valid: false, errors };
|
|
65
|
+
}
|
|
66
|
+
if (original === null || translated === null) {
|
|
67
|
+
if (original !== translated) {
|
|
68
|
+
errors.push(`Null mismatch at ${path || "root"}`);
|
|
69
|
+
}
|
|
70
|
+
return { valid: errors.length === 0, errors };
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(original)) {
|
|
73
|
+
if (!Array.isArray(translated)) {
|
|
74
|
+
errors.push(`Expected array at ${path || "root"}`);
|
|
75
|
+
return { valid: false, errors };
|
|
76
|
+
}
|
|
77
|
+
if (original.length !== translated.length) {
|
|
78
|
+
errors.push(
|
|
79
|
+
`Array length mismatch at ${path || "root"}: expected ${original.length}, got ${translated.length}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const minLen = Math.min(original.length, translated.length);
|
|
83
|
+
for (let i = 0; i < minLen; i++) {
|
|
84
|
+
const result = validateJsonKeys(
|
|
85
|
+
original[i],
|
|
86
|
+
translated[i],
|
|
87
|
+
`${path}[${i}]`
|
|
88
|
+
);
|
|
89
|
+
errors.push(...result.errors);
|
|
90
|
+
}
|
|
91
|
+
return { valid: errors.length === 0, errors };
|
|
92
|
+
}
|
|
93
|
+
if (typeof original === "object") {
|
|
94
|
+
if (typeof translated !== "object" || Array.isArray(translated)) {
|
|
95
|
+
errors.push(`Expected object at ${path || "root"}`);
|
|
96
|
+
return { valid: false, errors };
|
|
97
|
+
}
|
|
98
|
+
const origKeys = Object.keys(original);
|
|
99
|
+
const transKeys = Object.keys(translated);
|
|
100
|
+
for (const key of origKeys) {
|
|
101
|
+
if (!transKeys.includes(key)) {
|
|
102
|
+
errors.push(`Missing key: ${path ? `${path}.${key}` : key}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const key of transKeys) {
|
|
106
|
+
if (!origKeys.includes(key)) {
|
|
107
|
+
errors.push(
|
|
108
|
+
`Unexpected key: ${path ? `${path}.${key}` : key} (key was translated?)`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const key of origKeys) {
|
|
113
|
+
if (transKeys.includes(key)) {
|
|
114
|
+
const result = validateJsonKeys(
|
|
115
|
+
original[key],
|
|
116
|
+
translated[key],
|
|
117
|
+
path ? `${path}.${key}` : key
|
|
118
|
+
);
|
|
119
|
+
errors.push(...result.errors);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { valid: errors.length === 0, errors };
|
|
123
|
+
}
|
|
124
|
+
return { valid: true, errors: [] };
|
|
125
|
+
}
|
|
126
|
+
function validatePlaceholders(original, translated, path = "") {
|
|
127
|
+
const errors = [];
|
|
128
|
+
if (typeof original === "string" && typeof translated === "string") {
|
|
129
|
+
const origPlaceholders = extractPlaceholders(original);
|
|
130
|
+
const transPlaceholders = extractPlaceholders(translated);
|
|
131
|
+
for (const placeholder of origPlaceholders) {
|
|
132
|
+
if (!transPlaceholders.includes(placeholder)) {
|
|
133
|
+
errors.push(
|
|
134
|
+
`Missing placeholder "${placeholder}" at ${path || "root"}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const placeholder of transPlaceholders) {
|
|
139
|
+
if (!origPlaceholders.includes(placeholder)) {
|
|
140
|
+
errors.push(
|
|
141
|
+
`Extra placeholder "${placeholder}" at ${path || "root"}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else if (Array.isArray(original) && Array.isArray(translated)) {
|
|
146
|
+
const minLen = Math.min(original.length, translated.length);
|
|
147
|
+
for (let i = 0; i < minLen; i++) {
|
|
148
|
+
const result = validatePlaceholders(
|
|
149
|
+
original[i],
|
|
150
|
+
translated[i],
|
|
151
|
+
`${path}[${i}]`
|
|
152
|
+
);
|
|
153
|
+
errors.push(...result.errors);
|
|
154
|
+
}
|
|
155
|
+
} else if (typeof original === "object" && original !== null && typeof translated === "object" && translated !== null) {
|
|
156
|
+
for (const key of Object.keys(original)) {
|
|
157
|
+
if (key in translated) {
|
|
158
|
+
const result = validatePlaceholders(
|
|
159
|
+
original[key],
|
|
160
|
+
translated[key],
|
|
161
|
+
path ? `${path}.${key}` : key
|
|
162
|
+
);
|
|
163
|
+
errors.push(...result.errors);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { valid: errors.length === 0, errors };
|
|
168
|
+
}
|
|
169
|
+
function validateTranslation(original, translated) {
|
|
170
|
+
const keyResult = validateJsonKeys(original, translated);
|
|
171
|
+
const placeholderResult = validatePlaceholders(original, translated);
|
|
172
|
+
return {
|
|
173
|
+
valid: keyResult.valid && placeholderResult.valid,
|
|
174
|
+
errors: [...keyResult.errors, ...placeholderResult.errors]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/utils/json.ts
|
|
179
|
+
function extractJson(text) {
|
|
180
|
+
let jsonStr = text.trim();
|
|
181
|
+
if (jsonStr.startsWith("```json")) {
|
|
182
|
+
jsonStr = jsonStr.slice(7);
|
|
183
|
+
} else if (jsonStr.startsWith("```")) {
|
|
184
|
+
jsonStr = jsonStr.slice(3);
|
|
185
|
+
}
|
|
186
|
+
if (jsonStr.endsWith("```")) {
|
|
187
|
+
jsonStr = jsonStr.slice(0, -3);
|
|
188
|
+
}
|
|
189
|
+
jsonStr = jsonStr.trim();
|
|
190
|
+
const jsonStart = jsonStr.search(/[\[{]/);
|
|
191
|
+
const jsonEndBracket = jsonStr.lastIndexOf("]");
|
|
192
|
+
const jsonEndBrace = jsonStr.lastIndexOf("}");
|
|
193
|
+
const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
|
|
194
|
+
if (jsonStart !== -1 && jsonEnd !== -1) {
|
|
195
|
+
jsonStr = jsonStr.slice(jsonStart, jsonEnd + 1);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(jsonStr);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Failed to parse JSON from LLM response: ${error instanceof Error ? error.message : "Unknown error"}
|
|
202
|
+
|
|
203
|
+
Response:
|
|
204
|
+
${text}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/providers/base.ts
|
|
210
|
+
var BaseLLMProvider = class {
|
|
211
|
+
constructor(config) {
|
|
212
|
+
__publicField(this, "config");
|
|
213
|
+
this.config = {
|
|
214
|
+
model: config.model ?? "gpt-4o-mini",
|
|
215
|
+
temperature: config.temperature ?? 0.1,
|
|
216
|
+
maxTokens: config.maxTokens ?? 4096,
|
|
217
|
+
...config
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Send single chat message
|
|
222
|
+
*/
|
|
223
|
+
async chat(prompt, options) {
|
|
224
|
+
const messages = [];
|
|
225
|
+
if (options?.system) {
|
|
226
|
+
messages.push({ role: "system", content: options.system });
|
|
227
|
+
}
|
|
228
|
+
messages.push({ role: "user", content: prompt });
|
|
229
|
+
return this.chatMessages(messages, options);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get JSON response
|
|
233
|
+
*/
|
|
234
|
+
async json(prompt, options) {
|
|
235
|
+
const systemPrompt = `${options?.system ?? ""}
|
|
236
|
+
|
|
237
|
+
Respond with valid JSON only. No markdown, no explanations.`.trim();
|
|
238
|
+
const response = await this.chat(prompt, {
|
|
239
|
+
...options,
|
|
240
|
+
system: systemPrompt,
|
|
241
|
+
temperature: options?.temperature ?? 0
|
|
242
|
+
});
|
|
243
|
+
return extractJson(response.content);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get JSON response with schema hint
|
|
247
|
+
*/
|
|
248
|
+
async jsonSchema(prompt, schema, options) {
|
|
249
|
+
const systemPrompt = `${options?.system ?? ""}
|
|
250
|
+
|
|
251
|
+
Respond with valid JSON matching this schema:
|
|
252
|
+
${schema}
|
|
253
|
+
|
|
254
|
+
No markdown, no explanations.`.trim();
|
|
255
|
+
const response = await this.chat(prompt, {
|
|
256
|
+
...options,
|
|
257
|
+
system: systemPrompt,
|
|
258
|
+
temperature: options?.temperature ?? 0
|
|
259
|
+
});
|
|
260
|
+
return extractJson(response.content);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/providers/openai.ts
|
|
265
|
+
var OpenAIProvider = class extends BaseLLMProvider {
|
|
266
|
+
constructor(config) {
|
|
267
|
+
super({
|
|
268
|
+
model: config.model ?? "gpt-4o-mini",
|
|
269
|
+
...config
|
|
270
|
+
});
|
|
271
|
+
__publicField(this, "provider", "openai");
|
|
272
|
+
__publicField(this, "client");
|
|
273
|
+
if (!config.apiKey) {
|
|
274
|
+
throw new Error("OpenAI API key is required");
|
|
275
|
+
}
|
|
276
|
+
this.client = new OpenAI({
|
|
277
|
+
apiKey: config.apiKey,
|
|
278
|
+
baseURL: config.baseUrl
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async chatMessages(messages, options) {
|
|
282
|
+
const model = options?.model ?? this.config.model;
|
|
283
|
+
const temperature = options?.temperature ?? this.config.temperature;
|
|
284
|
+
const maxTokens = options?.maxTokens ?? this.config.maxTokens;
|
|
285
|
+
const allMessages = options?.system ? [{ role: "system", content: options.system }, ...messages] : messages;
|
|
286
|
+
const response = await this.client.chat.completions.create({
|
|
287
|
+
model,
|
|
288
|
+
messages: allMessages.map((m) => ({
|
|
289
|
+
role: m.role,
|
|
290
|
+
content: m.content
|
|
291
|
+
})),
|
|
292
|
+
temperature,
|
|
293
|
+
max_tokens: maxTokens
|
|
294
|
+
});
|
|
295
|
+
const choice = response.choices[0];
|
|
296
|
+
return {
|
|
297
|
+
content: choice.message.content ?? "",
|
|
298
|
+
model: response.model,
|
|
299
|
+
usage: response.usage ? {
|
|
300
|
+
promptTokens: response.usage.prompt_tokens,
|
|
301
|
+
completionTokens: response.usage.completion_tokens,
|
|
302
|
+
totalTokens: response.usage.total_tokens
|
|
303
|
+
} : void 0,
|
|
304
|
+
finishReason: choice.finish_reason ?? void 0
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var AnthropicProvider = class extends BaseLLMProvider {
|
|
309
|
+
constructor(config) {
|
|
310
|
+
super({
|
|
311
|
+
model: config.model ?? "claude-3-5-haiku-latest",
|
|
312
|
+
...config
|
|
313
|
+
});
|
|
314
|
+
__publicField(this, "provider", "anthropic");
|
|
315
|
+
__publicField(this, "client");
|
|
316
|
+
if (!config.apiKey) {
|
|
317
|
+
throw new Error("Anthropic API key is required");
|
|
318
|
+
}
|
|
319
|
+
this.client = new OpenAI({
|
|
320
|
+
apiKey: config.apiKey,
|
|
321
|
+
baseURL: config.baseUrl ?? "https://api.anthropic.com/v1",
|
|
322
|
+
defaultHeaders: {
|
|
323
|
+
"anthropic-version": "2023-06-01",
|
|
324
|
+
"x-api-key": config.apiKey
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async chatMessages(messages, options) {
|
|
329
|
+
const model = options?.model ?? this.config.model;
|
|
330
|
+
const temperature = options?.temperature ?? this.config.temperature;
|
|
331
|
+
const maxTokens = options?.maxTokens ?? this.config.maxTokens;
|
|
332
|
+
const systemMessage = options?.system ? options.system : messages.find((m) => m.role === "system")?.content;
|
|
333
|
+
const userMessages = messages.filter((m) => m.role !== "system");
|
|
334
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: {
|
|
337
|
+
"Content-Type": "application/json",
|
|
338
|
+
"x-api-key": this.config.apiKey,
|
|
339
|
+
"anthropic-version": "2023-06-01"
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
model,
|
|
343
|
+
max_tokens: maxTokens,
|
|
344
|
+
temperature,
|
|
345
|
+
system: systemMessage,
|
|
346
|
+
messages: userMessages.map((m) => ({
|
|
347
|
+
role: m.role,
|
|
348
|
+
content: m.content
|
|
349
|
+
}))
|
|
350
|
+
})
|
|
351
|
+
});
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
const error = await response.text();
|
|
354
|
+
throw new Error(`Anthropic API error: ${response.status} ${error}`);
|
|
355
|
+
}
|
|
356
|
+
const data = await response.json();
|
|
357
|
+
return {
|
|
358
|
+
content: data.content?.[0]?.text ?? "",
|
|
359
|
+
model: data.model,
|
|
360
|
+
usage: data.usage ? {
|
|
361
|
+
promptTokens: data.usage.input_tokens,
|
|
362
|
+
completionTokens: data.usage.output_tokens,
|
|
363
|
+
totalTokens: data.usage.input_tokens + data.usage.output_tokens
|
|
364
|
+
} : void 0,
|
|
365
|
+
finishReason: data.stop_reason ?? void 0
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
var SDKROUTER_BASE_URL = "https://llm.sdkrouter.com/v1";
|
|
370
|
+
function buildModelAlias(tier, options) {
|
|
371
|
+
const parts = [tier];
|
|
372
|
+
if (options) {
|
|
373
|
+
if (options.vision) parts.push("vision");
|
|
374
|
+
if (options.tools) parts.push("tools");
|
|
375
|
+
if (options.agents) parts.push("agents");
|
|
376
|
+
if (options.json) parts.push("json");
|
|
377
|
+
if (options.streaming) parts.push("streaming");
|
|
378
|
+
if (options.long) parts.push("long");
|
|
379
|
+
if (options.image) parts.push("image");
|
|
380
|
+
if (options.code) parts.push("code");
|
|
381
|
+
if (options.reasoning) parts.push("reasoning");
|
|
382
|
+
if (options.creative) parts.push("creative");
|
|
383
|
+
if (options.chat) parts.push("chat");
|
|
384
|
+
if (options.analysis) parts.push("analysis");
|
|
385
|
+
}
|
|
386
|
+
return "@" + parts.join("+");
|
|
387
|
+
}
|
|
388
|
+
var Model = {
|
|
389
|
+
/** Cheapest available model */
|
|
390
|
+
cheap: (options) => buildModelAlias("cheap", options),
|
|
391
|
+
/** Budget-friendly with decent quality */
|
|
392
|
+
budget: (options) => buildModelAlias("budget", options),
|
|
393
|
+
/** Standard tier */
|
|
394
|
+
standard: (options) => buildModelAlias("standard", options),
|
|
395
|
+
/** Best quality/price ratio (recommended) */
|
|
396
|
+
balanced: (options) => buildModelAlias("balanced", options),
|
|
397
|
+
/** Highest quality model */
|
|
398
|
+
smart: (options) => buildModelAlias("smart", options),
|
|
399
|
+
/** Lowest latency model */
|
|
400
|
+
fast: (options) => buildModelAlias("fast", options),
|
|
401
|
+
/** Top-tier premium model */
|
|
402
|
+
premium: (options) => buildModelAlias("premium", options),
|
|
403
|
+
/**
|
|
404
|
+
* Build alias from raw strings (escape hatch)
|
|
405
|
+
*
|
|
406
|
+
* @example Model.alias('cheap', 'vision', 'code') // '@cheap+vision+code'
|
|
407
|
+
*/
|
|
408
|
+
alias: (tier, ...modifiers) => "@" + [tier, ...modifiers].join("+")
|
|
409
|
+
};
|
|
410
|
+
var ModelPresets = {
|
|
411
|
+
/** Translation: cheap + json mode */
|
|
412
|
+
translation: Model.cheap({ json: true }),
|
|
413
|
+
/** Code generation: balanced + code */
|
|
414
|
+
code: Model.balanced({ code: true }),
|
|
415
|
+
/** Code with tools: balanced + code + tools */
|
|
416
|
+
codeWithTools: Model.balanced({ code: true, tools: true }),
|
|
417
|
+
/** Vision: balanced + vision */
|
|
418
|
+
vision: Model.balanced({ vision: true }),
|
|
419
|
+
/** Reasoning: smart + reasoning */
|
|
420
|
+
reasoning: Model.smart({ reasoning: true }),
|
|
421
|
+
/** Creative writing: balanced + creative */
|
|
422
|
+
creative: Model.balanced({ creative: true }),
|
|
423
|
+
/** Fast chat: fast + chat */
|
|
424
|
+
fastChat: Model.fast({ chat: true }),
|
|
425
|
+
/** Analysis: balanced + analysis */
|
|
426
|
+
analysis: Model.balanced({ analysis: true }),
|
|
427
|
+
/** Agents: smart + agents + tools */
|
|
428
|
+
agents: Model.smart({ agents: true, tools: true })
|
|
429
|
+
};
|
|
430
|
+
var SDKRouterProvider = class extends BaseLLMProvider {
|
|
431
|
+
constructor(config) {
|
|
432
|
+
const model = config.model ?? (config.tier ? buildModelAlias(config.tier, config.modelOptions) : "@balanced");
|
|
433
|
+
super({
|
|
434
|
+
model,
|
|
435
|
+
...config
|
|
436
|
+
});
|
|
437
|
+
__publicField(this, "provider", "sdkrouter");
|
|
438
|
+
__publicField(this, "client");
|
|
439
|
+
if (!config.apiKey) {
|
|
440
|
+
throw new Error("SDKRouter API key is required (SDKROUTER_API_KEY)");
|
|
441
|
+
}
|
|
442
|
+
this.client = new OpenAI({
|
|
443
|
+
apiKey: config.apiKey,
|
|
444
|
+
baseURL: config.baseUrl ?? SDKROUTER_BASE_URL
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async chatMessages(messages, options) {
|
|
448
|
+
const model = options?.model ?? this.config.model;
|
|
449
|
+
const temperature = options?.temperature ?? this.config.temperature;
|
|
450
|
+
const maxTokens = options?.maxTokens ?? this.config.maxTokens;
|
|
451
|
+
const allMessages = options?.system ? [{ role: "system", content: options.system }, ...messages] : messages;
|
|
452
|
+
const response = await this.client.chat.completions.create({
|
|
453
|
+
model,
|
|
454
|
+
messages: allMessages.map((m) => ({
|
|
455
|
+
role: m.role,
|
|
456
|
+
content: m.content
|
|
457
|
+
})),
|
|
458
|
+
temperature,
|
|
459
|
+
max_tokens: maxTokens
|
|
460
|
+
});
|
|
461
|
+
const choice = response.choices[0];
|
|
462
|
+
return {
|
|
463
|
+
content: choice.message.content ?? "",
|
|
464
|
+
model: response.model,
|
|
465
|
+
usage: response.usage ? {
|
|
466
|
+
promptTokens: response.usage.prompt_tokens,
|
|
467
|
+
completionTokens: response.usage.completion_tokens,
|
|
468
|
+
totalTokens: response.usage.total_tokens
|
|
469
|
+
} : void 0,
|
|
470
|
+
finishReason: choice.finish_reason ?? void 0
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/utils/env.ts
|
|
476
|
+
function getApiKey(provider) {
|
|
477
|
+
if (provider === "sdkrouter") {
|
|
478
|
+
return process.env.SDKROUTER_API_KEY;
|
|
479
|
+
}
|
|
480
|
+
if (provider === "openai") {
|
|
481
|
+
return process.env.OPENAI_API_KEY;
|
|
482
|
+
}
|
|
483
|
+
if (provider === "anthropic") {
|
|
484
|
+
return process.env.ANTHROPIC_API_KEY;
|
|
485
|
+
}
|
|
486
|
+
return process.env.SDKROUTER_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
487
|
+
}
|
|
488
|
+
function detectProvider() {
|
|
489
|
+
const explicit = process.env.LLM_PROVIDER?.toLowerCase();
|
|
490
|
+
if (explicit === "sdkrouter" || explicit === "openai" || explicit === "anthropic") {
|
|
491
|
+
return explicit;
|
|
492
|
+
}
|
|
493
|
+
if (process.env.SDKROUTER_API_KEY) {
|
|
494
|
+
return "sdkrouter";
|
|
495
|
+
}
|
|
496
|
+
if (process.env.OPENAI_API_KEY) {
|
|
497
|
+
return "openai";
|
|
498
|
+
}
|
|
499
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
500
|
+
return "anthropic";
|
|
501
|
+
}
|
|
502
|
+
return void 0;
|
|
503
|
+
}
|
|
504
|
+
function getDefaultModel(provider) {
|
|
505
|
+
if (provider === "sdkrouter") {
|
|
506
|
+
return process.env.SDKROUTER_MODEL || "@balanced";
|
|
507
|
+
}
|
|
508
|
+
if (provider === "openai") {
|
|
509
|
+
return process.env.OPENAI_MODEL || "gpt-4o-mini";
|
|
510
|
+
}
|
|
511
|
+
if (provider === "anthropic") {
|
|
512
|
+
return process.env.ANTHROPIC_MODEL || "claude-3-5-haiku-latest";
|
|
513
|
+
}
|
|
514
|
+
return "@balanced";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/client.ts
|
|
518
|
+
var LLMError = class extends Error {
|
|
519
|
+
constructor(message) {
|
|
520
|
+
super(message);
|
|
521
|
+
this.name = "LLMError";
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
function createLLMClient(config) {
|
|
525
|
+
const provider = config?.provider ?? detectProvider();
|
|
526
|
+
if (!provider) {
|
|
527
|
+
throw new LLMError(
|
|
528
|
+
"No LLM provider configured. Set SDKROUTER_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY environment variable."
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
const apiKey = config?.apiKey ?? getApiKey(provider);
|
|
532
|
+
if (!apiKey) {
|
|
533
|
+
const envVar = provider === "sdkrouter" ? "SDKROUTER_API_KEY" : provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
534
|
+
throw new LLMError(
|
|
535
|
+
`No API key found for ${provider}. Set ${envVar} environment variable.`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
const model = config?.model ?? getDefaultModel(provider);
|
|
539
|
+
const fullConfig = {
|
|
540
|
+
...config,
|
|
541
|
+
provider,
|
|
542
|
+
apiKey,
|
|
543
|
+
model
|
|
544
|
+
};
|
|
545
|
+
if (provider === "sdkrouter") {
|
|
546
|
+
return new SDKRouterProvider(fullConfig);
|
|
547
|
+
}
|
|
548
|
+
if (provider === "openai") {
|
|
549
|
+
return new OpenAIProvider(fullConfig);
|
|
550
|
+
}
|
|
551
|
+
if (provider === "anthropic") {
|
|
552
|
+
return new AnthropicProvider(fullConfig);
|
|
553
|
+
}
|
|
554
|
+
throw new LLMError(`Unknown provider: ${provider}`);
|
|
555
|
+
}
|
|
556
|
+
function createSDKRouterClient(config) {
|
|
557
|
+
return createLLMClient({ ...config, provider: "sdkrouter" });
|
|
558
|
+
}
|
|
559
|
+
function createOpenAIClient(config) {
|
|
560
|
+
return createLLMClient({ ...config, provider: "openai" });
|
|
561
|
+
}
|
|
562
|
+
function createAnthropicClient(config) {
|
|
563
|
+
return createLLMClient({ ...config, provider: "anthropic" });
|
|
564
|
+
}
|
|
565
|
+
function isLLMConfigured() {
|
|
566
|
+
return detectProvider() !== void 0;
|
|
567
|
+
}
|
|
568
|
+
function getConfiguredProvider() {
|
|
569
|
+
return detectProvider();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/translator/types.ts
|
|
573
|
+
var LANGUAGE_NAMES = {
|
|
574
|
+
en: "English",
|
|
575
|
+
ru: "Russian",
|
|
576
|
+
ko: "Korean",
|
|
577
|
+
ja: "Japanese",
|
|
578
|
+
zh: "Chinese",
|
|
579
|
+
de: "German",
|
|
580
|
+
fr: "French",
|
|
581
|
+
es: "Spanish",
|
|
582
|
+
it: "Italian",
|
|
583
|
+
pt: "Portuguese",
|
|
584
|
+
"pt-BR": "Brazilian Portuguese",
|
|
585
|
+
ar: "Arabic",
|
|
586
|
+
nl: "Dutch",
|
|
587
|
+
tr: "Turkish",
|
|
588
|
+
pl: "Polish",
|
|
589
|
+
sv: "Swedish",
|
|
590
|
+
no: "Norwegian",
|
|
591
|
+
da: "Danish",
|
|
592
|
+
uk: "Ukrainian",
|
|
593
|
+
hi: "Hindi"
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/translator/prompts.ts
|
|
597
|
+
function getLanguageName(code) {
|
|
598
|
+
return LANGUAGE_NAMES[code] ?? code;
|
|
599
|
+
}
|
|
600
|
+
function buildJsonTranslationPrompt(json, sourceLanguage, targetLanguage) {
|
|
601
|
+
const sourceName = getLanguageName(sourceLanguage);
|
|
602
|
+
const targetName = getLanguageName(targetLanguage);
|
|
603
|
+
return `Translate all string VALUES in this JSON from ${sourceName} to ${targetName}.
|
|
604
|
+
|
|
605
|
+
Rules:
|
|
606
|
+
- Translate ALL string values to ${targetName}
|
|
607
|
+
- Keep JSON keys unchanged (English)
|
|
608
|
+
- Skip: URLs, emails, numbers, "SKIP"
|
|
609
|
+
- Keep placeholders: {name}, {{var}}, %s
|
|
610
|
+
|
|
611
|
+
${json}
|
|
612
|
+
|
|
613
|
+
Return ONLY the JSON with all values translated to ${targetName}:`;
|
|
614
|
+
}
|
|
615
|
+
var TranslationCache = class {
|
|
616
|
+
constructor(maxMemorySize = 1e3, storage) {
|
|
617
|
+
this.maxMemorySize = maxMemorySize;
|
|
618
|
+
this.storage = storage;
|
|
619
|
+
__publicField(this, "memoryCache", /* @__PURE__ */ new Map());
|
|
620
|
+
__publicField(this, "cacheOrder", []);
|
|
621
|
+
__publicField(this, "hits", 0);
|
|
622
|
+
__publicField(this, "misses", 0);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Generate hash for text
|
|
626
|
+
*/
|
|
627
|
+
getTextHash(text) {
|
|
628
|
+
return crypto.createHash("md5").update(text).digest("hex");
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get storage key for language pair
|
|
632
|
+
*/
|
|
633
|
+
getStorageKey(sourceLang, targetLang) {
|
|
634
|
+
return `translator:${sourceLang}-${targetLang}`;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Load from persistent storage
|
|
638
|
+
*/
|
|
639
|
+
loadFromStorage(sourceLang, targetLang) {
|
|
640
|
+
if (!this.storage) return {};
|
|
641
|
+
try {
|
|
642
|
+
const key = this.getStorageKey(sourceLang, targetLang);
|
|
643
|
+
const data = this.storage.getItem(key);
|
|
644
|
+
return data ? JSON.parse(data) : {};
|
|
645
|
+
} catch {
|
|
646
|
+
return {};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Save to persistent storage
|
|
651
|
+
*/
|
|
652
|
+
saveToStorage(sourceLang, targetLang, cache) {
|
|
653
|
+
if (!this.storage) return;
|
|
654
|
+
try {
|
|
655
|
+
const key = this.getStorageKey(sourceLang, targetLang);
|
|
656
|
+
this.storage.setItem(key, JSON.stringify(cache));
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Evict oldest entries if memory is full
|
|
662
|
+
*/
|
|
663
|
+
evictIfNeeded() {
|
|
664
|
+
while (this.memoryCache.size >= this.maxMemorySize && this.cacheOrder.length > 0) {
|
|
665
|
+
const oldestKey = this.cacheOrder.shift();
|
|
666
|
+
this.memoryCache.delete(oldestKey);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Get translation from cache
|
|
671
|
+
*/
|
|
672
|
+
get(text, sourceLang, targetLang) {
|
|
673
|
+
const textHash = this.getTextHash(text);
|
|
674
|
+
const cacheKey = `${sourceLang}-${targetLang}:${textHash}`;
|
|
675
|
+
if (this.memoryCache.has(cacheKey)) {
|
|
676
|
+
this.hits++;
|
|
677
|
+
return this.memoryCache.get(cacheKey);
|
|
678
|
+
}
|
|
679
|
+
const fileCache = this.loadFromStorage(sourceLang, targetLang);
|
|
680
|
+
if (textHash in fileCache) {
|
|
681
|
+
const translation = fileCache[textHash];
|
|
682
|
+
this.evictIfNeeded();
|
|
683
|
+
this.memoryCache.set(cacheKey, translation);
|
|
684
|
+
this.cacheOrder.push(cacheKey);
|
|
685
|
+
this.hits++;
|
|
686
|
+
return translation;
|
|
687
|
+
}
|
|
688
|
+
this.misses++;
|
|
689
|
+
return void 0;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Store translation in cache
|
|
693
|
+
*/
|
|
694
|
+
set(text, sourceLang, targetLang, translation) {
|
|
695
|
+
const textHash = this.getTextHash(text);
|
|
696
|
+
const cacheKey = `${sourceLang}-${targetLang}:${textHash}`;
|
|
697
|
+
this.evictIfNeeded();
|
|
698
|
+
this.memoryCache.set(cacheKey, translation);
|
|
699
|
+
if (!this.cacheOrder.includes(cacheKey)) {
|
|
700
|
+
this.cacheOrder.push(cacheKey);
|
|
701
|
+
}
|
|
702
|
+
const fileCache = this.loadFromStorage(sourceLang, targetLang);
|
|
703
|
+
fileCache[textHash] = translation;
|
|
704
|
+
this.saveToStorage(sourceLang, targetLang, fileCache);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Get multiple translations at once
|
|
708
|
+
*/
|
|
709
|
+
getMany(texts, sourceLang, targetLang) {
|
|
710
|
+
const cached = /* @__PURE__ */ new Map();
|
|
711
|
+
const uncached = [];
|
|
712
|
+
for (const text of texts) {
|
|
713
|
+
const translation = this.get(text, sourceLang, targetLang);
|
|
714
|
+
if (translation !== void 0) {
|
|
715
|
+
cached.set(text, translation);
|
|
716
|
+
} else {
|
|
717
|
+
uncached.push(text);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return { cached, uncached };
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Store multiple translations at once
|
|
724
|
+
*/
|
|
725
|
+
setMany(translations, sourceLang, targetLang) {
|
|
726
|
+
for (const [text, translation] of translations) {
|
|
727
|
+
this.set(text, sourceLang, targetLang, translation);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Clear cache
|
|
732
|
+
*/
|
|
733
|
+
clear(sourceLang, targetLang) {
|
|
734
|
+
if (sourceLang && targetLang) {
|
|
735
|
+
const prefix = `${sourceLang}-${targetLang}:`;
|
|
736
|
+
for (const key of [...this.memoryCache.keys()]) {
|
|
737
|
+
if (key.startsWith(prefix)) {
|
|
738
|
+
this.memoryCache.delete(key);
|
|
739
|
+
const idx = this.cacheOrder.indexOf(key);
|
|
740
|
+
if (idx !== -1) this.cacheOrder.splice(idx, 1);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (this.storage) {
|
|
744
|
+
this.storage.removeItem(this.getStorageKey(sourceLang, targetLang));
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
this.memoryCache.clear();
|
|
748
|
+
this.cacheOrder = [];
|
|
749
|
+
this.hits = 0;
|
|
750
|
+
this.misses = 0;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Get cache statistics
|
|
755
|
+
*/
|
|
756
|
+
getStats() {
|
|
757
|
+
const languagePairs = [];
|
|
758
|
+
const pairCounts = /* @__PURE__ */ new Map();
|
|
759
|
+
for (const key of this.memoryCache.keys()) {
|
|
760
|
+
const pair = key.split(":")[0];
|
|
761
|
+
pairCounts.set(pair, (pairCounts.get(pair) || 0) + 1);
|
|
762
|
+
}
|
|
763
|
+
for (const [pair, count] of pairCounts) {
|
|
764
|
+
languagePairs.push({ pair, translations: count });
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
memorySize: this.memoryCache.size,
|
|
768
|
+
hits: this.hits,
|
|
769
|
+
misses: this.misses,
|
|
770
|
+
languagePairs
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
function createCache(maxMemorySize, storage) {
|
|
775
|
+
return new TranslationCache(maxMemorySize, storage);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/translator/json-translator.ts
|
|
779
|
+
var DEFAULT_TRANSLATION_MODEL = "openai/gpt-4o-mini";
|
|
780
|
+
var JsonTranslator = class {
|
|
781
|
+
constructor(client, cache, defaultModel) {
|
|
782
|
+
this.client = client;
|
|
783
|
+
__publicField(this, "cache");
|
|
784
|
+
__publicField(this, "defaultModel");
|
|
785
|
+
this.cache = cache ?? createCache();
|
|
786
|
+
this.defaultModel = defaultModel ?? DEFAULT_TRANSLATION_MODEL;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Detect language from text
|
|
790
|
+
*/
|
|
791
|
+
detectLanguage(text) {
|
|
792
|
+
const script = detectScript(text);
|
|
793
|
+
switch (script) {
|
|
794
|
+
case "cjk":
|
|
795
|
+
return "zh";
|
|
796
|
+
case "cyrillic":
|
|
797
|
+
return "ru";
|
|
798
|
+
case "arabic":
|
|
799
|
+
return "ar";
|
|
800
|
+
default:
|
|
801
|
+
return "en";
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Check if text needs translation
|
|
806
|
+
*/
|
|
807
|
+
needsTranslation(text, sourceLang, targetLang) {
|
|
808
|
+
if (!text || !text.trim()) return false;
|
|
809
|
+
if (isTechnicalContent(text)) return false;
|
|
810
|
+
if (sourceLang === targetLang) return false;
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Translate single text
|
|
815
|
+
*/
|
|
816
|
+
async translateText(text, targetLanguage, options) {
|
|
817
|
+
if (!text || !text.trim()) return text;
|
|
818
|
+
const sourceLang = options?.sourceLanguage ?? "auto";
|
|
819
|
+
const actualSource = sourceLang === "auto" ? this.detectLanguage(text) : sourceLang;
|
|
820
|
+
if (!this.needsTranslation(text, actualSource, targetLanguage)) {
|
|
821
|
+
return text;
|
|
822
|
+
}
|
|
823
|
+
const cached = this.cache.get(text, actualSource, targetLanguage);
|
|
824
|
+
if (cached) return cached;
|
|
825
|
+
const response = await this.client.chat(
|
|
826
|
+
`Translate this text to ${targetLanguage}. Return ONLY the translation:
|
|
827
|
+
|
|
828
|
+
${text}`,
|
|
829
|
+
{
|
|
830
|
+
...options,
|
|
831
|
+
model: options?.model ?? this.defaultModel,
|
|
832
|
+
temperature: 0
|
|
833
|
+
}
|
|
834
|
+
);
|
|
835
|
+
const translated = response.content.trim();
|
|
836
|
+
this.cache.set(text, actualSource, targetLanguage, translated);
|
|
837
|
+
return translated;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Translate JSON object with smart text-level caching
|
|
841
|
+
*
|
|
842
|
+
* @example
|
|
843
|
+
* ```ts
|
|
844
|
+
* const translator = new JsonTranslator(llm)
|
|
845
|
+
* const result = await translator.translate(
|
|
846
|
+
* { title: 'Hello', items: ['World', 'Earth'] },
|
|
847
|
+
* 'ru'
|
|
848
|
+
* )
|
|
849
|
+
* // { data: { title: 'Привет', items: ['Мир', 'Земля'] }, valid: true, ... }
|
|
850
|
+
* ```
|
|
851
|
+
*/
|
|
852
|
+
async translate(data, targetLanguage, options) {
|
|
853
|
+
const sourceLang = options?.sourceLanguage ?? "auto";
|
|
854
|
+
const translatableTexts = this.extractTranslatableTexts(data, sourceLang, targetLanguage);
|
|
855
|
+
if (translatableTexts.size === 0) {
|
|
856
|
+
return {
|
|
857
|
+
data,
|
|
858
|
+
valid: true,
|
|
859
|
+
errors: [],
|
|
860
|
+
retries: 0,
|
|
861
|
+
sourceLanguage: sourceLang,
|
|
862
|
+
targetLanguage
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
let actualSource = sourceLang;
|
|
866
|
+
if (sourceLang === "auto") {
|
|
867
|
+
const firstText = [...translatableTexts][0];
|
|
868
|
+
actualSource = this.detectLanguage(firstText);
|
|
869
|
+
}
|
|
870
|
+
const { cached, uncached } = this.cache.getMany(
|
|
871
|
+
[...translatableTexts],
|
|
872
|
+
actualSource,
|
|
873
|
+
targetLanguage
|
|
874
|
+
);
|
|
875
|
+
if (uncached.length === 0) {
|
|
876
|
+
return {
|
|
877
|
+
data: this.applyTranslations(data, cached),
|
|
878
|
+
valid: true,
|
|
879
|
+
errors: [],
|
|
880
|
+
retries: 0,
|
|
881
|
+
sourceLanguage: actualSource,
|
|
882
|
+
targetLanguage
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const partialJson = this.createPartialJson(data, new Set(uncached));
|
|
886
|
+
const jsonStr = JSON.stringify(partialJson, null, 2);
|
|
887
|
+
try {
|
|
888
|
+
const prompt = buildJsonTranslationPrompt(jsonStr, actualSource, targetLanguage);
|
|
889
|
+
const response = await this.client.chat(prompt, {
|
|
890
|
+
...options,
|
|
891
|
+
model: options?.model ?? this.defaultModel,
|
|
892
|
+
temperature: 0
|
|
893
|
+
});
|
|
894
|
+
const translatedPartial = extractJson(response.content);
|
|
895
|
+
const newTranslations = this.extractTranslationsByComparison(
|
|
896
|
+
partialJson,
|
|
897
|
+
translatedPartial,
|
|
898
|
+
uncached
|
|
899
|
+
);
|
|
900
|
+
this.cache.setMany(newTranslations, actualSource, targetLanguage);
|
|
901
|
+
const allTranslations = new Map([...cached, ...newTranslations]);
|
|
902
|
+
return {
|
|
903
|
+
data: this.applyTranslations(data, allTranslations),
|
|
904
|
+
valid: true,
|
|
905
|
+
errors: [],
|
|
906
|
+
retries: 0,
|
|
907
|
+
sourceLanguage: actualSource,
|
|
908
|
+
targetLanguage
|
|
909
|
+
};
|
|
910
|
+
} catch (error) {
|
|
911
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
912
|
+
console.error("Translation failed:", errorMsg);
|
|
913
|
+
return {
|
|
914
|
+
data: cached.size > 0 ? this.applyTranslations(data, cached) : data,
|
|
915
|
+
valid: false,
|
|
916
|
+
errors: [errorMsg],
|
|
917
|
+
retries: 0,
|
|
918
|
+
sourceLanguage: actualSource,
|
|
919
|
+
targetLanguage
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Translate to multiple languages in parallel
|
|
925
|
+
*/
|
|
926
|
+
async translateToMany(data, targetLanguages, options) {
|
|
927
|
+
const results = /* @__PURE__ */ new Map();
|
|
928
|
+
const promises = targetLanguages.map(async (lang) => {
|
|
929
|
+
const result = await this.translate(data, lang, options);
|
|
930
|
+
return { lang, result };
|
|
931
|
+
});
|
|
932
|
+
const settled = await Promise.all(promises);
|
|
933
|
+
for (const { lang, result } of settled) {
|
|
934
|
+
results.set(lang, result);
|
|
935
|
+
}
|
|
936
|
+
return results;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Extract all translatable texts from object
|
|
940
|
+
*/
|
|
941
|
+
extractTranslatableTexts(obj, sourceLang, targetLang) {
|
|
942
|
+
const texts = /* @__PURE__ */ new Set();
|
|
943
|
+
const extract = (item) => {
|
|
944
|
+
if (typeof item === "string") {
|
|
945
|
+
if (this.needsTranslation(item, sourceLang, targetLang)) {
|
|
946
|
+
texts.add(item);
|
|
947
|
+
}
|
|
948
|
+
} else if (Array.isArray(item)) {
|
|
949
|
+
for (const i of item) extract(i);
|
|
950
|
+
} else if (item !== null && typeof item === "object") {
|
|
951
|
+
for (const v of Object.values(item)) extract(v);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
extract(obj);
|
|
955
|
+
return texts;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Apply translations to object
|
|
959
|
+
*/
|
|
960
|
+
applyTranslations(obj, translations) {
|
|
961
|
+
const apply = (item) => {
|
|
962
|
+
if (typeof item === "string") {
|
|
963
|
+
return translations.get(item) ?? item;
|
|
964
|
+
} else if (Array.isArray(item)) {
|
|
965
|
+
return item.map(apply);
|
|
966
|
+
} else if (item !== null && typeof item === "object") {
|
|
967
|
+
const result = {};
|
|
968
|
+
for (const [k, v] of Object.entries(item)) {
|
|
969
|
+
result[k] = apply(v);
|
|
970
|
+
}
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
973
|
+
return item;
|
|
974
|
+
};
|
|
975
|
+
return apply(obj);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Create partial JSON with only texts that need translation
|
|
979
|
+
*/
|
|
980
|
+
createPartialJson(data, textsToInclude) {
|
|
981
|
+
const filter = (obj) => {
|
|
982
|
+
if (typeof obj === "string") {
|
|
983
|
+
return textsToInclude.has(obj) ? obj : "SKIP";
|
|
984
|
+
} else if (Array.isArray(obj)) {
|
|
985
|
+
return obj.map(filter).filter((i) => this.hasTranslatable(i, textsToInclude));
|
|
986
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
987
|
+
const result = {};
|
|
988
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
989
|
+
const filtered = filter(v);
|
|
990
|
+
if (this.hasTranslatable(filtered, textsToInclude)) {
|
|
991
|
+
result[k] = filtered;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return result;
|
|
995
|
+
}
|
|
996
|
+
return obj;
|
|
997
|
+
};
|
|
998
|
+
return filter(data);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Check if object contains translatable text
|
|
1002
|
+
*/
|
|
1003
|
+
hasTranslatable(obj, textsSet) {
|
|
1004
|
+
if (typeof obj === "string") return textsSet.has(obj);
|
|
1005
|
+
if (Array.isArray(obj)) return obj.some((i) => this.hasTranslatable(i, textsSet));
|
|
1006
|
+
if (obj !== null && typeof obj === "object") {
|
|
1007
|
+
return Object.values(obj).some((v) => this.hasTranslatable(v, textsSet));
|
|
1008
|
+
}
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Extract translations by comparing original and translated JSON
|
|
1013
|
+
*/
|
|
1014
|
+
extractTranslationsByComparison(original, translated, uncachedTexts) {
|
|
1015
|
+
const translations = /* @__PURE__ */ new Map();
|
|
1016
|
+
const uncachedSet = new Set(uncachedTexts);
|
|
1017
|
+
const compare = (orig, trans) => {
|
|
1018
|
+
if (typeof orig === "string" && typeof trans === "string") {
|
|
1019
|
+
if (uncachedSet.has(orig) && trans !== "SKIP" && trans.trim()) {
|
|
1020
|
+
translations.set(orig, trans);
|
|
1021
|
+
}
|
|
1022
|
+
} else if (Array.isArray(orig) && Array.isArray(trans)) {
|
|
1023
|
+
for (let i = 0; i < Math.min(orig.length, trans.length); i++) {
|
|
1024
|
+
compare(orig[i], trans[i]);
|
|
1025
|
+
}
|
|
1026
|
+
} else if (orig !== null && typeof orig === "object" && trans !== null && typeof trans === "object") {
|
|
1027
|
+
for (const key of Object.keys(orig)) {
|
|
1028
|
+
if (key in trans) {
|
|
1029
|
+
compare(
|
|
1030
|
+
orig[key],
|
|
1031
|
+
trans[key]
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
compare(original, translated);
|
|
1038
|
+
return translations;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Get translation statistics
|
|
1042
|
+
*/
|
|
1043
|
+
getStats() {
|
|
1044
|
+
return this.cache.getStats();
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Clear translation cache
|
|
1048
|
+
*/
|
|
1049
|
+
clearCache(sourceLang, targetLang) {
|
|
1050
|
+
this.cache.clear(sourceLang, targetLang);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function createTranslator(client, configOrCache) {
|
|
1054
|
+
if (configOrCache instanceof TranslationCache) {
|
|
1055
|
+
return new JsonTranslator(client, configOrCache);
|
|
1056
|
+
}
|
|
1057
|
+
return new JsonTranslator(client, configOrCache?.cache, configOrCache?.model);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/utils/schema.ts
|
|
1061
|
+
function generateSchemaFromExample(example) {
|
|
1062
|
+
if (example === null) {
|
|
1063
|
+
return { type: "null" };
|
|
1064
|
+
}
|
|
1065
|
+
if (Array.isArray(example)) {
|
|
1066
|
+
const itemSchema = example.length > 0 ? generateSchemaFromExample(example[0]) : { type: "string" };
|
|
1067
|
+
return { type: "array", items: itemSchema };
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof example === "object") {
|
|
1070
|
+
const properties = {};
|
|
1071
|
+
const required = [];
|
|
1072
|
+
for (const [key, value] of Object.entries(example)) {
|
|
1073
|
+
properties[key] = generateSchemaFromExample(value);
|
|
1074
|
+
required.push(key);
|
|
1075
|
+
}
|
|
1076
|
+
return { type: "object", properties, required };
|
|
1077
|
+
}
|
|
1078
|
+
return { type: typeof example };
|
|
1079
|
+
}
|
|
1080
|
+
function schemaToPromptString(schema) {
|
|
1081
|
+
return JSON.stringify(schema, null, 2);
|
|
1082
|
+
}
|
|
1083
|
+
async function getStructuredOutput(client, prompt, schema, options) {
|
|
1084
|
+
const response = await client.chat(prompt, {
|
|
1085
|
+
...options,
|
|
1086
|
+
temperature: 0,
|
|
1087
|
+
system: options?.system ?? "Return ONLY valid JSON matching the requested structure."
|
|
1088
|
+
});
|
|
1089
|
+
try {
|
|
1090
|
+
const parsed = extractJson(response.content);
|
|
1091
|
+
const result = schema.safeParse(parsed);
|
|
1092
|
+
if (result.success) {
|
|
1093
|
+
return { data: result.data, response };
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
error: "Schema validation failed",
|
|
1097
|
+
raw: parsed
|
|
1098
|
+
};
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
return {
|
|
1101
|
+
error: error instanceof Error ? error.message : "Failed to parse JSON",
|
|
1102
|
+
raw: response.content
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function getStructuredOutputWithRetry(client, prompt, schema, options) {
|
|
1107
|
+
const maxRetries = options?.maxRetries ?? 2;
|
|
1108
|
+
let retries = 0;
|
|
1109
|
+
let lastError = "";
|
|
1110
|
+
let lastRaw;
|
|
1111
|
+
while (retries <= maxRetries) {
|
|
1112
|
+
const result = await getStructuredOutput(client, prompt, schema, options);
|
|
1113
|
+
if ("data" in result) {
|
|
1114
|
+
return { data: result.data, retries };
|
|
1115
|
+
}
|
|
1116
|
+
lastError = result.error;
|
|
1117
|
+
lastRaw = result.raw;
|
|
1118
|
+
retries++;
|
|
1119
|
+
if (retries <= maxRetries) {
|
|
1120
|
+
console.warn(`Structured output validation failed (attempt ${retries}/${maxRetries + 1})`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return { error: lastError, raw: lastRaw, retries };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export { JsonTranslator, LANGUAGE_NAMES, LLMError, Model, ModelPresets, SDKROUTER_BASE_URL, TranslationCache, buildModelAlias, createAnthropicClient, createCache, createLLMClient, createOpenAIClient, createSDKRouterClient, createTranslator, detectProvider, detectScript, extractJson, extractPlaceholders, generateSchemaFromExample, getApiKey, getConfiguredProvider, getDefaultModel, getStructuredOutput, getStructuredOutputWithRetry, isLLMConfigured, isTechnicalContent, schemaToPromptString, validateJsonKeys, validateTranslation, validateJsonKeys as validateTranslationKeys };
|
|
1127
|
+
//# sourceMappingURL=index.mjs.map
|
|
1128
|
+
//# sourceMappingURL=index.mjs.map
|