@verbatra/sdk 0.6.0 → 0.6.1

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/dist/index.cjs CHANGED
@@ -359,77 +359,76 @@ function checkPlaceholders(source, translated) {
359
359
  reordered
360
360
  };
361
361
  }
362
- var LOCALE_TOKEN = "{locale}";
363
- function localeFilePath(cwd, pattern, locale) {
364
- return path.resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
365
- }
366
- var RATE_LIMITED_STATUS = 429;
367
- var TIMEOUT_STATUS = 408;
368
- var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
369
- var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
370
- "RateLimitError",
371
- "TooManyRequestsError"
372
- ]);
373
- var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
374
- "AuthenticationError",
375
- "PermissionDeniedError",
376
- "AuthorizationError"
377
- ]);
378
- var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
379
- "APIConnectionTimeoutError",
380
- "ConnectionError"
381
- ]);
382
- var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set(["APIUserAbortError"]);
383
- function getErrorStatus(error) {
384
- if (typeof error !== "object" || error === null || !("status" in error)) {
385
- return void 0;
362
+ var MIN_SOURCE_LENGTH = 8;
363
+ var MAX_LENGTH_MULTIPLE = 12;
364
+ var MAX_REPEAT_UNIT_LENGTH = 16;
365
+ var MIN_CONSECUTIVE_REPEATS = 8;
366
+ var REPEAT_COVERAGE_FRACTION = 0.5;
367
+ var MAX_SCAN_LENGTH = 8192;
368
+ function matchesUnit(text, a, b, unit) {
369
+ for (let i = 0; i < unit; i++) {
370
+ if (text[a + i] !== text[b + i]) {
371
+ return false;
372
+ }
386
373
  }
387
- const status = error.status;
388
- return typeof status === "number" ? status : void 0;
374
+ return true;
389
375
  }
390
- function readClassName(error) {
391
- if (typeof error !== "object" || error === null) {
392
- return void 0;
376
+ function countConsecutiveCopies(text, start, unit, scanLength) {
377
+ let copies = 1;
378
+ let next = start + unit;
379
+ while (next + unit <= scanLength && matchesUnit(text, start, next, unit)) {
380
+ copies++;
381
+ next += unit;
382
+ }
383
+ return copies;
384
+ }
385
+ function hasDominantRun(text, scanLength, unit, coverage) {
386
+ const limit = scanLength - unit;
387
+ let start = 0;
388
+ while (start <= limit) {
389
+ const copies = countConsecutiveCopies(text, start, unit, scanLength);
390
+ if (copies >= MIN_CONSECUTIVE_REPEATS && copies * unit >= coverage) {
391
+ return true;
392
+ }
393
+ start += copies * unit;
393
394
  }
394
- return error.constructor?.name;
395
+ return false;
395
396
  }
396
- function classifyProviderError(error) {
397
- const status = getErrorStatus(error);
398
- if (status === RATE_LIMITED_STATUS) {
399
- return "RATE_LIMITED";
400
- }
401
- if (status !== void 0 && AUTH_FAILED_STATUSES.has(status)) {
402
- return "AUTH_FAILED";
403
- }
404
- if (status === TIMEOUT_STATUS) {
405
- return "TIMEOUT";
406
- }
407
- const className = readClassName(error);
408
- if (className !== void 0) {
409
- if (RATE_LIMITED_CLASS_NAMES.has(className)) {
410
- return "RATE_LIMITED";
411
- }
412
- if (AUTH_FAILED_CLASS_NAMES.has(className)) {
413
- return "AUTH_FAILED";
414
- }
415
- if (TIMEOUT_CLASS_NAMES.has(className)) {
416
- return "TIMEOUT";
397
+ function hasRunawayRepetition(text) {
398
+ const scanLength = Math.min(text.length, MAX_SCAN_LENGTH);
399
+ const coverage = scanLength * REPEAT_COVERAGE_FRACTION;
400
+ const maxUnit = Math.min(
401
+ MAX_REPEAT_UNIT_LENGTH,
402
+ Math.floor(scanLength / MIN_CONSECUTIVE_REPEATS)
403
+ );
404
+ for (let unit = 1; unit <= maxUnit; unit++) {
405
+ if (hasDominantRun(text, scanLength, unit, coverage)) {
406
+ return true;
417
407
  }
418
408
  }
419
- return "PROVIDER_ERROR";
409
+ return false;
420
410
  }
421
- function isAbortShapedError(error) {
422
- if (error instanceof Error && error.name === "AbortError") {
423
- return true;
411
+ function assessValueDegeneracy(sourceValue, candidateValue) {
412
+ const source = sourceValue.trim();
413
+ const candidate = candidateValue.trim();
414
+ if (source.length >= MIN_SOURCE_LENGTH && candidate.length >= source.length * MAX_LENGTH_MULTIPLE) {
415
+ return { degenerate: true };
424
416
  }
425
- const className = readClassName(error);
426
- return className !== void 0 && ABORT_ERROR_CLASS_NAMES.has(className);
417
+ return { degenerate: hasRunawayRepetition(candidate) };
427
418
  }
428
- function isAbortError(error, signal) {
429
- if (signal !== void 0) {
430
- return signal.aborted === true && isAbortShapedError(error);
419
+ var LOCALE_TOKEN = "{locale}";
420
+ function localeFilePath(cwd, pattern, locale) {
421
+ return path.resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
422
+ }
423
+ function checkBatchIntegrity(inputs, extract, compare) {
424
+ const integrity = /* @__PURE__ */ new Map();
425
+ for (const { key, sourceValue, translatedValue } of inputs) {
426
+ integrity.set(
427
+ key,
428
+ compare !== void 0 ? compare(sourceValue, translatedValue) : checkPlaceholders(extract(sourceValue), extract(translatedValue))
429
+ );
431
430
  }
432
- return isAbortShapedError(error);
431
+ return integrity;
433
432
  }
434
433
  var REDACTED = "[REDACTED]";
435
434
  var KEY_PATTERNS = [
@@ -467,38 +466,6 @@ var ProviderError = class extends Error {
467
466
  this.code = code;
468
467
  }
469
468
  };
470
- var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
471
- var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
472
- var TIMEOUT_MESSAGE = "The translation provider request timed out.";
473
- var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
474
- var MESSAGE_BY_CODE = {
475
- RATE_LIMITED: RATE_LIMITED_MESSAGE,
476
- TIMEOUT: TIMEOUT_MESSAGE,
477
- AUTH_FAILED: AUTH_FAILED_MESSAGE,
478
- PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
479
- };
480
- async function guardProviderCall(call, signal) {
481
- signal?.throwIfAborted();
482
- try {
483
- return await call();
484
- } catch (error) {
485
- if (isAbortError(error, signal)) {
486
- throw error;
487
- }
488
- const code = classifyProviderError(error);
489
- throw new ProviderError(code, MESSAGE_BY_CODE[code]);
490
- }
491
- }
492
- function checkBatchIntegrity(inputs, extract, compare) {
493
- const integrity = /* @__PURE__ */ new Map();
494
- for (const { key, sourceValue, translatedValue } of inputs) {
495
- integrity.set(
496
- key,
497
- compare !== void 0 ? compare(sourceValue, translatedValue) : checkPlaceholders(extract(sourceValue), extract(translatedValue))
498
- );
499
- }
500
- return integrity;
501
- }
502
469
  var requestDataSchema = zod.z.object({
503
470
  sourceLocale: zod.z.string().min(1),
504
471
  targetLocale: zod.z.string().min(1),
@@ -741,6 +708,121 @@ function assertNotTruncated(truncated) {
741
708
  throw new ProviderError("OUTPUT_TRUNCATED", OUTPUT_TRUNCATED_MESSAGE);
742
709
  }
743
710
  }
711
+ var RATE_LIMITED_STATUS = 429;
712
+ var TIMEOUT_STATUS = 408;
713
+ var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
714
+ var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
715
+ "RateLimitError",
716
+ "TooManyRequestsError"
717
+ ]);
718
+ var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
719
+ "AuthenticationError",
720
+ "PermissionDeniedError",
721
+ "AuthorizationError"
722
+ ]);
723
+ var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
724
+ "APIConnectionTimeoutError",
725
+ "ConnectionError"
726
+ ]);
727
+ var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set(["APIUserAbortError"]);
728
+ function getErrorStatus(error) {
729
+ if (typeof error !== "object" || error === null || !("status" in error)) {
730
+ return void 0;
731
+ }
732
+ const status = error.status;
733
+ return typeof status === "number" ? status : void 0;
734
+ }
735
+ function readClassName(error) {
736
+ if (typeof error !== "object" || error === null) {
737
+ return void 0;
738
+ }
739
+ return error.constructor?.name;
740
+ }
741
+ function classifyProviderError(error) {
742
+ const status = getErrorStatus(error);
743
+ if (status === RATE_LIMITED_STATUS) {
744
+ return "RATE_LIMITED";
745
+ }
746
+ if (status !== void 0 && AUTH_FAILED_STATUSES.has(status)) {
747
+ return "AUTH_FAILED";
748
+ }
749
+ if (status === TIMEOUT_STATUS) {
750
+ return "TIMEOUT";
751
+ }
752
+ const className = readClassName(error);
753
+ if (className !== void 0) {
754
+ if (RATE_LIMITED_CLASS_NAMES.has(className)) {
755
+ return "RATE_LIMITED";
756
+ }
757
+ if (AUTH_FAILED_CLASS_NAMES.has(className)) {
758
+ return "AUTH_FAILED";
759
+ }
760
+ if (TIMEOUT_CLASS_NAMES.has(className)) {
761
+ return "TIMEOUT";
762
+ }
763
+ }
764
+ return "PROVIDER_ERROR";
765
+ }
766
+ function isAbortShapedError(error) {
767
+ if (error instanceof Error && error.name === "AbortError") {
768
+ return true;
769
+ }
770
+ const className = readClassName(error);
771
+ return className !== void 0 && ABORT_ERROR_CLASS_NAMES.has(className);
772
+ }
773
+ function isAbortError(error, signal) {
774
+ if (signal !== void 0) {
775
+ return signal.aborted === true && isAbortShapedError(error);
776
+ }
777
+ return isAbortShapedError(error);
778
+ }
779
+ var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
780
+ var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
781
+ var TIMEOUT_MESSAGE = "The translation provider request timed out.";
782
+ var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
783
+ var MESSAGE_BY_CODE = {
784
+ RATE_LIMITED: RATE_LIMITED_MESSAGE,
785
+ TIMEOUT: TIMEOUT_MESSAGE,
786
+ AUTH_FAILED: AUTH_FAILED_MESSAGE,
787
+ PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
788
+ };
789
+ async function guardProviderCall(call, signal) {
790
+ signal?.throwIfAborted();
791
+ try {
792
+ return await call();
793
+ } catch (error) {
794
+ if (isAbortError(error, signal)) {
795
+ throw error;
796
+ }
797
+ const code = classifyProviderError(error);
798
+ throw new ProviderError(code, MESSAGE_BY_CODE[code]);
799
+ }
800
+ }
801
+ var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
802
+ function requestTimedOutMessage(timeoutMs) {
803
+ return `The translation provider request exceeded the ${timeoutMs} ms request timeout.`;
804
+ }
805
+ function combineSignals(caller, timeout) {
806
+ return caller === void 0 ? timeout : AbortSignal.any([caller, timeout]);
807
+ }
808
+ async function withRequestTimeout(timeoutMs, callerSignal, call) {
809
+ const timeoutController = new AbortController();
810
+ const signal = combineSignals(callerSignal, timeoutController.signal);
811
+ let timer;
812
+ const timedOut = new Promise((_, reject) => {
813
+ timer = setTimeout(() => {
814
+ timeoutController.abort();
815
+ reject(new ProviderError("TIMEOUT", requestTimedOutMessage(timeoutMs)));
816
+ }, timeoutMs);
817
+ });
818
+ try {
819
+ return await Promise.race([guardProviderCall(() => call(signal), signal), timedOut]);
820
+ } finally {
821
+ if (timer !== void 0) {
822
+ clearTimeout(timer);
823
+ }
824
+ }
825
+ }
744
826
  var PROVIDER_ENV = {
745
827
  anthropic: "ANTHROPIC_API_KEY",
746
828
  openai: "OPENAI_API_KEY",
@@ -795,7 +877,13 @@ function createDefaultClient() {
795
877
  }
796
878
  var anthropicConfigSchema = zod.z.object({
797
879
  model: zod.z.string().min(1),
798
- maxTokens: zod.z.number().int().positive()
880
+ maxTokens: zod.z.number().int().positive(),
881
+ /**
882
+ * Optional verbatra-imposed per-request timeout in milliseconds. A positive integer; when absent,
883
+ * the shared default request timeout applies. Bounds each outbound request so a hung-but-alive
884
+ * server cannot hold a locale's write lock open forever.
885
+ */
886
+ requestTimeoutMs: zod.z.number().int().positive().optional()
799
887
  });
800
888
  var SUBMIT_TOOL_NAME = "submit_translations";
801
889
  var SYSTEM_RULES = [
@@ -855,10 +943,11 @@ function createAnthropicProvider(config, deps = {}) {
855
943
  };
856
944
  }
857
945
  function createMechanism(client, config) {
946
+ const timeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
858
947
  return {
859
948
  translate: async ({ payloadJson, signal }) => {
860
949
  const body = buildRequest(config, payloadJson);
861
- const message = await callClient(client, body, signal);
950
+ const message = await callClient(client, body, timeoutMs, signal);
862
951
  assertNotTruncated(message.stop_reason === "max_tokens");
863
952
  const raw = requireToolInput(message.content);
864
953
  const usage = toUsage(message.usage);
@@ -866,10 +955,11 @@ function createMechanism(client, config) {
866
955
  }
867
956
  };
868
957
  }
869
- function callClient(client, body, signal) {
870
- return guardProviderCall(
871
- () => client.messages.create(body, signal !== void 0 ? { signal } : void 0),
872
- signal
958
+ function callClient(client, body, timeoutMs, signal) {
959
+ return withRequestTimeout(
960
+ timeoutMs,
961
+ signal,
962
+ (requestSignal) => client.messages.create(body, { signal: requestSignal })
873
963
  );
874
964
  }
875
965
  function toUsage(usage) {
@@ -883,7 +973,14 @@ function toUsage(usage) {
883
973
  return { inputTokens: input_tokens, outputTokens: output_tokens };
884
974
  }
885
975
  var deepLConfigSchema = zod.z.object({
886
- glossaryId: zod.z.string().min(1).optional()
976
+ glossaryId: zod.z.string().min(1).optional(),
977
+ /**
978
+ * Optional verbatra-imposed per-request timeout in milliseconds. A positive integer; when absent,
979
+ * the shared default request timeout applies. Bounds each outbound request so a hung-but-alive
980
+ * server cannot hold a locale's write lock open forever. deepl-node cannot cancel an in-flight
981
+ * request, so on timeout verbatra's await is released but the underlying request is left to settle.
982
+ */
983
+ requestTimeoutMs: zod.z.number().int().positive().optional()
887
984
  });
888
985
  var DEEPL_LOGGER = "deepl";
889
986
  function resolveDeeplLoglevel(requireFn = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
@@ -1054,6 +1151,7 @@ async function translate(bundle, config, request) {
1054
1151
  options,
1055
1152
  request.extractPlaceholders,
1056
1153
  request.comparePlaceholders,
1154
+ config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
1057
1155
  request.signal
1058
1156
  );
1059
1157
  if (unprotectable.length > 0) {
@@ -1088,7 +1186,7 @@ function buildReviewFlags2(protectable, data, values, integrity, glossary) {
1088
1186
  }
1089
1187
  return reviewFlags;
1090
1188
  }
1091
- async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
1189
+ async function translateProtectable(client, data, protectable, options, extract, compare, timeoutMs, signal) {
1092
1190
  if (protectable.length === 0) {
1093
1191
  return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
1094
1192
  }
@@ -1099,29 +1197,45 @@ async function translateProtectable(client, data, protectable, options, extract,
1099
1197
  data.sourceLocale,
1100
1198
  data.targetLocale,
1101
1199
  options,
1200
+ timeoutMs,
1102
1201
  signal
1103
1202
  );
1104
1203
  const { values, integrityInputs } = zipResults(protectable, results);
1105
1204
  const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
1106
1205
  return { values, integrity };
1107
1206
  }
1108
- function callClient2(client, texts, sourceLang, targetLang, options, signal) {
1109
- return guardProviderCall(
1110
- () => client.translateText(texts, sourceLang, targetLang, options),
1111
- signal
1207
+ function callClient2(client, texts, sourceLang, targetLang, options, timeoutMs, signal) {
1208
+ return withRequestTimeout(
1209
+ timeoutMs,
1210
+ signal,
1211
+ () => client.translateText(texts, sourceLang, targetLang, options)
1112
1212
  );
1113
1213
  }
1114
- async function callClientChunked(client, texts, sourceLang, targetLang, options, signal) {
1214
+ async function callClientChunked(client, texts, sourceLang, targetLang, options, timeoutMs, signal) {
1115
1215
  const results = [];
1116
1216
  for (const chunk2 of chunkTextsForDeepL(texts)) {
1117
- const chunkResults = await callClient2(client, chunk2, sourceLang, targetLang, options, signal);
1217
+ const chunkResults = await callClient2(
1218
+ client,
1219
+ chunk2,
1220
+ sourceLang,
1221
+ targetLang,
1222
+ options,
1223
+ timeoutMs,
1224
+ signal
1225
+ );
1118
1226
  results.push(...chunkResults);
1119
1227
  }
1120
1228
  return results;
1121
1229
  }
1122
1230
  var geminiConfigSchema = zod.z.object({
1123
1231
  model: zod.z.string().min(1),
1124
- maxOutputTokens: zod.z.number().int().positive()
1232
+ maxOutputTokens: zod.z.number().int().positive(),
1233
+ /**
1234
+ * Optional verbatra-imposed per-request timeout in milliseconds. A positive integer; when absent,
1235
+ * the shared default request timeout applies. Bounds each outbound request so a hung-but-alive
1236
+ * server cannot hold a locale's write lock open forever.
1237
+ */
1238
+ requestTimeoutMs: zod.z.number().int().positive().optional()
1125
1239
  });
1126
1240
  var DEFAULT_GEMINI_RETRY = { attempts: 3, baseDelayMs: 250 };
1127
1241
  function isRetryableStatus(error) {
@@ -1311,20 +1425,27 @@ function createGeminiProvider(config, deps = {}) {
1311
1425
  };
1312
1426
  }
1313
1427
  function createMechanism2(client, config) {
1428
+ const timeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1314
1429
  return {
1315
1430
  translate: async ({ payloadJson, signal }) => {
1316
- const request = buildGeminiRequest(config, payloadJson, signal);
1317
- const response = await callClient3(client, request, signal);
1431
+ const response = await withRequestTimeout(
1432
+ timeoutMs,
1433
+ signal,
1434
+ (requestSignal) => client.models.generateContent(buildGeminiRequest(config, payloadJson, requestSignal))
1435
+ );
1318
1436
  return extractGeminiResult(response);
1319
1437
  }
1320
1438
  };
1321
1439
  }
1322
- function callClient3(client, request, signal) {
1323
- return guardProviderCall(() => client.models.generateContent(request), signal);
1324
- }
1325
1440
  var openAiConfigSchema = zod.z.object({
1326
1441
  model: zod.z.string().min(1),
1327
- maxOutputTokens: zod.z.number().int().positive()
1442
+ maxOutputTokens: zod.z.number().int().positive(),
1443
+ /**
1444
+ * Optional verbatra-imposed per-request timeout in milliseconds. A positive integer; when absent,
1445
+ * the shared default request timeout applies. Bounds each outbound request so a hung-but-alive
1446
+ * server cannot hold a locale's write lock open forever.
1447
+ */
1448
+ requestTimeoutMs: zod.z.number().int().positive().optional()
1328
1449
  });
1329
1450
  function createDefaultClient4() {
1330
1451
  const sdk = new OpenAI__default.default({ apiKey: requireOpenAiKey(), logLevel: "off" });
@@ -1479,18 +1600,20 @@ function createOpenAiProvider(config, deps = {}) {
1479
1600
  };
1480
1601
  }
1481
1602
  function createMechanism3(client, config) {
1603
+ const timeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1482
1604
  return {
1483
1605
  translate: async ({ payloadJson, signal }) => {
1484
1606
  const body = buildOpenAiRequest(config, payloadJson);
1485
- const completion = await callClient4(client, body, signal);
1607
+ const completion = await callClient3(client, body, timeoutMs, signal);
1486
1608
  return extractOpenAiResult(completion);
1487
1609
  }
1488
1610
  };
1489
1611
  }
1490
- function callClient4(client, body, signal) {
1491
- return guardProviderCall(
1492
- () => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
1493
- signal
1612
+ function callClient3(client, body, timeoutMs, signal) {
1613
+ return withRequestTimeout(
1614
+ timeoutMs,
1615
+ signal,
1616
+ (requestSignal) => client.chat.completions.create(body, { signal: requestSignal })
1494
1617
  );
1495
1618
  }
1496
1619
  var HOSTED_PROVIDER_ENV_VARS = new Set(
@@ -1513,7 +1636,13 @@ var openAiCompatibleConfigSchema = zod.z.object({
1513
1636
  maxOutputTokens: zod.z.number().int().positive(),
1514
1637
  apiKeyEnvVar: zod.z.string().min(1).refine(isNotHostedProviderEnvVar, {
1515
1638
  message: "apiKeyEnvVar must not name a hosted provider's environment variable."
1516
- }).optional()
1639
+ }).optional(),
1640
+ /**
1641
+ * Optional verbatra-imposed per-request timeout in milliseconds. A positive integer; when absent,
1642
+ * the shared default request timeout applies. Bounds each outbound request so a hung-but-alive local
1643
+ * server (the reported stuck-LM-Studio case) cannot hold a locale's write lock open forever.
1644
+ */
1645
+ requestTimeoutMs: zod.z.number().int().positive().optional()
1517
1646
  });
1518
1647
  function createDefaultClient5(config) {
1519
1648
  const sdk = new OpenAI__default.default({
@@ -1545,18 +1674,20 @@ function createOpenAiCompatibleProvider(config, deps = {}) {
1545
1674
  };
1546
1675
  }
1547
1676
  function createMechanism4(client, config) {
1677
+ const timeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1548
1678
  return {
1549
1679
  translate: async ({ payloadJson, signal }) => {
1550
1680
  const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
1551
- const completion = await callClient5(client, body, signal);
1681
+ const completion = await callClient4(client, body, timeoutMs, signal);
1552
1682
  return extractOpenAiResult(completion, true);
1553
1683
  }
1554
1684
  };
1555
1685
  }
1556
- function callClient5(client, body, signal) {
1557
- return guardProviderCall(
1558
- () => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
1559
- signal
1686
+ function callClient4(client, body, timeoutMs, signal) {
1687
+ return withRequestTimeout(
1688
+ timeoutMs,
1689
+ signal,
1690
+ (requestSignal) => client.chat.completions.create(body, { signal: requestSignal })
1560
1691
  );
1561
1692
  }
1562
1693
  var SCAFFOLD_MODELS = {
@@ -1766,6 +1897,8 @@ var LOCAL_DIR_NAME = ".verbatra-local";
1766
1897
  var LOCK_FILE_GUARD_STEM = "_lockfile";
1767
1898
  var DEFAULT_POLL_INTERVAL_MS = 100;
1768
1899
  var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
1900
+ var WAIT_NOTICE_INTERVAL_MS = 1e3;
1901
+ var MAX_LOCK_PAYLOAD_BYTES = 64 * 1024;
1769
1902
  function lockPath(cwd, stem) {
1770
1903
  return path.resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
1771
1904
  }
@@ -1783,7 +1916,47 @@ function sleep(ms) {
1783
1916
  function lockPayload() {
1784
1917
  return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
1785
1918
  }
1786
- async function acquireLock(path, fs, pollIntervalMs, deadline) {
1919
+ function parseHolder(read) {
1920
+ if (read.kind !== "ok") {
1921
+ return void 0;
1922
+ }
1923
+ let parsed;
1924
+ try {
1925
+ parsed = JSON.parse(read.content);
1926
+ } catch {
1927
+ return void 0;
1928
+ }
1929
+ if (typeof parsed !== "object" || parsed === null) {
1930
+ return void 0;
1931
+ }
1932
+ const record = parsed;
1933
+ const holder = {};
1934
+ if (typeof record.pid === "number") {
1935
+ holder.pid = record.pid;
1936
+ }
1937
+ if (typeof record.acquiredAt === "string") {
1938
+ holder.acquiredAt = record.acquiredAt;
1939
+ }
1940
+ return holder;
1941
+ }
1942
+ function makeWaitNotifier(path, fs, onWait, start) {
1943
+ let holder;
1944
+ let holderRead = false;
1945
+ let lastEmit;
1946
+ return async () => {
1947
+ if (!holderRead) {
1948
+ holderRead = true;
1949
+ holder = parseHolder(await fs.readFileBounded(path, MAX_LOCK_PAYLOAD_BYTES));
1950
+ }
1951
+ const elapsedMs = Date.now() - start;
1952
+ if (lastEmit !== void 0 && elapsedMs - lastEmit < WAIT_NOTICE_INTERVAL_MS) {
1953
+ return;
1954
+ }
1955
+ lastEmit = elapsedMs;
1956
+ onWait({ lockPath: path, elapsedMs, ...holder !== void 0 ? { holder } : {} });
1957
+ };
1958
+ }
1959
+ async function acquireLock(path, fs, pollIntervalMs, deadline, notify) {
1787
1960
  for (; ; ) {
1788
1961
  if (await fs.createExclusive(path, lockPayload())) {
1789
1962
  return;
@@ -1794,6 +1967,9 @@ async function acquireLock(path, fs, pollIntervalMs, deadline) {
1794
1967
  `Could not acquire the write lock at ${path}: another process may be holding it. If no verbatra process is currently running, this lock file was likely left behind by one that was killed; delete it and retry.`
1795
1968
  );
1796
1969
  }
1970
+ if (notify !== void 0) {
1971
+ await notify();
1972
+ }
1797
1973
  const jitter = Math.random() * pollIntervalMs;
1798
1974
  await sleep(pollIntervalMs + jitter);
1799
1975
  }
@@ -1801,7 +1977,9 @@ async function acquireLock(path, fs, pollIntervalMs, deadline) {
1801
1977
  async function withFileLock(path, fs, fn, options) {
1802
1978
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1803
1979
  const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
1804
- await acquireLock(path, fs, pollIntervalMs, Date.now() + acquireTimeoutMs);
1980
+ const start = Date.now();
1981
+ const notify = options.onWait !== void 0 ? makeWaitNotifier(path, fs, options.onWait, start) : void 0;
1982
+ await acquireLock(path, fs, pollIntervalMs, start + acquireTimeoutMs, notify);
1805
1983
  try {
1806
1984
  return await fn();
1807
1985
  } finally {
@@ -3486,6 +3664,9 @@ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
3486
3664
  if (!adapter.validateMessage(candidateValue)) {
3487
3665
  return { accepted: false, reason: "icu" };
3488
3666
  }
3667
+ if (assessValueDegeneracy(sourceEntry.value, candidateValue).degenerate) {
3668
+ return { accepted: false, reason: "degenerate" };
3669
+ }
3489
3670
  return { accepted: true };
3490
3671
  }
3491
3672
 
@@ -3773,7 +3954,7 @@ var runBudgetSchema = zod.z.object({
3773
3954
  });
3774
3955
  var runStatusLocaleSchema = zod.z.object({
3775
3956
  locale: zod.z.string(),
3776
- status: zod.z.enum(["succeeded", "failed"]),
3957
+ status: zod.z.enum(["succeeded", "partial", "failed"]),
3777
3958
  needsReview: zod.z.array(needsReviewEntrySchema),
3778
3959
  usage: usageSummarySchema.optional()
3779
3960
  });
@@ -3918,10 +4099,21 @@ function failureSummary(locale, error) {
3918
4099
  error: describeError(error)
3919
4100
  };
3920
4101
  }
4102
+ function deriveLocaleStatus(parts) {
4103
+ const withheld = parts.integrityMismatches.length > 0 || parts.providerFailures.length > 0 || parts.budgetWithheld.length > 0;
4104
+ if (!withheld) {
4105
+ return "succeeded";
4106
+ }
4107
+ const accepted = parts.translated.length > 0 || parts.generated.length > 0;
4108
+ return accepted ? "partial" : "failed";
4109
+ }
3921
4110
  function partition(locales) {
3922
- const succeeded = locales.filter((s) => s.status === "succeeded").map((s) => s.locale);
3923
- const failed = locales.filter((s) => s.status === "failed").map((s) => s.locale);
3924
- return { succeeded, failed };
4111
+ const namesWith = (status) => locales.filter((s) => s.status === status).map((s) => s.locale);
4112
+ return {
4113
+ succeeded: namesWith("succeeded"),
4114
+ partial: namesWith("partial"),
4115
+ failed: namesWith("failed")
4116
+ };
3925
4117
  }
3926
4118
 
3927
4119
  // src/flow/batching.ts
@@ -4402,7 +4594,7 @@ function pluralNoticeFor(params, merged) {
4402
4594
  function baseSummary(parts) {
4403
4595
  return {
4404
4596
  locale: parts.locale,
4405
- status: "succeeded",
4597
+ status: deriveLocaleStatus(parts),
4406
4598
  translated: parts.translated,
4407
4599
  unchanged: parts.unchanged,
4408
4600
  orphaned: parts.orphaned,
@@ -4461,10 +4653,16 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
4461
4653
  try {
4462
4654
  result = await provider.translateBatch(buildRequest3(params, batch));
4463
4655
  } catch (error) {
4464
- for (const entry of batch) {
4465
- providerFailures.push(entry.key);
4466
- }
4467
- return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
4656
+ return handleSubBatchFailure(
4657
+ error,
4658
+ provider,
4659
+ params,
4660
+ batch,
4661
+ accepted,
4662
+ integrityMismatches,
4663
+ providerFailures,
4664
+ reviewFlags
4665
+ );
4468
4666
  }
4469
4667
  for (const entry of batch) {
4470
4668
  foldEntryResult(entry, result, params.adapter, accepted, integrityMismatches, providerFailures);
@@ -4476,6 +4674,44 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
4476
4674
  }
4477
4675
  return { notices: readNotices(result), usage: result.usage };
4478
4676
  }
4677
+ function isOutputTruncated(error) {
4678
+ return error instanceof ProviderError && error.code === "OUTPUT_TRUNCATED";
4679
+ }
4680
+ async function handleSubBatchFailure(error, provider, params, batch, accepted, integrityMismatches, providerFailures, reviewFlags) {
4681
+ if (isOutputTruncated(error) && batch.length > 1) {
4682
+ return retryTruncatedSplit(
4683
+ provider,
4684
+ params,
4685
+ batch,
4686
+ accepted,
4687
+ integrityMismatches,
4688
+ providerFailures,
4689
+ reviewFlags
4690
+ );
4691
+ }
4692
+ for (const entry of batch) {
4693
+ providerFailures.push(entry.key);
4694
+ }
4695
+ return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
4696
+ }
4697
+ async function retryTruncatedSplit(provider, params, batch, accepted, integrityMismatches, providerFailures, reviewFlags) {
4698
+ const notices = [];
4699
+ let usage;
4700
+ for (const half of chunk(batch, Math.ceil(batch.length / 2))) {
4701
+ const sub = await runSubBatch(
4702
+ provider,
4703
+ params,
4704
+ half,
4705
+ accepted,
4706
+ integrityMismatches,
4707
+ providerFailures,
4708
+ reviewFlags
4709
+ );
4710
+ notices.push(...sub.notices);
4711
+ usage = combineUsage(usage, sub.usage);
4712
+ }
4713
+ return { notices, usage };
4714
+ }
4479
4715
  function foldEntryResult(entry, result, adapter, accepted, integrityMismatches, providerFailures) {
4480
4716
  const value = result.values.get(entry.key);
4481
4717
  if (value === void 0) {
@@ -4559,16 +4795,26 @@ async function runDryLocale(context, targetLocale, lock) {
4559
4795
  return (await runLocale(params)).summary;
4560
4796
  }
4561
4797
  async function runLiveLocale(context, targetLocale) {
4562
- return withLocaleWriteLock(context.cwd, targetLocale, context.fs, async () => {
4563
- const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4564
- const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
4565
- const result = await runLocale(params);
4566
- await updateLockFileLocale(context.cwd, context.fs, targetLocale, {
4567
- mode: "replace",
4568
- entries: result.lockEntries
4569
- });
4570
- return result.summary;
4571
- });
4798
+ const lockOptions = {
4799
+ ...context.onLockWait !== void 0 ? { onWait: context.onLockWait } : {},
4800
+ ...context.lockAcquireTimeoutMs !== void 0 ? { acquireTimeoutMs: context.lockAcquireTimeoutMs } : {}
4801
+ };
4802
+ return withLocaleWriteLock(
4803
+ context.cwd,
4804
+ targetLocale,
4805
+ context.fs,
4806
+ async () => {
4807
+ const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4808
+ const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
4809
+ const result = await runLocale(params);
4810
+ await updateLockFileLocale(context.cwd, context.fs, targetLocale, {
4811
+ mode: "replace",
4812
+ entries: result.lockEntries
4813
+ });
4814
+ return result.summary;
4815
+ },
4816
+ lockOptions
4817
+ );
4572
4818
  }
4573
4819
  async function runOneLocale(targetLocale, run) {
4574
4820
  try {
@@ -4622,10 +4868,12 @@ async function translate2(input, deps = {}) {
4622
4868
  generatePlurals,
4623
4869
  maxBatchSize,
4624
4870
  fs,
4625
- budget
4871
+ budget,
4872
+ ...input.onLockWait !== void 0 ? { onLockWait: input.onLockWait } : {},
4873
+ ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {}
4626
4874
  };
4627
4875
  const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
4628
- const { succeeded, failed } = partition(summaries);
4876
+ const { succeeded, partial, failed } = partition(summaries);
4629
4877
  const usage = summaries.reduce(
4630
4878
  (total, summary2) => combineUsage(total, summary2.usage),
4631
4879
  void 0
@@ -4635,6 +4883,7 @@ async function translate2(input, deps = {}) {
4635
4883
  dryRun,
4636
4884
  locales: summaries,
4637
4885
  succeeded,
4886
+ partial,
4638
4887
  failed,
4639
4888
  ...usage !== void 0 ? { usage } : {},
4640
4889
  ...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
@@ -5193,15 +5442,23 @@ function importLocale(params) {
5193
5442
  classifyRows(params, buckets);
5194
5443
  const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
5195
5444
  const invalidIcuSource = [...new Set(params.sourceInvalidIcuKeys)].filter((key) => rowKeys.has(key)).sort();
5445
+ const translated = [...buckets.accepted.keys()].sort();
5446
+ const integrityMismatches = [...buckets.mismatches].sort();
5196
5447
  const summary = {
5197
5448
  locale: params.sheet.locale,
5198
- status: "succeeded",
5199
- translated: [...buckets.accepted.keys()].sort(),
5449
+ status: deriveLocaleStatus({
5450
+ translated,
5451
+ generated: [],
5452
+ integrityMismatches,
5453
+ providerFailures: [],
5454
+ budgetWithheld: []
5455
+ }),
5456
+ translated,
5200
5457
  unchanged: diff2.unchanged,
5201
5458
  orphaned: diff2.orphaned,
5202
5459
  pruned: [],
5203
5460
  invalidIcuSource,
5204
- integrityMismatches: [...buckets.mismatches].sort(),
5461
+ integrityMismatches,
5205
5462
  providerFailures: [],
5206
5463
  budgetWithheld: [],
5207
5464
  generated: [],
@@ -5330,8 +5587,8 @@ async function importWorkbook(input, deps = {}) {
5330
5587
  summaries.push(failureSummary(sheet.locale, error));
5331
5588
  }
5332
5589
  }
5333
- const { succeeded, failed } = partition(summaries);
5334
- return { dryRun, locales: summaries, succeeded, failed };
5590
+ const { succeeded, partial, failed } = partition(summaries);
5591
+ return { dryRun, locales: summaries, succeeded, partial, failed };
5335
5592
  }
5336
5593
 
5337
5594
  // src/lock/load-lock-file.ts
@@ -5389,7 +5646,12 @@ async function watch(input, deps = {}) {
5389
5646
  );
5390
5647
  }
5391
5648
  const runTranslate = deps.runTranslate ?? defaultRunTranslate(deps);
5392
- const runInput = { config: input.config, cwd };
5649
+ const runInput = {
5650
+ config: input.config,
5651
+ cwd,
5652
+ ...input.onLockWait !== void 0 ? { onLockWait: input.onLockWait } : {},
5653
+ ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {}
5654
+ };
5393
5655
  let state = "idle";
5394
5656
  let pending = false;
5395
5657
  let stopped = false;