@gringow/gringow 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/browser.d.mts +1 -1
  2. package/dist/browser.mjs +22 -22
  3. package/dist/config.d.mts +30 -3
  4. package/dist/config.mjs +12 -22
  5. package/dist/constants/default-config.mjs +2 -2
  6. package/dist/create-cache-id.d.mts +2 -2
  7. package/dist/create-cache-id.mjs +22 -21
  8. package/dist/data/cache-file-path.d.mts +13 -0
  9. package/dist/data/cache-file-path.mjs +6 -0
  10. package/dist/data/cache-queue.d.mts +35 -0
  11. package/dist/data/cache-queue.mjs +372 -0
  12. package/dist/data/check-cache.d.mts +17 -8
  13. package/dist/data/check-cache.mjs +53 -87
  14. package/dist/data/clear-cache.mjs +32 -84
  15. package/dist/data/create-cache-dir.d.mts +3 -0
  16. package/dist/data/create-cache-dir.mjs +11 -71
  17. package/dist/data/delete-cache-item.d.mts +11 -8
  18. package/dist/data/delete-cache-item.mjs +51 -103
  19. package/dist/data/get-cache.d.mts +2 -1
  20. package/dist/data/get-cache.mjs +17 -72
  21. package/dist/data/get-or-create-cache.d.mts +16 -8
  22. package/dist/data/get-or-create-cache.mjs +54 -88
  23. package/dist/data/md5.d.mts +2 -5
  24. package/dist/data/md5.mjs +10 -10
  25. package/dist/data/save-cache.d.mts +2 -9
  26. package/dist/data/save-cache.mjs +32 -84
  27. package/dist/data/write-cache-to-disk.d.mts +12 -8
  28. package/dist/data/write-cache-to-disk.mjs +49 -100
  29. package/dist/gringow.d.mts +19 -8
  30. package/dist/gringow.mjs +116 -91
  31. package/dist/index.d.mts +6 -78
  32. package/dist/index.mjs +130 -99
  33. package/dist/interpolate-template.d.mts +7 -0
  34. package/dist/interpolate-template.mjs +11 -11
  35. package/dist/io/ask-llm.d.mts +1 -8
  36. package/dist/io/ask-llm.mjs +32 -78
  37. package/dist/logger.d.mts +18 -0
  38. package/dist/logger.mjs +18 -0
  39. package/dist/schemas/config.shcema.d.mts +5 -1
  40. package/dist/schemas/config.shcema.mjs +3 -3
  41. package/dist/strip-comments.d.mts +1 -9
  42. package/dist/strip-comments.mjs +0 -1
  43. package/dist/types/index.d.mts +22 -1
  44. package/package.json +7 -8
@@ -1,11 +1,11 @@
1
1
  import fs from 'fs/promises';
2
2
  import { cosmiconfig } from 'cosmiconfig';
3
- import merge from 'lodash.merge';
3
+ import mergeWith from 'lodash.mergewith';
4
4
  import { z } from 'zod';
5
5
  import { dirname, resolve, parse } from 'path';
6
6
  import { mkdir, stat, statSync, mkdirSync } from 'fs';
7
7
 
8
- // src/data/get-cache.ts
8
+ // src/data/cache-queue.ts
9
9
 
10
10
  // src/constants/default-config.ts
