gptrans 2.2.0 → 2.2.4

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.
@@ -66,10 +66,18 @@ const model = ModelMix.new()
66
66
 
67
67
  If `sonnet45` fails, it automatically tries `gpt5mini`, then `gemini3flash`.
68
68
 
69
+ The equivalent `chain()` syntax accepts `shortcut@<effort>` to override unified effort for one model. `<effort>` must be an integer from `0` to `100`; models without the suffix keep their configured or provider-default effort:
70
+
71
+ ```javascript
72
+ const model = ModelMix.new()
73
+ .chain('sonnet5@50', 'gpt56luna@100');
74
+ ```
75
+
69
76
  ## Available Model Shorthands
70
77
 
71
78
  - **OpenAI**: `gpt52` `gpt51` `gpt5` `gpt5mini` `gpt5nano` `gpt41` `gpt41mini` `gpt41nano`
72
- - **Anthropic**: `opus46` `opus45` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
79
+ - **Anthropic**: `opus46` `opus45` `sonnet5` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
80
+ - **OpenAI Codex**: `gpt56sol` `gpt56terra` `gpt56luna`
73
81
  - **Google**: `gemini3pro` `gemini3flash` `gemini25pro` `gemini25flash`
74
82
  - **Grok**: `grok4` `grok41` (thinking variant available)
75
83
  - **Perplexity**: `sonar` `sonarPro`
@@ -158,11 +166,11 @@ const description = await model.message();
158
166
  const model = ModelMix.new().gpt5mini();
159
167
  model.setSystemFromFile('./prompts/system.md');
160
168
  model.addTextFromFile('./prompts/task.md');
