@verbatra/sdk 0.5.0-next.1 → 0.5.0-next.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.
package/dist/index.d.cts CHANGED
@@ -100,7 +100,31 @@ interface Usage {
100
100
  readonly inputTokens: number;
101
101
  readonly outputTokens: number;
102
102
  }
103
- /** Result of a batch translation: per-key values and per-key integrity outcomes. */
103
+ /**
104
+ * Stable codes for a provider's graceful-degradation notices. These are returned DATA on a
105
+ * successful result, NOT thrown:
106
+ *
107
+ * - `FORMALITY_DOWNGRADED`: a requested formality was not applied (DeepL's free tier does not
108
+ * support it).
109
+ * - `GLOSSARY_IGNORED`: a supplied generic glossary term map was not applied (DeepL only applies a
110
+ * native glossary id, never a term map).
111
+ * - `PLACEHOLDER_UNSUPPORTED`: at least one placeholder- or ICU-bearing entry was left untranslated
112
+ * because the provider cannot preserve those tokens; such entries are withheld (absent from the
113
+ * result maps) rather than sent to the provider and mangled.
114
+ */
115
+ type ProviderNoticeCode = "FORMALITY_DOWNGRADED" | "GLOSSARY_IGNORED" | "PLACEHOLDER_UNSUPPORTED";
116
+ /**
117
+ * An observable, structured signal that something was gracefully degraded (not an
118
+ * error). Carries only a stable code and a static message, never a key or content.
119
+ * Surfaced as result data, never thrown; callers inspect it but need not treat it as a failure.
120
+ */
121
+ interface ProviderNotice {
122
+ /** The stable {@link ProviderNoticeCode} for what was degraded. */
123
+ readonly code: ProviderNoticeCode;
124
+ /** A static, safe description; never a key or translatable content. */
125
+ readonly message: string;
126
+ }
127
+ /** Result of a batch translation: per-key values, per-key integrity outcomes, and any notices. */
104
128
  interface TranslateResult {
105
129
  /** The translated value for each requested key. */
106
130
  readonly values: ReadonlyMap<string, string>;
@@ -108,6 +132,12 @@ interface TranslateResult {
108
132
  readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult$1>;
109
133
  /** Token usage when the provider reports it; absent for token-less providers. */
110
134
  readonly usage?: Usage;
135
+ /**
136
+ * Graceful-degradation notices for this batch. Every provider populates this as a present array:
137
+ * DeepL reports real notices (for example `GLOSSARY_IGNORED`); an LLM provider with nothing to
138
+ * report returns an empty array rather than omitting the field.
139
+ */
140
+ readonly notices?: readonly ProviderNotice[];
111
141
  }
112
142
  /**
113
143
  * The single contract every provider implements. It is narrow enough that a machine-translation API like
@@ -166,28 +196,6 @@ interface TranslationProvider {
166
196
  * are still accepted. This drives editor completions only; the runtime schema validates.
167
197
  */
168
198
  type AnthropicModel = Anthropic.Messages.Model;
169
- /**
170
- * Stable codes for the DeepL graceful-degradation notices, DeepL only. These are returned DATA on a
171
- * successful result, NOT thrown:
172
- *
173
- * - `FORMALITY_DOWNGRADED`: a requested formality was not applied (the free tier does not support it).
174
- * - `GLOSSARY_IGNORED`: a supplied generic glossary was not applied.
175
- * - `PLACEHOLDER_UNSUPPORTED`: at least one placeholder- or ICU-bearing entry was left untranslated
176
- * because DeepL cannot preserve those tokens; such entries are withheld (absent from the result maps)
177
- * rather than sent to DeepL and mangled.
178
- */
179
- type ProviderNoticeCode = "FORMALITY_DOWNGRADED" | "GLOSSARY_IGNORED" | "PLACEHOLDER_UNSUPPORTED";
180
- /**
181
- * An observable, structured signal that something was gracefully degraded (not an
182
- * error). Carries only a stable code and a static message, never a key or content.
183
- * Surfaced as result data, never thrown; callers inspect it but need not treat it as a failure.
184
- */
185
- interface ProviderNotice {
186
- /** The stable {@link ProviderNoticeCode} for what was degraded. */
187
- readonly code: ProviderNoticeCode;
188
- /** A static, safe description; never a key or translatable content. */
189
- readonly message: string;
190
- }
191
199
 
192
200
  /**
193
201
  * The Gemini authoring model type, sourced from the installed SDK's own model union so
@@ -943,11 +951,13 @@ interface LocaleSummary {
943
951
  */
944
952
  readonly integrityMismatches: readonly string[];
945
953
  /**
946
- * Keys withheld because the provider call itself failed (for example a revoked API key, a rate
947
- * limit, or a network timeout), as distinct from a translation that came back and failed the
948
- * placeholder-integrity check. Nothing was translated for these keys. The corresponding
949
- * {@link LocaleSummary.notices} entry carries the secret-free failure code and message. Empty in a
950
- * dry-run and empty unless a provider call threw.
954
+ * Keys withheld because nothing was translated for them this run, as distinct from a translation
955
+ * that came back and failed the placeholder-integrity check. This covers two causes: the provider
956
+ * call itself failed (for example a revoked API key, a rate limit, or a network timeout), in which
957
+ * case the corresponding {@link LocaleSummary.notices} entry carries the secret-free failure code
958
+ * and message; or the call succeeded but the key was still missing or duplicated in the response
959
+ * after the shared LLM layer's bounded reconcile repair round, in which case no notice is added.
960
+ * Empty in a dry-run.
951
961
  */
952
962
  readonly providerFailures: readonly string[];
953
963
  /**
package/dist/index.d.ts CHANGED
@@ -100,7 +100,31 @@ interface Usage {
100
100
  readonly inputTokens: number;
101
101
  readonly outputTokens: number;
102
102
  }
103
- /** Result of a batch translation: per-key values and per-key integrity outcomes. */
103
+ /**
104
+ * Stable codes for a provider's graceful-degradation notices. These are returned DATA on a
105
+ * successful result, NOT thrown:
106
+ *
107
+ * - `FORMALITY_DOWNGRADED`: a requested formality was not applied (DeepL's free tier does not
108
+ * support it).
109
+ * - `GLOSSARY_IGNORED`: a supplied generic glossary term map was not applied (DeepL only applies a
110
+ * native glossary id, never a term map).
111
+ * - `PLACEHOLDER_UNSUPPORTED`: at least one placeholder- or ICU-bearing entry was left untranslated
112
+ * because the provider cannot preserve those tokens; such entries are withheld (absent from the
113
+ * result maps) rather than sent to the provider and mangled.
114
+ */
115
+ type ProviderNoticeCode = "FORMALITY_DOWNGRADED" | "GLOSSARY_IGNORED" | "PLACEHOLDER_UNSUPPORTED";
116
+ /**
117
+ * An observable, structured signal that something was gracefully degraded (not an
118
+ * error). Carries only a stable code and a static message, never a key or content.
119
+ * Surfaced as result data, never thrown; callers inspect it but need not treat it as a failure.
120
+ */
121
+ interface ProviderNotice {
122
+ /** The stable {@link ProviderNoticeCode} for what was degraded. */
123
+ readonly code: ProviderNoticeCode;
124
+ /** A static, safe description; never a key or translatable content. */
125
+ readonly message: string;
126
+ }
127
+ /** Result of a batch translation: per-key values, per-key integrity outcomes, and any notices. */
104
128
  interface TranslateResult {
105
129
  /** The translated value for each requested key. */
106
130
  readonly values: ReadonlyMap<string, string>;
@@ -108,6 +132,12 @@ interface TranslateResult {
108
132
  readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult$1>;
109
133
  /** Token usage when the provider reports it; absent for token-less providers. */
110
134
  readonly usage?: Usage;
135
+ /**
136
+ * Graceful-degradation notices for this batch. Every provider populates this as a present array:
137
+ * DeepL reports real notices (for example `GLOSSARY_IGNORED`); an LLM provider with nothing to
138
+ * report returns an empty array rather than omitting the field.
139
+ */
140
+ readonly notices?: readonly ProviderNotice[];
111
141
  }
112
142
  /**
113
143
  * The single contract every provider implements. It is narrow enough that a machine-translation API like
@@ -166,28 +196,6 @@ interface TranslationProvider {
166
196
  * are still accepted. This drives editor completions only; the runtime schema validates.
167
197
  */
168
198
  type AnthropicModel = Anthropic.Messages.Model;
169
- /**
170
- * Stable codes for the DeepL graceful-degradation notices, DeepL only. These are returned DATA on a
171
- * successful result, NOT thrown:
172
- *
173
- * - `FORMALITY_DOWNGRADED`: a requested formality was not applied (the free tier does not support it).
174
- * - `GLOSSARY_IGNORED`: a supplied generic glossary was not applied.
175
- * - `PLACEHOLDER_UNSUPPORTED`: at least one placeholder- or ICU-bearing entry was left untranslated
176
- * because DeepL cannot preserve those tokens; such entries are withheld (absent from the result maps)
177
- * rather than sent to DeepL and mangled.
178
- */
179
- type ProviderNoticeCode = "FORMALITY_DOWNGRADED" | "GLOSSARY_IGNORED" | "PLACEHOLDER_UNSUPPORTED";
180
- /**
181
- * An observable, structured signal that something was gracefully degraded (not an
182
- * error). Carries only a stable code and a static message, never a key or content.
183
- * Surfaced as result data, never thrown; callers inspect it but need not treat it as a failure.
184
- */
185
- interface ProviderNotice {
186
- /** The stable {@link ProviderNoticeCode} for what was degraded. */
187
- readonly code: ProviderNoticeCode;
188
- /** A static, safe description; never a key or translatable content. */
189
- readonly message: string;
190
- }
191
199
 
192
200
  /**
193
201
  * The Gemini authoring model type, sourced from the installed SDK's own model union so
@@ -943,11 +951,13 @@ interface LocaleSummary {
943
951
  */
944
952
  readonly integrityMismatches: readonly string[];
945
953
  /**
946
- * Keys withheld because the provider call itself failed (for example a revoked API key, a rate
947
- * limit, or a network timeout), as distinct from a translation that came back and failed the
948
- * placeholder-integrity check. Nothing was translated for these keys. The corresponding
949
- * {@link LocaleSummary.notices} entry carries the secret-free failure code and message. Empty in a
950
- * dry-run and empty unless a provider call threw.
954
+ * Keys withheld because nothing was translated for them this run, as distinct from a translation
955
+ * that came back and failed the placeholder-integrity check. This covers two causes: the provider
956
+ * call itself failed (for example a revoked API key, a rate limit, or a network timeout), in which
957
+ * case the corresponding {@link LocaleSummary.notices} entry carries the secret-free failure code
958
+ * and message; or the call succeeded but the key was still missing or duplicated in the response
959
+ * after the shared LLM layer's bounded reconcile repair round, in which case no notice is added.
960
+ * Empty in a dry-run.
951
961
  */
952
962
  readonly providerFailures: readonly string[];
953
963
  /**
package/dist/index.js CHANGED
@@ -473,16 +473,14 @@ function validateRequest(request) {
473
473
  return parsed.data;
474
474
  }
475
475
  function toIntegrityInputs(entries, values) {
476
- return entries.map((entry) => {
476
+ const inputs = [];
477
+ for (const entry of entries) {
477
478
  const translatedValue = values.get(entry.key);
478
- if (translatedValue === void 0) {
479
- throw new ProviderError(
480
- "INVALID_RESPONSE",
481
- "The provider response is missing one or more keys."
482
- );
479
+ if (translatedValue !== void 0) {
480
+ inputs.push({ key: entry.key, sourceValue: entry.value, translatedValue });
483
481
  }
484
- return { key: entry.key, sourceValue: entry.value, translatedValue };
485
- });
482
+ }
483
+ return inputs;
486
484
  }
487
485
  function toItem(entry) {
488
486
  return {
@@ -514,25 +512,29 @@ function deriveJsonSchema(schema) {
514
512
  }
515
513
  return result;
516
514
  }
517
- function reconcile(translations, requestedKeys) {
518
- const requested = new Set(requestedKeys);
519
- const values = /* @__PURE__ */ new Map();
515
+ function partitionTranslations(translations, requested) {
516
+ const firstSeen = /* @__PURE__ */ new Map();
517
+ const duplicated = /* @__PURE__ */ new Set();
520
518
  for (const { key, value } of translations) {
521
- if (!requested.has(key) || values.has(key)) {
522
- throw new ProviderError(
523
- "INVALID_RESPONSE",
524
- "The provider returned an unexpected or duplicate key."
525
- );
519
+ if (!requested.has(key)) {
520
+ throw new ProviderError("INVALID_RESPONSE", "The provider returned an unexpected key.");
526
521
  }
527
- values.set(key, value);
522
+ if (firstSeen.has(key)) {
523
+ duplicated.add(key);
524
+ continue;
525
+ }
526
+ firstSeen.set(key, value);
528
527
  }
529
- if (values.size !== requested.size) {
530
- throw new ProviderError(
531
- "INVALID_RESPONSE",
532
- "The provider response is missing one or more keys."
533
- );
528
+ return { firstSeen, duplicated };
529
+ }
530
+ function reconcile(translations, requestedKeys) {
531
+ const requested = new Set(requestedKeys);
532
+ const { firstSeen, duplicated } = partitionTranslations(translations, requested);
533
+ const missingKeys = requestedKeys.filter((key) => duplicated.has(key) || !firstSeen.has(key));
534
+ for (const key of duplicated) {
535
+ firstSeen.delete(key);
534
536
  }
535
- return values;
537
+ return { accepted: firstSeen, missingKeys };
536
538
  }
537
539
  function reconcileResult(raw, requestedKeys) {
538
540
  const parsed = translationsResultSchema.safeParse(raw);
@@ -544,22 +546,54 @@ function reconcileResult(raw, requestedKeys) {
544
546
  }
545
547
  return reconcile(parsed.data.translations, requestedKeys);
546
548
  }
549
+ var MAX_REPAIR_ROUNDS = 1;
547
550
  async function runLlmTranslation(request, mechanism) {
548
551
  const data = validateRequest(request);
552
+ const signal = request.signal;
553
+ const first = await requestTranslations(mechanism, data, signal);
554
+ const values = first.outcome.accepted;
555
+ let usage = first.completion.usage;
556
+ let toRepair = entriesFor(data.entries, first.outcome.missingKeys);
557
+ for (let round = 0; round < MAX_REPAIR_ROUNDS && toRepair.length > 0; round += 1) {
558
+ const repair = await requestTranslations(mechanism, { ...data, entries: toRepair }, signal);
559
+ for (const [key, value] of repair.outcome.accepted) {
560
+ values.set(key, value);
561
+ }
562
+ usage = mergeUsage(usage, repair.completion.usage);
563
+ toRepair = entriesFor(data.entries, repair.outcome.missingKeys);
564
+ }
565
+ const integrity = checkBatchIntegrity(
566
+ toIntegrityInputs(data.entries, values),
567
+ request.extractPlaceholders,
568
+ request.comparePlaceholders
569
+ );
570
+ return usage === void 0 ? { values, integrity, notices: [] } : { values, integrity, usage, notices: [] };
571
+ }
572
+ async function requestTranslations(mechanism, data, signal) {
549
573
  const payloadJson = JSON.stringify(buildDataPayload(data));
550
574
  const requestedKeys = data.entries.map((entry) => entry.key);
551
575
  const completion = await mechanism.translate({
552
576
  payloadJson,
553
577
  requestedKeys,
554
- ...request.signal !== void 0 ? { signal: request.signal } : {}
578
+ ...signal !== void 0 ? { signal } : {}
555
579
  });
556
- const values = reconcileResult(completion.raw, requestedKeys);
557
- const integrity = checkBatchIntegrity(
558
- toIntegrityInputs(data.entries, values),
559
- request.extractPlaceholders,
560
- request.comparePlaceholders
561
- );
562
- return completion.usage === void 0 ? { values, integrity } : { values, integrity, usage: completion.usage };
580
+ return { completion, outcome: reconcileResult(completion.raw, requestedKeys) };
581
+ }
582
+ function entriesFor(entries, keys) {
583
+ const wanted = new Set(keys);
584
+ return entries.filter((entry) => wanted.has(entry.key));
585
+ }
586
+ function mergeUsage(first, second) {
587
+ if (first === void 0) {
588
+ return second;
589
+ }
590
+ if (second === void 0) {
591
+ return first;
592
+ }
593
+ return {
594
+ inputTokens: first.inputTokens + second.inputTokens,
595
+ outputTokens: first.outputTokens + second.outputTokens
596
+ };
563
597
  }
564
598
  var OUTPUT_TRUNCATED_MESSAGE = "The provider stopped because the output-token limit was reached. Reduce the batch size or raise the configured max output tokens.";
565
599
  function assertNotTruncated(truncated) {
@@ -807,7 +841,10 @@ function createDeepLProvider(config, deps = {}) {
807
841
  return {
808
842
  id: PROVIDER_ID2,
809
843
  kind: "machine-translation",
810
- supportsGlossary: true,
844
+ // Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
845
+ // generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
846
+ // silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
847
+ supportsGlossary: validConfig.glossaryId !== void 0,
811
848
  translateBatch: (request) => translate(bundle, validConfig, request)
812
849
  };
813
850
  }
@@ -1111,16 +1148,19 @@ function buildResponseFormat(mode) {
1111
1148
  }
1112
1149
  };
1113
1150
  }
1114
- function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
1115
- return {
1116
- model: config.model,
1117
- max_completion_tokens: config.maxOutputTokens,
1151
+ function buildOpenAiRequest(config, payloadJson, mode = "strict-schema", tokenLimitField = "max_completion_tokens") {
1152
+ const base = {
1118
1153
  messages: [
1119
1154
  { role: "system", content: OPENAI_SYSTEM_RULES },
1120
1155
  { role: "user", content: payloadJson }
1121
1156
  ],
1157
+ model: config.model,
1122
1158
  response_format: buildResponseFormat(mode)
1123
1159
  };
1160
+ if (tokenLimitField === "max_tokens") {
1161
+ return { ...base, max_tokens: config.maxOutputTokens };
1162
+ }
1163
+ return { ...base, max_completion_tokens: config.maxOutputTokens };
1124
1164
  }
1125
1165
  function advanceStringScan(char, escaped) {
1126
1166
  if (escaped) {
@@ -1291,7 +1331,7 @@ function createOpenAiCompatibleProvider(config, deps = {}) {
1291
1331
  function createMechanism4(client, config) {
1292
1332
  return {
1293
1333
  translate: async ({ payloadJson, signal }) => {
1294
- const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
1334
+ const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
1295
1335
  const completion = await callClient5(client, body, signal);
1296
1336
  return extractOpenAiResult(completion, true);
1297
1337
  }
@@ -1892,6 +1932,9 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
1892
1932
  throw error;
1893
1933
  }
1894
1934
  }
1935
+ function stripLeadingBom(content) {
1936
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
1937
+ }
1895
1938
  async function readBoundedUtf82(handle, size) {
1896
1939
  const buffer = Buffer.allocUnsafe(size);
1897
1940
  let offset = 0;
@@ -1902,7 +1945,7 @@ async function readBoundedUtf82(handle, size) {
1902
1945
  }
1903
1946
  offset += bytesRead;
1904
1947
  }
1905
- return buffer.toString("utf8", 0, offset);
1948
+ return stripLeadingBom(buffer.toString("utf8", 0, offset));
1906
1949
  }
1907
1950
  async function readBounded2(filePath) {
1908
1951
  const handle = await open(filePath, "r");
@@ -2945,15 +2988,8 @@ function subBatchFailedNotice(count, error) {
2945
2988
  }
2946
2989
 
2947
2990
  // src/flow/notices.ts
2948
- function isNotice(value) {
2949
- return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
2950
- }
2951
2991
  function readNotices(result) {
2952
- const candidate = result.notices;
2953
- if (!Array.isArray(candidate)) {
2954
- return [];
2955
- }
2956
- return candidate.filter(isNotice);
2992
+ return result.notices ?? [];
2957
2993
  }
2958
2994
 
2959
2995
  // src/flow/plural-categories.ts
@@ -3157,20 +3193,26 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
3157
3193
  return [subBatchFailedNotice(batch.length, error)];
3158
3194
  }
3159
3195
  for (const item of batch) {
3160
- const value = result.values.get(item.targetKey);
3161
- const integrity = result.integrity.get(item.targetKey);
3162
- if (value !== void 0 && integrity?.matches === true) {
3163
- accepted.push({
3164
- targetKey: item.targetKey,
3165
- entry: { ...syntheticEntry(item), value },
3166
- lockHash: generatedLockHash(item.governingEntries, item.category)
3167
- });
3168
- } else {
3169
- withheld.push(item.targetKey);
3170
- }
3196
+ foldGenerationItem(item, result, accepted, withheld, providerFailures);
3171
3197
  }
3172
3198
  return readNotices(result);
3173
3199
  }
3200
+ function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
3201
+ const value = result.values.get(item.targetKey);
3202
+ if (value === void 0) {
3203
+ providerFailures.push(item.targetKey);
3204
+ return;
3205
+ }
3206
+ if (result.integrity.get(item.targetKey)?.matches === true) {
3207
+ accepted.push({
3208
+ targetKey: item.targetKey,
3209
+ entry: { ...syntheticEntry(item), value },
3210
+ lockHash: generatedLockHash(item.governingEntries, item.category)
3211
+ });
3212
+ } else {
3213
+ withheld.push(item.targetKey);
3214
+ }
3215
+ }
3174
3216
 
3175
3217
  // src/flow/locale-run.ts
3176
3218
  function emptyResource(locale, format) {
@@ -3358,16 +3400,22 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
3358
3400
  return [subBatchFailedNotice(batch.length, error)];
3359
3401
  }
3360
3402
  for (const entry of batch) {
3361
- const value = result.values.get(entry.key);
3362
- const integrity = result.integrity.get(entry.key);
3363
- if (value !== void 0 && integrity?.matches === true) {
3364
- accepted.set(entry.key, { value, source: entry });
3365
- } else {
3366
- integrityMismatches.push(entry.key);
3367
- }
3403
+ foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
3368
3404
  }
3369
3405
  return readNotices(result);
3370
3406
  }
3407
+ function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
3408
+ const value = result.values.get(entry.key);
3409
+ if (value === void 0) {
3410
+ providerFailures.push(entry.key);
3411
+ return;
3412
+ }
3413
+ if (result.integrity.get(entry.key)?.matches === true) {
3414
+ accepted.set(entry.key, { value, source: entry });
3415
+ } else {
3416
+ integrityMismatches.push(entry.key);
3417
+ }
3418
+ }
3371
3419
  function computeLockEntries(params, merged, withheld, generated) {
3372
3420
  const lockEntries = {};
3373
3421
  const sourceBaseKeys = sourcePluralBaseKeys(params.source);