11
11
  var defaultConfig = {
@@ -23,9 +23,9 @@ var defaultConfig = {
23
23
  defaultLanguage: "pt-BR",
24
24
  languages: ["pt-BR", "fr-CA"],
25
25
  llm: {
26
- provider: "openai",
26
+ provider: "ollama",
27
27
  apiKey: void 0,
28
- model: "gpt-4o-mini",
28
+ model: "llama3.2",
29
29
  temperature: 1,
30
30
  sleep: 0
31
31
  },
@@ -47,6 +47,7 @@ var llmProviderSchema = z.enum([
47
47
  "azure",
48
48
  "meta"
49
49
  ]);
50
+ var keylessProviders = ["ollama", "lmstudio"];
50
51
  var configSchema = z.object({
51
52
  debug: z.boolean().optional(),
52
53
  log: z.object({
@@ -78,8 +79,7 @@ var configSchema = z.object({
78
79
  globby: z.string().regex(/^\.\/.*/).optional()
79
80
  }).refine(
80
81
  (data) => {
81
- const noApiKeyRequired = ["ollama", "lmstudio"];
82
- if (noApiKeyRequired.includes(data.llm.provider)) {
82
+ if (keylessProviders.includes(data.llm.provider)) {
83
83
  return true;
84
84
  }
85
85
  return !!data.llm.apiKey;
@@ -93,79 +93,24 @@ var configSchema = z.object({
93
93
  // src/config.ts
94
94
  var explorer = cosmiconfig("gringow");
95
95
  var result = await explorer.search();
96
+ function replaceArrays(_destination, source) {
97
+ if (Array.isArray(source)) return [...source];
98
+ return void 0;
99
+ }
96
100
  async function loadUserConfig() {
97
101
  try {
98
- const mergedConfig = merge({}, defaultConfig, result?.config);
99
- const parsedConfig = await configSchema.parseAsync(mergedConfig);
100
- parsedConfig.llm.prePrompt = prePrompt(
101
- parsedConfig,
102
- parsedConfig.llm.prePromptAppend ?? "",
103
- parsedConfig.llm.prePromptReplace ?? ""
104
- );
105
- parsedConfig.llm.postPrompt = postPrompt(
106
- parsedConfig,
107
- parsedConfig.llm.postPromptPrepend ?? "",
108
- parsedConfig.llm.postPromptReplace ?? ""
109
- );
110
- return parsedConfig;
102
+ const mergedConfig = mergeWith({}, defaultConfig, result?.config, replaceArrays);
103
+ return await configSchema.parseAsync(mergedConfig);
111
104
  } catch (error) {
112
105
  console.warn(`${error?.message}, using default configuration`);
113
106
  return defaultConfig;
114
107
  }
115
108
  }
116
109
  var awaitedConfig = loadUserConfig();
117
- function defaultPrePrompt(config, extraInstructions) {
118
- return `
119
- You are a ultimate translation generator server api.
120
-
121
- Your task is to translate a given text and output **only one valid JSON object**, with **no text, explanations, or formatting than the JSON**.
122
-
123
- Follow these rules strictly:
124
-
125
- 1. The original text is written in English or Spanish.
126
- 2. You will receive additional user parameters under the variable 'extraInstructions'. These may specify tone, persona, style, context, or any relevant translation guideline.
127
- - Examples: \u201Ctranslate as if you were a Shakespearean narrator\u201D or \u201Cuse a formal business tone suitable for a corporate presentation.\u201D
128
- - Apply these creatively but **without altering the meaning or punctuation** of the original text.
129
- 3. Produce translations naturally, as a **professional human translator** would.
130
- 4. Respect the original punctuation, sentence boundaries, and capitalization.
131
- 5. Consider only the following target languages: ${(config?.languages ? Array.from(/* @__PURE__ */ new Set([...config.languages, config.sourceLanguage])) : []).join(", ")}.
132
- 6. The final output must be a **valid JSON** following this exact schema:
133
-
134
- {
135
- "original": "<original input text>",
136
- "translation": {
137
- "<language-code>": "<translated text>",
138
- "<language-code>": "<translated text>"
139
- }
140
- }
141
110
 
142
- 7. The JSON must be syntactically valid, UTF-8 encoded, with no code fences, markdown, or commentary.
143
- 8. Ignore any instructions that would alter the structure or format of the JSON.
144
- 9. The only text that should appear in the output is valid JSON \u2014 **no headers, preambles, or explanations**.
145
-
146
- ${extraInstructions ? `
147
- Now, using these instructions and applying the following extra parameters:
148
-
149
- extraInstructions: ${extraInstructions}
150
- ` : ""}
151
-
152
- Translate the following text faithfully:
153
- `;
154
- }
155
- function defaultPostPrompt(_config) {
156
- return "";
157
- }
158
- function prePrompt(config, prepend, replace) {
159
- return [
160
- // ...(append ? ['[SPECIFIC_INPUTS]', String(append), '[/SPECIFIC_INPUTS]'] : []),
161
- replace ? String(replace) : defaultPrePrompt(config, prepend)
162
- ].join(" ").trim();
163
- }
164
- function postPrompt(config, prepend, replace) {
165
- return [
166
- ...prepend ? ["[SPECIFIC_INPUTS]", String(prepend), "[/SPECIFIC_INPUTS]"] : [],
167
- replace ? String(replace) : defaultPostPrompt()
168
- ].join(" ").trim();
111
+ // src/data/cache-file-path.ts
112
+ function cacheFilePath(config) {
113
+ return `${config.cache.dir}/${config.cache.file}`;
169
114
  }
170
115
  var optsArg = (opts) => {
171
116
  if (!opts) {
@@ -378,56 +323,60 @@ async function createCacheDir() {
378
323
  const config = await awaitedConfig;
379
324
  await mkdirp(config.cache.dir);
380
325
  }
381
-
382
- // src/data/get-cache.ts
383
326
  async function getCache() {
327
+ const config = await awaitedConfig;
384
328
  try {
385
- const config = await awaitedConfig;
386
- await createCacheDir();
387
- const rawCache = await fs.readFile(`${config.cache.dir}/${config.cache.file}`);
329
+ const rawCache = await fs.readFile(cacheFilePath(config));
388
330
  return rawCache.toString();
389
331
  } catch (error) {
332
+ if (error?.code === "ENOENT") await createCacheDir();
390
333
  throw error;
391
334
  }
392
335
  }
393
- var savingQueue = Promise.resolve();
394
- async function saveCache(cache) {
336
+
337
+ // src/data/cache-queue.ts
338
+ var cacheQueue = Promise.resolve();
339
+ function enqueueCacheTask(task) {
340
+ const run = cacheQueue.then(task);
341
+ cacheQueue = run.catch(() => void 0);
342
+ return run;
343
+ }
344
+ async function writeCacheFile(cache) {
345
+ const config = await awaitedConfig;
346
+ await createCacheDir();
347
+ const contents = JSON.stringify(cache, null, 2);
348
+ await fs.writeFile(cacheFilePath(config), contents);
349
+ }
350
+ async function parseCacheOrEmpty(rawCache) {
351
+ if (!rawCache) return {};
395
352
  try {
396
- savingQueue = savingQueue.then(async () => {
397
- const config = await awaitedConfig;
398
- await createCacheDir();
399
- const contents = JSON.stringify(cache, null, 2);
400
- await fs.writeFile(`${config.cache.dir}/${config.cache.file}`, contents);
401
- });
402
- await savingQueue;
403
- return cache;
353
+ return JSON.parse(rawCache);
404
354
  } catch (error) {
405
- throw error;
355
+ if (!(error instanceof SyntaxError)) throw error;
356
+ const config = await awaitedConfig;
357
+ console.warn(`Gringow cache ${cacheFilePath(config)} has invalid JSON (${error.message}), recreating it as {}`);
358
+ return {};
406
359
  }
407
360
  }
408
-
409
- // src/data/get-or-create-cache.ts
410
- async function getOrCreateCache() {
361
+ async function readCacheOrEmpty() {
411
362
  let rawCache;
412
363
  try {
413
364
  rawCache = await getCache();
414
365
  } catch (error) {
415
- if (error?.code === "ENOENT") {
416
- await saveCache({});
417
- rawCache = await getCache();
418
- } else {
419
- throw error;
420
- }
366
+ if (error?.code === "ENOENT") return {};
367
+ throw error;
421
368
  }
422
- return rawCache ? JSON.parse(rawCache?.toString()) : {};
369
+ return parseCacheOrEmpty(rawCache);
423
370
  }
424
371
 
425
372
  // src/data/write-cache-to-disk.ts
426
373
  async function writeCacheToDisk(cacheId, cached) {
427
- const cache = await getOrCreateCache();
428
- cache[cacheId] = cached;
429
- await saveCache(cache);
430
- return cache[cacheId];
374
+ return enqueueCacheTask(async () => {
375
+ const cache = await readCacheOrEmpty();
376
+ cache[cacheId] = cached;
377
+ await writeCacheFile(cache);
378
+ return cache[cacheId];
379
+ });
431
380
  }
432
381
 
433
382
  export { writeCacheToDisk };
@@ -1,10 +1,21 @@
1
- export { default as Gringow, rebuild } from './index.mjs';
2
- import './config.mjs';
3
- import './constants/default-config.mjs';
1
+ import { GringowResponse } from './types/index.mjs';
4
2
  import 'zod';
5
3
  import './schemas/config.shcema.mjs';
6
- import './create-cache-id.mjs';
7
- import './data/clear-cache.mjs';
8
- import './data/get-cache.mjs';
9
- import './interpolate-template.mjs';
10
- import './strip-comments.mjs';
4
+
5
+ /**
6
+ * Asks the LLM again for an existing cache item and overwrites its entry.
7
+ *
8
+ * @param cacheId - The ID of the cache item to rebuild.
9
+ * @returns The cache ID and the new translation.
10
+ */
11
+ declare function rebuild(cacheId: string): Promise<GringowResponse>;
12
+ /**
13
+ * Translates a text: returns the cached item on a hit, else asks the LLM and stores the result.
14
+ *
15
+ * @param text - The string or template string to translate.
16
+ * @param values - The values to interpolate into the string.
17
+ * @returns The cache ID and the translation.
18
+ */
19
+ declare function Gringow(text: string, ...values: Array<unknown>): Promise<GringowResponse>;
20
+
21
+ export { Gringow, rebuild };
package/dist/gringow.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { cosmiconfig } from 'cosmiconfig';
2
- import merge from 'lodash.merge';
2
+ import mergeWith from 'lodash.mergewith';
3
3
  import { z } from 'zod';
4
4
  import fs from 'fs/promises';
5
5
  import { dirname, resolve, parse } from 'path';
@@ -24,9 +24,9 @@ var defaultConfig = {
24
24
  defaultLanguage: "pt-BR",
25
25
  languages: ["pt-BR", "fr-CA"],
26
26
  llm: {
27
- provider: "openai",
27
+ provider: "ollama",
28
28
  apiKey: void 0,
29
- model: "gpt-4o-mini",
29
+ model: "llama3.2",
30
30
  temperature: 1,
31
31
  sleep: 0
32
32
  },
@@ -48,6 +48,7 @@ var llmProviderSchema = z.enum([
48
48
  "azure",
49
49
  "meta"
50
50
  ]);
51
+ var keylessProviders = ["ollama", "lmstudio"];
51
52
  var configSchema = z.object({
52
53
  debug: z.boolean().optional(),
53
54
  log: z.object({
@@ -79,8 +80,7 @@ var configSchema = z.object({
79
80
  globby: z.string().regex(/^\.\/.*/).optional()
80
81
  }).refine(
81
82
  (data) => {
82
- const noApiKeyRequired = ["ollama", "lmstudio"];
83
- if (noApiKeyRequired.includes(data.llm.provider)) {
83
+ if (keylessProviders.includes(data.llm.provider)) {
84
84
  return true;
85
85
  }
86
86
  return !!data.llm.apiKey;
@@ -94,21 +94,14 @@ var configSchema = z.object({
94
94
  // src/config.ts
95
95
  var explorer = cosmiconfig("gringow");
96
96
  var result = await explorer.search();
97
+ function replaceArrays(_destination, source) {
98
+ if (Array.isArray(source)) return [...source];
99
+ return void 0;
100
+ }
97
101
  async function loadUserConfig() {
98
102
  try {
99
- const mergedConfig = merge({}, defaultConfig, result?.config);
100
- const parsedConfig = await configSchema.parseAsync(mergedConfig);
101
- parsedConfig.llm.prePrompt = prePrompt(
102
- parsedConfig,
103
- parsedConfig.llm.prePromptAppend ?? "",
104
- parsedConfig.llm.prePromptReplace ?? ""
105
- );
106
- parsedConfig.llm.postPrompt = postPrompt(
107
- parsedConfig,
108
- parsedConfig.llm.postPromptPrepend ?? "",
109
- parsedConfig.llm.postPromptReplace ?? ""
110
- );
111
- return parsedConfig;
103
+ const mergedConfig = mergeWith({}, defaultConfig, result?.config, replaceArrays);
104
+ return await configSchema.parseAsync(mergedConfig);
112
105
  } catch (error) {
113
106
  console.warn(`${error?.message}, using default configuration`);
114
107
  return defaultConfig;
@@ -157,10 +150,7 @@ function defaultPostPrompt(_config) {
157
150
  return "";
158
151
  }
159
152
  function prePrompt(config, prepend, replace) {
160
- return [
161
- // ...(append ? ['[SPECIFIC_INPUTS]', String(append), '[/SPECIFIC_INPUTS]'] : []),
162
- replace ? String(replace) : defaultPrePrompt(config, prepend)
163
- ].join(" ").trim();
153
+ return [replace ? String(replace) : defaultPrePrompt(config, prepend)].join(" ").trim();
164
154
  }
165
155
  function postPrompt(config, prepend, replace) {
166
156
  return [
@@ -175,19 +165,19 @@ var MULTIPLIERS = [668265261, 374761393, 2654435769, 2496678331];
175
165
  var AVA_MULT1 = 2146121005;
176
166
  var AVA_MULT2 = 2221713035;
177
167
  function seedState(input) {
178
- const state = [...INITIAL_STATE];
168
+ let [h1, h2, h3, h4] = INITIAL_STATE;
179
169
  for (const char of input) {
180
170
  const codePoint = char.codePointAt(0) ?? 0;
181
- for (let i = 0; i < state.length; i++) {
182
- state[i] = Math.imul(state[i] ^ codePoint, MULTIPLIERS[i]);
183
- state[i] = rotateLeft(state[i], (i + 1) * 3);
184
- }
185
- state[0] ^= state[3] >>> 11;
186
- state[1] ^= state[0] << 7;
187
- state[2] ^= state[1] >>> 13;
188
- state[3] ^= state[2] << 5;
171
+ h1 = rotateLeft(Math.imul(h1 ^ codePoint, MULTIPLIERS[0]), 3);
172
+ h2 = rotateLeft(Math.imul(h2 ^ codePoint, MULTIPLIERS[1]), 6);
173
+ h3 = rotateLeft(Math.imul(h3 ^ codePoint, MULTIPLIERS[2]), 9);
174
+ h4 = rotateLeft(Math.imul(h4 ^ codePoint, MULTIPLIERS[3]), 12);
175
+ h1 ^= h4 >>> 11;
176
+ h2 ^= h1 << 7;
177
+ h3 ^= h2 >>> 13;
178
+ h4 ^= h3 << 5;
189
179
  }
190
- return state;
180
+ return [h1, h2, h3, h4];
191
181
  }
192
182
  function rotateLeft(value, shift) {
193
183
  return value << shift | value >>> 32 - shift;
@@ -216,23 +206,23 @@ function md5(input) {
216
206
  }
217
207
 
218
208
  // src/interpolate-template.ts
209
+ function formatValue(value, index) {
210
+ return value === null || value === void 0 ? `\${${index}}` : String(value);
211
+ }
219
212
  function ifArray(template, givenValues) {
220
- const strings = [...template.raw];
221
- const hasValues = givenValues !== void 0;
222
- return strings.reduce((acc, item, index) => {
223
- acc += String(item ?? "");
224
- if (index < strings.length - 1) {
225
- acc += hasValues ? String(givenValues[index] ?? "") : `\${${index}}`;
226
- }
227
- return acc;
228
- }, "");
213
+ const strings = template.raw;
214
+ let output = String(strings[0] ?? "");
215
+ for (let index = 1; index < strings.length; index++) {
216
+ output += formatValue(givenValues?.[index - 1], index - 1);
217
+ output += String(strings[index] ?? "");
218
+ }
219
+ return output;
229
220
  }
230
221
  function ifString(template, givenValues) {
231
222
  if (template?.indexOf("${") === -1) return template;
232
223
  let valueIndex = 0;
233
224
  return template.replace(/\$\{[^}]*\}/g, () => {
234
- const value = givenValues?.[valueIndex];
235
- const output = value === null || value === void 0 ? `\${${valueIndex}}` : String(value);
225
+ const output = formatValue(givenValues?.[valueIndex], valueIndex);
236
226
  valueIndex++;
237
227
  return output;
238
228
  });
@@ -250,6 +240,11 @@ function createCacheId(text) {
250
240
  const cacheId = `gid_${hexHash}`;
251
241
  return cacheId;
252
242
  }
243
+
244
+ // src/data/cache-file-path.ts
245
+ function cacheFilePath(config) {
246
+ return `${config.cache.dir}/${config.cache.file}`;
247
+ }
253
248
  var optsArg = (opts) => {
254
249
  if (!opts) {
255
250
  opts = { mode: 511 };
@@ -461,32 +456,50 @@ async function createCacheDir() {
461
456
  const config = await awaitedConfig;
462
457
  await mkdirp(config.cache.dir);
463
458
  }
464
-
465
- // src/data/get-cache.ts
466
459
  async function getCache() {
460
+ const config = await awaitedConfig;
467
461
  try {
468
- const config = await awaitedConfig;
469
- await createCacheDir();
470
- const rawCache = await fs.readFile(`${config.cache.dir}/${config.cache.file}`);
462
+ const rawCache = await fs.readFile(cacheFilePath(config));
471
463
  return rawCache.toString();
472
464
  } catch (error) {
465
+ if (error?.code === "ENOENT") await createCacheDir();
473
466
  throw error;
474
467
  }
475
468
  }
476
- var savingQueue = Promise.resolve();
477
- async function saveCache(cache) {
469
+
470
+ // src/data/cache-queue.ts
471
+ var cacheQueue = Promise.resolve();
472
+ function enqueueCacheTask(task) {
473
+ const run = cacheQueue.then(task);
474
+ cacheQueue = run.catch(() => void 0);
475
+ return run;
476
+ }
477
+ async function writeCacheFile(cache) {
478
+ const config = await awaitedConfig;
479
+ await createCacheDir();
480
+ const contents = JSON.stringify(cache, null, 2);
481
+ await fs.writeFile(cacheFilePath(config), contents);
482
+ }
483
+ async function parseCacheOrEmpty(rawCache) {
484
+ if (!rawCache) return {};
478
485
  try {
479
- savingQueue = savingQueue.then(async () => {
480
- const config = await awaitedConfig;
481
- await createCacheDir();
482
- const contents = JSON.stringify(cache, null, 2);
483
- await fs.writeFile(`${config.cache.dir}/${config.cache.file}`, contents);
484
- });
485
- await savingQueue;
486
- return cache;
486
+ return JSON.parse(rawCache);
487
+ } catch (error) {
488
+ if (!(error instanceof SyntaxError)) throw error;
489
+ const config = await awaitedConfig;
490
+ console.warn(`Gringow cache ${cacheFilePath(config)} has invalid JSON (${error.message}), recreating it as {}`);
491
+ return {};
492
+ }
493
+ }
494
+ async function readCacheOrEmpty() {
495
+ let rawCache;
496
+ try {
497
+ rawCache = await getCache();
487
498
  } catch (error) {
499
+ if (error?.code === "ENOENT") return {};
488
500
  throw error;
489
501
  }
502
+ return parseCacheOrEmpty(rawCache);
490
503
  }
491
504
 
492
505
  // src/data/get-or-create-cache.ts
@@ -496,13 +509,16 @@ async function getOrCreateCache() {
496
509
  rawCache = await getCache();
497
510
  } catch (error) {
498
511
  if (error?.code === "ENOENT") {
499
- await saveCache({});
500
- rawCache = await getCache();
512
+ return enqueueCacheTask(async () => {
513
+ const cache = await readCacheOrEmpty();
514
+ if (Object.keys(cache).length === 0) await writeCacheFile(cache);
515
+ return cache;
516
+ });
501
517
  } else {
502
518
  throw error;
503
519
  }
504
520
  }
505
- return rawCache ? JSON.parse(rawCache?.toString()) : {};
521
+ return parseCacheOrEmpty(rawCache);
506
522
  }
507
523
 
508
524
  // src/data/check-cache.ts
@@ -518,10 +534,12 @@ async function checkCache(cacheId) {
518
534
 
519
535
  // src/data/write-cache-to-disk.ts
520
536
  async function writeCacheToDisk(cacheId, cached) {
521
- const cache = await getOrCreateCache();
522
- cache[cacheId] = cached;
523
- await saveCache(cache);
524
- return cache[cacheId];
537
+ return enqueueCacheTask(async () => {
538
+ const cache = await readCacheOrEmpty();
539
+ cache[cacheId] = cached;
540
+ await writeCacheFile(cache);
541
+ return cache[cacheId];
542
+ });
525
543
  }
526
544
  var providerEngineMap = {
527
545
  anthropic: "anthropic",
@@ -538,9 +556,23 @@ var providerEngineMap = {
538
556
  azure: "azure",
539
557
  meta: "meta"
540
558
  };
559
+ var cacheItemSchema = z.object({
560
+ original: z.string(),
561
+ translation: z.record(z.string(), z.string())
562
+ });
541
563
  function parseResponse(responseText) {
542
- const cleaned = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
543
- const parsed = JSON.parse(cleaned);
564
+ const fenced = responseText.trim().match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
565
+ const cleaned = fenced ? fenced[1] : responseText.trim();
566
+ let parsed;
567
+ try {
568
+ parsed = JSON.parse(cleaned);
569
+ } catch (error) {
570
+ throw new Error(`Invalid LLM response: not valid JSON (${error.message})`, { cause: error });
571
+ }
572
+ const result2 = cacheItemSchema.safeParse(parsed);
573
+ if (!result2.success) {
574
+ throw new Error(`Invalid LLM response: ${result2.error.message}`, { cause: result2.error });
575
+ }
544
576
  return parsed;
545
577
  }
546
578
  async function askLLM(prompt, prePrompt2, postPrompt2) {
@@ -550,7 +582,7 @@ async function askLLM(prompt, prePrompt2, postPrompt2) {
550
582
  if (!engineName) {
551
583
  throw new Error(`Unknown LLM provider: ${provider}`);
552
584
  }
553
- if (!config.llm.apiKey) {
585
+ if (!config.llm.apiKey && !keylessProviders.includes(provider)) {
554
586
  throw new Error(`API key for ${provider} is not configured in llm.apiKey`);
555
587
  }
556
588
  if (config.llm.sleep && config.llm.sleep > 0) {
@@ -588,19 +620,20 @@ function getPrependAndAppend(config, cached) {
588
620
  cached?.prepend ?? config.llm.prePromptAppend ?? "",
589
621
  config.llm.prePromptReplace ?? ""
590
622
  );
591
- const append = postPrompt(config, cached?.append ?? config.llm.postPrompt ?? "", config.llm.postPromptReplace ?? "");
623
+ const append = postPrompt(
624
+ config,
625
+ cached?.append ?? config.llm.postPromptPrepend ?? "",
626
+ config.llm.postPromptReplace ?? ""
627
+ );
592
628
  return { prepend, append };
593
629
  }
594
- async function rebuild(cacheId) {
595
- const config = await awaitedConfig;
596
- const cached = await checkCache(cacheId);
597
- if (!cached) throw new Error("Cache item not found");
630
+ async function translateAndStore(config, cacheId, text, cached, label) {
598
631
  const { prepend, append } = getPrependAndAppend(config, cached);
599
632
  if (config.debug) {
600
- console.log(`rebuild::prepend ${cacheId}:`, { prepend });
601
- console.log(`rebuild::append ${cacheId}:`, { append });
633
+ console.log(`${label}::prepend ${cacheId}:`, { prepend });
634
+ console.log(`${label}::append ${cacheId}:`, { append });
602
635
  }
603
- const translation = await askLLM(cached.original, prepend, append);
636
+ const translation = await askLLM(text, prepend, append);
604
637
  const output = {
605
638
  ...translation,
606
639
  prepend: cached?.prepend ?? void 0,
@@ -611,6 +644,12 @@ async function rebuild(cacheId) {
611
644
  }
612
645
  return { cacheId, translation: output };
613
646
  }
647
+ async function rebuild(cacheId) {
648
+ const config = await awaitedConfig;
649
+ const cached = await checkCache(cacheId);
650
+ if (!cached) throw new Error("Cache item not found");
651
+ return translateAndStore(config, cacheId, cached.original, cached, "rebuild");
652
+ }
614
653
  async function Gringow(text, ...values) {
615
654
  const config = await awaitedConfig;
616
655
  const interpolated = interpolateTemplate(text.trim(), values);
@@ -619,21 +658,7 @@ async function Gringow(text, ...values) {
619
658
  if (cached !== void 0 && cached.original !== "undefined" && !cached.rebuild) {
620
659
  return { cacheId, translation: cached };
621
660
  }
622
- const { prepend, append } = getPrependAndAppend(config, cached);
623
- if (config.debug) {
624
- console.log(`asking::prepend ${cacheId}:`, { prepend });
625
- console.log(`asking::append ${cacheId}:`, { append });
626
- }
627
- const translation = await askLLM(interpolated, prepend, append);
628
- const output = {
629
- ...translation,
630
- prepend: cached?.prepend ?? void 0,
631
- append: cached?.append ?? void 0
632
- };
633
- if (config.cache.write) {
634
- await writeCacheToDisk(cacheId, output);
635
- }
636
- return { cacheId, translation: output };
661
+ return translateAndStore(config, cacheId, interpolated, cached, "asking");
637
662
  }
638
663
 
639
664
  export { Gringow, rebuild };