161
- model.replace({
162
- '{role}': 'data analyst',
163
- '{language}': 'Spanish'
169
+ model.assign({
170
+ role: 'data analyst',
171
+ language: 'Spanish'
164
172
  });
165
- model.replaceKeyFromFile('{code}', './src/utils.js');
173
+ model.assignKeyFromFile('code', './src/utils.js');
166
174
  console.log(await model.message());
167
175
  ```
168
176
 
@@ -268,7 +276,7 @@ const reply = await chat.message(); // "Martin"
268
276
  - Use `.json()` for structured output instead of parsing text manually.
269
277
  - Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
270
278
  - For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
271
- - Template placeholders use `{key}` syntax in both system prompts and user messages.
279
+ - Template placeholders use EJS syntax such as `<%- key %>` in system prompts and user messages.
272
280
  - The library uses CommonJS internally (`require`) but supports ESM import via `{ ModelMix }`.
273
281
  - Available provider Mix classes for custom setups: `MixOpenAI`, `MixAnthropic`, `MixGoogle`, `MixPerplexity`, `MixGroq`, `MixTogether`, `MixGrok`, `MixOpenRouter`, `MixOllama`, `MixLMStudio`, `MixCustom`, `MixCerebras`, `MixFireworks`, `MixMiniMax`.
274
282
 
@@ -282,8 +290,9 @@ const reply = await chat.message(); // "Martin"
282
290
  | `.setSystemFromFile(path)` | `this` | Set system prompt from file |
283
291
  | `.addImage(path)` | `this` | Add image from file |
284
292
  | `.addImageFromUrl(url)` | `this` | Add image from URL or data URI |
285
- | `.replace({})` | `this` | Set placeholder replacements |
286
- | `.replaceKeyFromFile(key, path)` | `this` | Replace placeholder with file content |
293
+ | `.assign({})` | `this` | Assign EJS template data |
294
+ | `.assignKeyFromFile(key, path)` | `this` | Assign rendered file content to one template key |
295
+ | `.chain(...models)` | `this` | Attach ordered shortcuts; each may use `@<effort>` from `0` to `100` |
287
296
  | `.message()` | `Promise<string>` | Get text response |
288
297
  | `.json(example, desc?, opts?)` | `Promise<object>` | Get structured JSON |
289
298
  | `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
package/README.md CHANGED
@@ -30,7 +30,7 @@ npm install gptrans
30
30
 
31
31
  ### 🌐 Environment Setup
32
32
 
33
- On Node.js 20.6+, GPTrans loads a `.env` from the current working directory via `process.loadEnvFile()` when you construct `GPTrans` (missing or invalid files are ignored). Create a `.env` in your project root and add your API keys:
33
+ GPTrans reads provider credentials from `process.env` and does not load `.env` files itself. If you use a `.env` file, load it in your application before constructing `GPTrans`. Create the file in your project root and add your API keys:
34
34
 
35
35
  ```env
36
36
  OPENAI_API_KEY=your_openai_api_key
@@ -44,13 +44,15 @@ Here's a simple example to get you started:
44
44
 
45
45
  ```javascript
46
46
  import GPTrans from 'gptrans';
47
+ import { join } from 'node:path';
48
+ import { loadEnvFile } from 'node:process';
47
49
 
48
- const dbPath = new URL('./db', import.meta.url).pathname;
50
+ loadEnvFile(join(import.meta.dirname, '.env'));
51
+ const dbPath = join(import.meta.dirname, 'db');
49
52
  const gptrans = new GPTrans({
50
53
  path: dbPath,
51
54
  from: 'en-US',
52
- target: 'es-AR',
53
- model: 'sonnet45'
55
+ target: 'es-AR'
54
56
  });
55
57
 
56
58
  // Translate text with parameter substitution
@@ -79,7 +81,7 @@ When creating a new instance of GPTrans, you can customize:
79
81
  | `path` | Absolute directory path for the DeepBase translation cache | Required |
80
82
  | `from` | Source language locale (BCP 47) | `en-US` |
81
83
  | `target` | Target language locale (BCP 47) | `es` |
82
- | `model` | Translation model key or array of models for fallback | `sonnet45` `gpt41` |
84
+ | `model` | ModelMix shortcut or array for an ordered fallback chain; append `@<effort>` (`0`–`100`) to override one model | `['sonnet5@50', 'gpt56luna@100']` |
83
85
  | `batchThreshold` | Maximum number of characters to accumulate before triggering batch processing | `1500` |
84
86
  | `debounceTimeout` | Time in milliseconds to wait before processing translations | `500` |
85
87
  | `instruction` | Additional instruction for the translator (e.g., tone, style). Does not affect the cache key | `''` |
@@ -241,7 +243,7 @@ GPTrans supports a fallback mechanism for translation models. Instead of providi
241
243
  ```javascript
242
244
  const translator = new GPTrans({
243
245
  path: dbPath,
244
- model: ['claude46', 'gpt54'],
246
+ model: ['sonnet5@50', 'gpt56luna@100'],
245
247
  // ... other options
246
248
  });
247
249
  ```
@@ -249,6 +251,7 @@ const translator = new GPTrans({
249
251
  When using multiple models:
250
252
  - The first model in the array is used as the primary translation service
251
253
  - If the primary model fails (due to API errors, rate limits, etc.), GPTrans automatically falls back to the next model
254
+ - Use `model@<effort>` with an integer from `0` to `100` to set ModelMix's unified effort for that model only. By default, `sonnet5` uses effort `50`; if it fails, `gpt56luna` uses effort `100`. Models without the suffix keep their configured or provider-default effort.
252
255
  - This ensures higher availability and resilience of your translation service
253
256
 
254
257
  ## ✏️ Refining Translations
package/demo/case_1.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import GPTrans from '../index.js';
2
+ import { join } from 'path';
2
3
 
3
- const dbPath = new URL('../db', import.meta.url).pathname;
4
+ const dbPath = join(import.meta.dirname, '../db');
5
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
4
6
  const gptrans = new GPTrans({ path: dbPath, model: 'sonnet45' });
5
7
 
6
8
  console.log(gptrans.t('Hello, {name}!', { name: 'Anya' }));
@@ -30,7 +32,7 @@ const ar2es = new GPTrans({
30
32
  path: dbPath,
31
33
  from: 'es-AR',
32
34
  target: 'es-ES',
33
- model: 'gpt41'
35
+ model: 'gpt56terra@20'
34
36
  });
35
37
 
36
38
  console.log(ar2es.t('¿Tenés fuego?'));
package/demo/case_2.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import GPTrans from '../index.js';
2
+ import { join } from 'path';
2
3
 
3
- const dbPath = new URL('../db', import.meta.url).pathname;
4
+ const dbPath = join(import.meta.dirname, '../db');
5
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
4
6
 
5
7
  try {
6
8
  const gptrans = new GPTrans({
package/demo/case_3.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import GPTrans from '../index.js';
2
+ import { join } from 'path';
2
3
 
3
- const dbPath = new URL('../db', import.meta.url).pathname;
4
+ const dbPath = join(import.meta.dirname, '../db');
5
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
4
6
 
5
7
  try {
6
8
  const gptrans = new GPTrans({
package/demo/case_4.js CHANGED
@@ -1,16 +1,14 @@
1
1
  import GPTrans from '../index.js';
2
- import { dirname } from 'path';
3
- import { fileURLToPath } from 'url';
2
+ import { join } from 'path';
4
3
  import { promises as fs } from 'fs';
5
4
 
6
- // Get current file directory
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const dbPath = new URL('../db', import.meta.url).pathname;
5
+ const dbPath = join(import.meta.dirname, '../db');
6
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
9
7
 
10
8
  // Initialize translator
11
9
  const model = new GPTrans({
12
10
  path: dbPath,
13
- model: ['sonnet46', 'gpt54'],
11
+ model: ['sonnet50', 'gpt56luna@80'],
14
12
  from: 'es', // Assuming the source file is in Spanish
15
13
  target: 'en',
16
14
  });
@@ -19,10 +17,9 @@ const model = new GPTrans({
19
17
  await model.preload();
20
18
 
21
19
  // Read and translate the file
22
- const filePath = `${__dirname}/georgia_incident.md`;
20
+ const filePath = join(import.meta.dirname, 'georgia_incident.md');
23
21
  const content = await fs.readFile(filePath, 'utf-8');
24
22
 
25
23
  // Translate the content
26
24
  const translatedContent = model.t(content);
27
25
  console.log(translatedContent);
28
-
@@ -1,6 +1,8 @@
1
1
  import GPTrans from '../index.js';
2
+ import { join } from 'path';
2
3
 
3
- const dbPath = new URL('../db', import.meta.url).pathname;
4
+ const dbPath = join(import.meta.dirname, '../db');
5
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
4
6
 
5
7
  // Ejemplo: Cómo obtener la información completa del idioma
6
8
 
@@ -1,16 +1,9 @@
1
1
  import GPTrans from '../index.js';
2
- import { fileURLToPath } from 'url';
3
- import { dirname, join } from 'path';
2
+ import { join } from 'path';
4
3
 
5
4
  // Cargar .env desde la carpeta demo
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
- const dbPath = new URL('../db', import.meta.url).pathname;
9
- try {
10
- process.loadEnvFile(join(__dirname, '.env'));
11
- } catch {
12
- /* optional .env missing or unreadable */
13
- }
5
+ const dbPath = join(import.meta.dirname, '../db');
6
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
14
7
 
15
8
  console.log('🚀 Prueba de Paralelismo en GPTrans');
16
9
  console.log('⚠️ Múltiples instancias con MISMO NOMBRE y MISMO PAR DE IDIOMAS\n');
@@ -1,16 +1,9 @@
1
1
  import GPTrans from '../index.js';
2
- import { fileURLToPath } from 'url';
3
- import { dirname, join } from 'path';
2
+ import { join } from 'path';
4
3
 
5
4
  // Load .env from demo folder
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
- const dbPath = new URL('../db', import.meta.url).pathname;
9
- try {
10
- process.loadEnvFile(join(__dirname, '.env'));
11
- } catch {
12
- /* optional .env missing or unreadable */
13
- }
5
+ const dbPath = join(import.meta.dirname, '../db');
6
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
14
7
 
15
8
  console.log('🚀 Testing GPTrans with Reference Translations\n');
16
9
  console.log('='.repeat(70));
@@ -1,6 +1,8 @@
1
1
  import GPTrans from '../index.js';
2
+ import { join } from 'path';
2
3
 
3
- const dbPath = new URL('../db', import.meta.url).pathname;
4
+ const dbPath = join(import.meta.dirname, '../db');
5
+ try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
4
6
 
5
7
  console.log('🚀 Testing GPTrans Refine\n');
6
8
  console.log('='.repeat(70));
@@ -2,16 +2,12 @@
2
2
  import GPTrans from '../index.js';
3
3
  import path from 'path';
4
4
  import fs from 'fs';
5
- import { fileURLToPath } from 'url';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
5
 
10
6
  async function main() {
11
7
  try {
12
8
  // Initialize GPTrans with Spanish as target
13
9
  const gptrans = new GPTrans({
14
- path: path.join(__dirname, 'db'),
10
+ path: path.join(import.meta.dirname, 'db'),
15
11
  from: 'en-US',
16
12
  target: 'es'
17
13
  });
@@ -24,7 +20,7 @@ async function main() {
24
20
  console.log(' Input: en/image.jpg');
25
21
  console.log(' Expected output: es/image.jpg (sibling folder)\n');
26
22
 
27
- const imageInLangFolder = path.join(__dirname, 'en', 'camera_4126.jpg');
23
+ const imageInLangFolder = path.join(import.meta.dirname, 'en', 'camera_4126.jpg');
28
24
  if (fs.existsSync(imageInLangFolder)) {
29
25
  const result1 = await gptrans.img(imageInLangFolder);
30
26
  console.log(' Result:', result1);
@@ -39,7 +35,7 @@ async function main() {
39
35
  // console.log(' Input: ./image.jpg');
40
36
  // console.log(' Expected output: ./es/image.jpg (subfolder)\n');
41
37
 
42
- // const imageInRoot = path.join(__dirname, 'camera_4126.jpg');
38
+ // const imageInRoot = path.join(import.meta.dirname, 'camera_4126.jpg');
43
39
  // if (fs.existsSync(imageInRoot)) {
44
40
  // const result2 = await gptrans.img(imageInRoot);
45
41
  // console.log(' Result:', result2);
@@ -54,7 +50,7 @@ async function main() {
54
50
  // console.log(' Input: images/photo.jpg');
55
51
  // console.log(' Expected output: images/es/photo.jpg (subfolder)\n');
56
52
 
57
- // const imageInCustomFolder = path.join(__dirname, 'images', 'photo.jpg');
53
+ // const imageInCustomFolder = path.join(import.meta.dirname, 'images', 'photo.jpg');
58
54
  // if (fs.existsSync(imageInCustomFolder)) {
59
55
  // const result3 = await gptrans.img(imageInCustomFolder);
60
56
  // console.log(' Result:', result3);
package/index.js CHANGED
@@ -7,15 +7,20 @@ import { GeminiGenerator } from 'genmix';
7
7
  import fs from 'fs';
8
8
  import pathModule from 'path';
9
9
 
10
+ const TRANSLATE_PROMPT_FILE = pathModule.join(import.meta.dirname, 'prompt', 'translate.md');
11
+ const REFINE_PROMPT_FILE = pathModule.join(import.meta.dirname, 'prompt', 'refine.md');
12
+ const DEFAULT_MODEL_CHAIN = Object.freeze(['sonnet5@50', 'gpt56luna@100']);
13
+
10
14
  class GPTrans {
11
15
  static #mmixInstances = new Map();
12
16
  static #translationLocks = new Map();
13
17
 
14
- static mmix(models = 'sonnet45', { debug = 0 } = {}) {
15
- const key = Array.isArray(models) ? models.join(',') : models;
18
+ static mmix(models = DEFAULT_MODEL_CHAIN, { debug = 0 } = {}) {
19
+ const modelChain = Array.isArray(models) ? models : [models];
20
+ const key = modelChain.join(',');
16
21
 
17
22
  if (!this.#mmixInstances.has(key)) {
18
- let instance = ModelMix.new({
23
+ const instance = ModelMix.new({
19
24
  config: {
20
25
  debug,
21
26
  bottleneck: {
@@ -23,18 +28,7 @@ class GPTrans {
23
28
  maxConcurrent: 1
24
29
  }
25
30
  }
26
- });
27
- const modelArray = Array.isArray(models) ? models : [models];
28
-
29
- for (const model of modelArray) {
30
- if (typeof instance[model] !== 'function') {
31
- throw new Error(
32
- `Model "${model}" is not available. Please check the model name. ` +
33
- `Available models include: gpt51, gpt52, sonnet46, sonnet45, opus46, haiku45, etc.`
34
- );
35
- }
36
- instance = instance[model]();
37
- }
31
+ }).chain(...modelChain);
38
32
 
39
33
  this.#mmixInstances.set(key, instance);
40
34
  }
@@ -63,7 +57,7 @@ class GPTrans {
63
57
  return isLanguageAvailable(langCode);
64
58
  }
65
59
 
66
- constructor({ from = 'en-US', target = 'es', model = 'sonnet46', batchThreshold = 1500, debounceTimeout = 500, promptFile = null, name = '', context = '', instruction = '', freeze = false, debug = false, path: dbPath } = {}) {
60
+ constructor({ from = 'en-US', target = 'es', model = DEFAULT_MODEL_CHAIN, batchThreshold = 1500, debounceTimeout = 500, promptFile = null, name = '', context = '', instruction = '', freeze = false, debug = false, path: dbPath } = {}) {
67
61
 
68
62
  if (typeof dbPath !== 'string' || !pathModule.isAbsolute(dbPath)) {
69
63
  throw new TypeError('GPTrans requires an absolute "path" option.');
@@ -72,12 +66,6 @@ class GPTrans {
72
66
  target = this.normalizeBCP47(target);
73
67
  from = this.normalizeBCP47(from);
74
68
 
75
- try {
76
- process.loadEnvFile();
77
- } catch {
78
- /* optional .env missing or unreadable */
79
- }
80
-
81
69
  const namePrefix = name ? '_' + name : '';
82
70
  this.dbPath = dbPath;
83
71
  this.instanceName = name;
@@ -100,7 +88,7 @@ class GPTrans {
100
88
  this.isProcessingBatch = false; // Track if a batch is currently being processed
101
89
  this.modelMixOptions = { debug };
102
90
  this.modelKey = model;
103
- this.promptFile = promptFile ?? new URL('./prompt/translate.md', import.meta.url).pathname;
91
+ this.promptFile = promptFile ? pathModule.resolve(promptFile) : TRANSLATE_PROMPT_FILE;
104
92
  this.context = context;
105
93
  this.instruction = instruction;
106
94
  this.freeze = freeze;
@@ -335,7 +323,7 @@ class GPTrans {
335
323
  try {
336
324
  const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
337
325
 
338
- model.setSystem("You are an expert translator specialized in literary translation between {FROM_LANG} and {TARGET_DENONYM} {TARGET_LANG}.");
326
+ model.setSystem("You are an expert translator specialized in literary translation between <%- FROM_LANG %> and <%- TARGET_DENONYM %> <%- TARGET_LANG %>.");
339
327
 
340
328
  // Build references section (includes header when references exist, empty otherwise)
341
329
  let referencesText = '';
@@ -379,21 +367,21 @@ class GPTrans {
379
367
  }
380
368
  }
381
369
 
382
- // Use ModelMix templates: addTextFromFile + replace + block
370
+ // Use ModelMix templates: addTextFromFile + assign + block
383
371
  model.addTextFromFile(this.promptFile);
384
- model.replace({
385
- '{INPUT}': text,
386
- '{CONTEXT}': this.context,
387
- '{INSTRUCTION}': this.instruction,
388
- '{REFERENCES}': referencesText,
389
- '{TARGET_ISO}': this.replaceTarget.TARGET_ISO,
390
- '{TARGET_LANG}': this.replaceTarget.TARGET_LANG,
391
- '{TARGET_COUNTRY}': this.replaceTarget.TARGET_COUNTRY,
392
- '{TARGET_DENONYM}': this.replaceTarget.TARGET_DENONYM,
393
- '{FROM_ISO}': fromReplace.FROM_ISO,
394
- '{FROM_LANG}': fromReplace.FROM_LANG,
395
- '{FROM_COUNTRY}': fromReplace.FROM_COUNTRY,
396
- '{FROM_DENONYM}': fromReplace.FROM_DENONYM,
372
+ model.assign({
373
+ INPUT: text,
374
+ CONTEXT: this.context,
375
+ INSTRUCTION: this.instruction,
376
+ REFERENCES: referencesText,
377
+ TARGET_ISO: this.replaceTarget.TARGET_ISO,
378
+ TARGET_LANG: this.replaceTarget.TARGET_LANG,
379
+ TARGET_COUNTRY: this.replaceTarget.TARGET_COUNTRY,
380
+ TARGET_DENONYM: this.replaceTarget.TARGET_DENONYM,
381
+ FROM_ISO: fromReplace.FROM_ISO,
382
+ FROM_LANG: fromReplace.FROM_LANG,
383
+ FROM_COUNTRY: fromReplace.FROM_COUNTRY,
384
+ FROM_DENONYM: fromReplace.FROM_DENONYM,
397
385
  });
398
386
 
399
387
  return await model.block({ addSystemExtra: false });
@@ -557,7 +545,7 @@ class GPTrans {
557
545
  }
558
546
 
559
547
  const finalInstruction = instructions.length > 1 ? `- ${merged}` : merged;
560
- const refinePromptFile = promptFile ?? new URL('./prompt/refine.md', import.meta.url).pathname;
548
+ const refinePromptFile = promptFile ? pathModule.resolve(promptFile) : REFINE_PROMPT_FILE;
561
549
 
562
550
  // Collect all existing translations grouped by contextHash
563
551
  const allBatches = [];
@@ -652,22 +640,22 @@ class GPTrans {
652
640
  try {
653
641
  const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
654
642
 
655
- model.setSystem("You are an expert translator and editor specialized in refining {TARGET_DENONYM} {TARGET_LANG} translations.");
643
+ model.setSystem("You are an expert translator and editor specialized in refining <%- TARGET_DENONYM %> <%- TARGET_LANG %> translations.");
656
644
 
657
- // Use ModelMix templates: addTextFromFile + replace + block
645
+ // Use ModelMix templates: addTextFromFile + assign + block
658
646
  model.addTextFromFile(refinePromptFile);
659
- model.replace({
660
- '{INPUT}': text,
661
- '{INSTRUCTION}': instruction,
662
- '{CONTEXT}': this.context,
663
- '{TARGET_ISO}': this.replaceTarget.TARGET_ISO,
664
- '{TARGET_LANG}': this.replaceTarget.TARGET_LANG,
665
- '{TARGET_COUNTRY}': this.replaceTarget.TARGET_COUNTRY,
666
- '{TARGET_DENONYM}': this.replaceTarget.TARGET_DENONYM,
667
- '{FROM_ISO}': this.replaceFrom.FROM_ISO,
668
- '{FROM_LANG}': this.replaceFrom.FROM_LANG,
669
- '{FROM_COUNTRY}': this.replaceFrom.FROM_COUNTRY,
670
- '{FROM_DENONYM}': this.replaceFrom.FROM_DENONYM,
647
+ model.assign({
648
+ INPUT: text,
649
+ INSTRUCTION: instruction,
650
+ CONTEXT: this.context,
651
+ TARGET_ISO: this.replaceTarget.TARGET_ISO,
652
+ TARGET_LANG: this.replaceTarget.TARGET_LANG,
653
+ TARGET_COUNTRY: this.replaceTarget.TARGET_COUNTRY,
654
+ TARGET_DENONYM: this.replaceTarget.TARGET_DENONYM,
655
+ FROM_ISO: this.replaceFrom.FROM_ISO,
656
+ FROM_LANG: this.replaceFrom.FROM_LANG,
657
+ FROM_COUNTRY: this.replaceFrom.FROM_COUNTRY,
658
+ FROM_DENONYM: this.replaceFrom.FROM_DENONYM,
671
659
  });
672
660
 
673
661
  return await model.block({ addSystemExtra: false });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gptrans",
3
3
  "type": "module",
4
- "version": "2.2.0",
4
+ "version": "2.2.4",
5
5
  "description": "🚆 GPTrans - The smarter AI-powered way to translate.",
6
6
  "keywords": [
7
7
  "translate",
@@ -32,13 +32,13 @@
32
32
  },
33
33
  "homepage": "https://github.com/clasen/GPTrans#readme",
34
34
  "engines": {
35
- "node": ">=20.6.0"
35
+ "node": ">=20.12.0"
36
36
  },
37
37
  "dependencies": {
38
38
  "deepbase": "^3.9.0",
39
39
  "deepbase-json": "3.9.0",
40
40
  "genmix": "^1.2.5",
41
- "modelmix": "^4.6.5",
41
+ "modelmix": "^5.1.6",
42
42
  "string-hash": "^1.1.3"
43
43
  },
44
44
  "scripts": {
@@ -4,6 +4,7 @@ minimumReleaseAgeExclude:
4
4
  - genmix@1.2.5
5
5
  - deepbase-json@3.9.0
6
6
  - deepbase@3.9.0
7
+ - modelmix@5.1.6
7
8
  overrides:
8
9
  sharp: "^0.35.0"
9
10
  "@hono/node-server": "^2.0.5"
package/prompt/refine.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Goal
2
- Refine existing translations in {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) based on the following instruction.
2
+ Refine existing translations in <%- TARGET_ISO %> (<%- TARGET_DENONYM %> <%- TARGET_LANG %>) based on the following instruction.
3
3
 
4
4
  ## Refinement Instruction
5
- {INSTRUCTION}
5
+ <%- INSTRUCTION %>
6
6
 
7
7
  ## Current Translations to Evaluate
8
8
  ```
9
- {INPUT}
9
+ <%- INPUT %>
10
10
  ```
11
11
 
12
12
  # Return Format
@@ -23,4 +23,4 @@ Refine existing translations in {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) ba
23
23
  - **Consistency:** Maintain consistent terminology across all translations in the batch.
24
24
 
25
25
  # Context
26
- {CONTEXT}
26
+ <%- CONTEXT %>
@@ -1,13 +1,13 @@
1
1
  # Goal
2
- Translation from {FROM_ISO} to {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) with cultural adaptations.
3
- {INSTRUCTION}
2
+ Translation from <%- FROM_ISO %> to <%- TARGET_ISO %> (<%- TARGET_DENONYM %> <%- TARGET_LANG %>) with cultural adaptations.
3
+ <%- INSTRUCTION %>
4
4
 
5
5
  ## Text to translate
6
6
  ```
7
- {INPUT}
7
+ <%- INPUT %>
8
8
  ```
9
9
 
10
- {REFERENCES}
10
+ <%- REFERENCES %>
11
11
 
12
12
  # Return Format
13
13
  - The input may contain multiple texts separated by `------`. Translate each one independently and return them in the same order, separated by `------`. The number of segments in your output must exactly match the number of segments in the input.
@@ -15,13 +15,13 @@ Translation from {FROM_ISO} to {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) wit
15
15
  - Do not include alternative translations, only provide the best translation.
16
16
 
17
17
  # Warnings
18
- - **Context:** I will provide you with a text in {FROM_DENONYM} {FROM_LANG}. The goal is to translate it to {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) while maintaining the essence, style, intention, and tone of the original.
18
+ - **Context:** I will provide you with a text in <%- FROM_DENONYM %> <%- FROM_LANG %>. The goal is to translate it to <%- TARGET_ISO %> (<%- TARGET_DENONYM %> <%- TARGET_LANG %>) while maintaining the essence, style, intention, and tone of the original.
19
19
  - **Proper names:** Do not translate proper names (people, places, brands, etc.) unless they have an officially recognized translation in the target language.
20
- - **Cultural references:** Adapt or explain references that are not familiar in {TARGET_DENONYM} culture, whenever necessary.
20
+ - **Cultural references:** Adapt or explain references that are not familiar in <%- TARGET_DENONYM %> culture, whenever necessary.
21
21
  - **Wordplay and humor:** When it's impossible to directly translate wordplay, find a resource that recreates the playful effect.
22
22
  - **Idioms:** Do not introduce new idioms or expressions that are not present in the original text.
23
23
  - **Variables:** Do not translate content between curly braces. These are system variables and must remain exactly the same.
24
24
 
25
25
 
26
26
  # Context
27
- {CONTEXT}
27
+ <%- CONTEXT %>
@@ -81,13 +81,13 @@ You can manually edit translation files to override specific entries.
81
81
 
82
82
  ```javascript
83
83
  import GPTrans from 'gptrans';
84
+ import path from 'node:path';
84
85
 
85
- const dbPath = new URL('./db', import.meta.url).pathname;
86
+ const dbPath = path.join(import.meta.dirname, 'db');
86
87
  const gptrans = new GPTrans({
87
88
  path: dbPath,
88
89
  from: 'en-US',
89
- target: 'es-AR',
90
- model: 'sonnet45'
90
+ target: 'es-AR'
91
91
  });
92
92
 
93
93
  // Translate text — returns original on first call, cached translation after
@@ -106,7 +106,7 @@ console.log(gptrans.setContext().t('Welcome back'));
106
106
  | --- | --- | --- | --- |
107
107
  | `from` | `string` | `'en-US'` | Source language (BCP 47) |
108
108
  | `target` | `string` | `'es'` | Target language (BCP 47) |
109
- | `model` | `string \| string[]` | `'sonnet45'` | Model key or array for fallback chain |
109
+ | `model` | `string \| string[]` | `['sonnet5@50', 'gpt56luna@100']` | ModelMix shortcut or ordered fallback chain; supports `@<effort>` from `0` to `100` |
110
110
  | `batchThreshold` | `number` | `1500` | Max characters before triggering batch |
111
111
  | `debounceTimeout` | `number` | `500` | Milliseconds to wait before processing |
112
112
  | `freeze` | `boolean` | `false` | Prevent new translations from being queued |
@@ -134,10 +134,12 @@ const gptrans = new GPTrans({
134
134
  path: dbPath,
135
135
  from: 'en',
136
136
  target: 'fr',
137
- model: ['sonnet45', 'gpt41'] // falls back to gpt41 if sonnet45 fails
137
+ model: ['sonnet5@50', 'gpt56luna@100']
138
138
  });
139
139
  ```
140
140
 
141
+ Use `model@<effort>` with an integer from `0` to `100` to override the unified effort for one model. The default chain uses `sonnet5` at effort `50`, then falls back to `gpt56luna` at effort `100`.
142
+
141
143
  ### Pre-translate all pending texts
142
144
 
143
145
  ```javascript
@@ -268,8 +270,7 @@ await gptrans.purge();
268
270
  const es2ar = new GPTrans({
269
271
  path: dbPath,
270
272
  from: 'es-ES',
271
- target: 'es-AR',
272
- model: 'sonnet45'
273
+ target: 'es-AR'
273
274
  });
274
275
 
275
276
  console.log(es2ar.t('Eres muy bueno'));
@@ -295,7 +296,7 @@ if (GPTrans.isLanguageAvailable('pt-BR')) {
295
296
  - Use `setContext()` for gender-aware or domain-specific translations. Context is captured per-batch and auto-resets when changed.
296
297
  - Use the `instruction` constructor option for style/tone guidance (e.g., "Use a more natural tone"). Unlike `context`, `instruction` does NOT affect the cache key — different instructions for the same text overwrite the same translation entry.
297
298
  - Prefer passing an array of instructions to `refine()` over multiple calls — it processes everything in a single API pass.
298
- - Use model arrays (`model: ['sonnet45', 'gpt41']`) for production resilience with automatic fallback.
299
+ - Use model arrays (`model: ['sonnet5@50', 'gpt56luna@100']`) for production resilience with automatic fallback. The optional `@<effort>` suffix accepts integers from `0` to `100` and applies only to that model.
299
300
  - Translation caches live in `db/gptrans_<locale>.json`. These files can be manually edited to override specific translations.
300
301
  - The `name` constructor option isolates cache files (`db/gptrans_<name>_<locale>.json`), useful for multiple independent translation contexts in the same project.
301
302
  - When translating images, ensure `GEMINI_API_KEY` is set. The `img()` method auto-creates sibling language folders.
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { access, mkdtemp, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import GPTrans from '../index.js';
@@ -161,6 +161,87 @@ test('constructor requires an absolute database path', () => {
161
161
  assert.throws(() => new GPTrans({ path: './db' }), /requires an absolute "path" option/);
162
162
  });
163
163
 
164
+ test('the default model chain uses the configured efforts and renders the EJS prompt', async () => {
165
+ const previousAnthropicKey = process.env.ANTHROPIC_API_KEY;
166
+ const previousOpenAiKey = process.env.OPENAI_API_KEY;
167
+ process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
168
+ process.env.OPENAI_API_KEY = 'test-openai-key';
169
+
170
+ try {
171
+ const model = GPTrans.mmix();
172
+ assert.deepEqual(
173
+ model.models.map(({ key }) => key),
174
+ ['claude-sonnet-5', 'gpt-5.6-luna']
175
+ );
176
+ assert.equal(model.models[0].provider.config.effort, 50);
177
+ assert.equal(model.models[1].provider.config.effort, 100);
178
+
179
+ const gp = new GPTrans({ path: tmpdir(), freeze: true, name: 'modelmix-render' });
180
+ assert.deepEqual(gp.modelKey, ['sonnet5@50', 'gpt56luna@100']);
181
+ model.addTextFromFile(gp.promptFile);
182
+ model.assign({
183
+ INPUT: 'Hello',
184
+ CONTEXT: 'Greeting',
185
+ INSTRUCTION: 'Be friendly',
186
+ REFERENCES: '',
187
+ TARGET_ISO: 'es',
188
+ TARGET_LANG: 'Spanish',
189
+ TARGET_COUNTRY: 'Spain',
190
+ TARGET_DENONYM: 'Spanish',
191
+ FROM_ISO: 'en-US',
192
+ FROM_LANG: 'English',
193
+ FROM_COUNTRY: 'United States',
194
+ FROM_DENONYM: 'American'
195
+ });
196
+ const messages = await model.prepareMessages();
197
+ const renderedPrompt = messages[0].content[0].text;
198
+ assert.match(renderedPrompt, /Translation from en-US to es/);
199
+ assert.match(renderedPrompt, /```\nHello\n```/);
200
+ assert.doesNotMatch(renderedPrompt, /<%-\s*[A-Z_]+\s*%>/);
201
+ } finally {
202
+ if (previousAnthropicKey === undefined) delete process.env.ANTHROPIC_API_KEY;
203
+ else process.env.ANTHROPIC_API_KEY = previousAnthropicKey;
204
+ if (previousOpenAiKey === undefined) delete process.env.OPENAI_API_KEY;
205
+ else process.env.OPENAI_API_KEY = previousOpenAiKey;
206
+ }
207
+ });
208
+
209
+ test('_translate uses assign() with the absolute built-in prompt path', async () => {
210
+ const gp = createTestInstance();
211
+ const originalMmix = GPTrans.mmix;
212
+ const calls = {};
213
+ const model = {
214
+ setSystem(value) {
215
+ calls.system = value;
216
+ },
217
+ addTextFromFile(value) {
218
+ calls.promptFile = value;
219
+ },
220
+ assign(value) {
221
+ calls.templateData = value;
222
+ },
223
+ async block(options) {
224
+ calls.blockOptions = options;
225
+ return 'Hola';
226
+ }
227
+ };
228
+ GPTrans.mmix = () => model;
229
+
230
+ try {
231
+ assert.equal(await gp._translate('Hello'), 'Hola');
232
+ assert.equal(path.isAbsolute(calls.promptFile), true);
233
+ assert.equal(path.basename(calls.promptFile), 'translate.md');
234
+ await access(calls.promptFile);
235
+ assert.equal(calls.templateData.INPUT, 'Hello');
236
+ assert.equal(calls.templateData.FROM_ISO, 'en-US');
237
+ assert.equal(calls.templateData.TARGET_ISO, 'es');
238
+ assert.match(calls.system, /<%- FROM_LANG %>/);
239
+ assert.deepEqual(calls.blockOptions, { addSystemExtra: false });
240
+ } finally {
241
+ GPTrans.mmix = originalMmix;
242
+ }
243
+ });
244
+
164
245
  test('DeepBase 3.9 cache is available to synchronous translations', async () => {
165
246
  const directory = await mkdtemp(path.join(tmpdir(), 'gptrans-deepbase-'));
166
247
  const originalMmix = GPTrans.mmix;
@@ -193,6 +274,17 @@ test('DeepBase 3.9 cache is available to synchronous translations', async () =>
193
274
 
194
275
  assert.equal(gp.dbFrom.getSync('', sourceKey), sourceText);
195
276
  assert.equal(gp.dbTarget.getSync(contextHash, sourceKey), 'Nuevo texto fuente');
277
+
278
+ let refinePromptFile;
279
+ gp._processRefineBatch = async (entries, instruction, promptFile) => {
280
+ assert.equal(entries.length, 2);
281
+ assert.equal(instruction, 'Use a formal tone');
282
+ refinePromptFile = promptFile;
283
+ };
284
+ await gp.refine('Use a formal tone');
285
+ assert.equal(path.isAbsolute(refinePromptFile), true);
286
+ assert.equal(path.basename(refinePromptFile), 'refine.md');
287
+ await access(refinePromptFile);
196
288
  } finally {
197
289
  GPTrans.mmix = originalMmix;
198
290
  if (gp.debounceTimer) {
@@ -1,10 +1,6 @@
1
1
  import GPTrans from './index.js';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
- import { fileURLToPath } from 'url';
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
4
 
9
5
  async function testImageTranslation() {
10
6
  try {
@@ -12,14 +8,14 @@ async function testImageTranslation() {
12
8
 
13
9
  // Initialize GPTrans with Portuguese as target language
14
10
  const gptrans = new GPTrans({
15
- path: path.join(__dirname, 'db'),
11
+ path: path.join(import.meta.dirname, 'db'),
16
12
  from: 'en-US',
17
13
  target: 'pt-BR',
18
14
  model: 'sonnet45'
19
15
  });
20
16
 
21
17
  // Test image path (you'll need to provide an actual image)
22
- const testImagePath = path.join(__dirname, 'test-image.jpg');
18
+ const testImagePath = path.join(import.meta.dirname, 'test-image.jpg');
23
19
 
24
20
  // Check if test image exists
25
21
  if (!fs.existsSync(testImagePath)) {