@mux/ai 0.31.0 → 0.33.0

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.js CHANGED
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // package.json
8
- var version = "0.31.0";
8
+ var version = "0.33.0";
9
9
 
10
10
  // src/lib/language-codes.ts
11
11
  var ISO639_1_TO_3 = {
@@ -250,6 +250,43 @@ var EnvSchema = z.object({
250
250
  MUX_AI_EVAL_MODELS: optionalString("Comma-separated eval model pairs.", "Comma-separated provider:model pairs (e.g. 'openai:gpt-5.6-luna,anthropic:claude-sonnet-4-5,google:gemini-3-flash-preview')"),
251
251
  // AI Providers
252
252
  OPENAI_API_KEY: optionalString("OpenAI API key for OpenAI-backed workflows.", "OpenAI API key"),
253
+ BASETEN_API_KEY: optionalString("Baseten API key for Baseten-backed workflows.", "Baseten API key"),
254
+ BASETEN_URL: optionalString(
255
+ "Optional Baseten language endpoint URL: a dedicated deployment's /sync/v1 URL (such as https://model-{MODEL_ID}.api.baseten.co/environments/production/sync/v1) or a shared OpenAI-compatible base URL. Omit to use Baseten Model APIs.",
256
+ "Baseten URL"
257
+ ),
258
+ BASETEN_EMBEDDING_URL: optionalString(
259
+ "Baseten dedicated embedding deployment URL, such as https://model-{MODEL_ID}.api.baseten.co/environments/production/sync. Required for Baseten embeddings.",
260
+ "Baseten embedding URL"
261
+ ),
262
+ BASETEN_MODEL: optionalString(
263
+ "Default Baseten language model identifier used when provider='baseten' and no explicit model is passed.",
264
+ "Baseten model"
265
+ ),
266
+ BASETEN_EMBEDDING_MODEL: optionalString(
267
+ "Default Baseten embedding model identifier used when provider='baseten' and no explicit model is passed.",
268
+ "Baseten embedding model"
269
+ ),
270
+ OPENAI_COMPATIBLE_API_KEY: optionalString(
271
+ "Optional API key for OpenAI-compatible endpoints. Some local or self-hosted endpoints do not require one.",
272
+ "OpenAI-compatible API key"
273
+ ),
274
+ OPENAI_COMPATIBLE_BASE_URL: optionalString(
275
+ "Base URL of an OpenAI-compatible API, such as https://my-endpoint.example.com/v1.",
276
+ "OpenAI-compatible base URL"
277
+ ),
278
+ OPENAI_COMPATIBLE_EMBEDDING_BASE_URL: optionalString(
279
+ "Optional embedding-specific OpenAI-compatible base URL. Falls back to OPENAI_COMPATIBLE_BASE_URL.",
280
+ "OpenAI-compatible embedding base URL"
281
+ ),
282
+ OPENAI_COMPATIBLE_MODEL: optionalString(
283
+ "Default language model identifier used when provider='openai-compatible' and no explicit model is passed.",
284
+ "OpenAI-compatible model"
285
+ ),
286
+ OPENAI_COMPATIBLE_EMBEDDING_MODEL: optionalString(
287
+ "Default embedding model identifier used when provider='openai-compatible' and no explicit model is passed.",
288
+ "OpenAI-compatible embedding model"
289
+ ),
253
290
  ANTHROPIC_API_KEY: optionalString("Anthropic API key for Claude-backed workflows.", "Anthropic API key"),
254
291
  GOOGLE_GENERATIVE_AI_API_KEY: optionalString("Google Generative AI API key for Gemini-backed workflows.", "Google Generative AI API key"),
255
292
  ELEVENLABS_API_KEY: optionalString("ElevenLabs API key for audio translation.", "ElevenLabs API key"),
@@ -1025,6 +1062,7 @@ async function resolveMuxClient(credentials) {
1025
1062
  function resolveProviderApiKeyFromCredentials(provider, resolved) {
1026
1063
  const record = resolved;
1027
1064
  const openaiApiKey = readString(record, "openaiApiKey");
1065
+ const basetenApiKey = readString(record, "basetenApiKey");
1028
1066
  const anthropicApiKey = readString(record, "anthropicApiKey");
1029
1067
  const googleApiKey = readString(record, "googleApiKey");
1030
1068
  const googleVisionApiKey = readString(record, "googleVisionApiKey");
@@ -1032,6 +1070,7 @@ function resolveProviderApiKeyFromCredentials(provider, resolved) {
1032
1070
  const elevenLabsApiKey = readString(record, "elevenLabsApiKey");
1033
1071
  const apiKeyMap = {
1034
1072
  "openai": openaiApiKey ?? env_default.OPENAI_API_KEY,
1073
+ "baseten": basetenApiKey ?? env_default.BASETEN_API_KEY,
1035
1074
  "anthropic": anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
1036
1075
  "google": googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
1037
1076
  "google-vision-api": googleVisionApiKey ?? env_default.GOOGLE_VISION_API_KEY,
@@ -1042,6 +1081,7 @@ function resolveProviderApiKeyFromCredentials(provider, resolved) {
1042
1081
  if (!apiKey) {
1043
1082
  const envVarNames = {
1044
1083
  "openai": "OPENAI_API_KEY",
1084
+ "baseten": "BASETEN_API_KEY",
1045
1085
  "anthropic": "ANTHROPIC_API_KEY",
1046
1086
  "google": "GOOGLE_GENERATIVE_AI_API_KEY",
1047
1087
  "google-vision-api": "GOOGLE_VISION_API_KEY",
@@ -1410,6 +1450,7 @@ __export(primitives_exports, {
1410
1450
  import { createAnthropic } from "@ai-sdk/anthropic";
1411
1451
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
1412
1452
  import { createOpenAI } from "@ai-sdk/openai";
1453
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1413
1454
  var DEFAULT_LANGUAGE_MODELS = {
1414
1455
  // GPT-5.6 models default to medium reasoning when no effort is specified.
1415
1456
  openai: "gpt-5.6-luna",
@@ -1420,6 +1461,60 @@ var DEFAULT_EMBEDDING_MODELS = {
1420
1461
  openai: "text-embedding-3-small",
1421
1462
  google: "gemini-embedding-001"
1422
1463
  };
1464
+ function resolveBasetenLanguageModelId(model) {
1465
+ const resolved = model ?? env_default.BASETEN_MODEL;
1466
+ if (!resolved) {
1467
+ throw new Error(
1468
+ 'Baseten model is required. Pass `model` when provider is "baseten" or set BASETEN_MODEL.'
1469
+ );
1470
+ }
1471
+ return resolved;
1472
+ }
1473
+ function resolveBasetenEmbeddingModelId(model) {
1474
+ const resolved = model ?? env_default.BASETEN_EMBEDDING_MODEL;
1475
+ if (!resolved) {
1476
+ throw new Error(
1477
+ 'Baseten embedding model is required. Pass `model` when provider is "baseten" or set BASETEN_EMBEDDING_MODEL.'
1478
+ );
1479
+ }
1480
+ return resolved;
1481
+ }
1482
+ function resolveOpenAICompatibleLanguageModelId(model) {
1483
+ const resolved = model ?? env_default.OPENAI_COMPATIBLE_MODEL;
1484
+ if (!resolved) {
1485
+ throw new Error(
1486
+ 'OpenAI-compatible model is required. Pass `model` when provider is "openai-compatible" or set OPENAI_COMPATIBLE_MODEL.'
1487
+ );
1488
+ }
1489
+ return resolved;
1490
+ }
1491
+ function resolveOpenAICompatibleEmbeddingModelId(model) {
1492
+ const resolved = model ?? env_default.OPENAI_COMPATIBLE_EMBEDDING_MODEL;
1493
+ if (!resolved) {
1494
+ throw new Error(
1495
+ 'OpenAI-compatible embedding model is required. Pass `model` when provider is "openai-compatible" or set OPENAI_COMPATIBLE_EMBEDDING_MODEL.'
1496
+ );
1497
+ }
1498
+ return resolved;
1499
+ }
1500
+ function getDefaultLanguageModel(provider) {
1501
+ if (provider === "baseten") {
1502
+ return resolveBasetenLanguageModelId();
1503
+ }
1504
+ if (provider === "openai-compatible") {
1505
+ return resolveOpenAICompatibleLanguageModelId();
1506
+ }
1507
+ return DEFAULT_LANGUAGE_MODELS[provider];
1508
+ }
1509
+ function getDefaultEmbeddingModel(provider) {
1510
+ if (provider === "baseten") {
1511
+ return resolveBasetenEmbeddingModelId();
1512
+ }
1513
+ if (provider === "openai-compatible") {
1514
+ return resolveOpenAICompatibleEmbeddingModelId();
1515
+ }
1516
+ return DEFAULT_EMBEDDING_MODELS[provider];
1517
+ }
1423
1518
  var LANGUAGE_MODELS = {
1424
1519
  openai: ["gpt-5.6-luna", "gpt-5.1", "gpt-5-mini"],
1425
1520
  anthropic: ["claude-sonnet-4-5"],
@@ -1485,7 +1580,7 @@ function getDefaultEvalModelConfigs() {
1485
1580
  function getAllEvalModelConfigs() {
1486
1581
  return Object.entries(LANGUAGE_MODELS).flatMap(([provider, models]) => models.map((modelId) => ({ provider, modelId })));
1487
1582
  }
1488
- function isSupportedProvider(value) {
1583
+ function isSupportedEvalProvider(value) {
1489
1584
  return value === "openai" || value === "anthropic" || value === "google";
1490
1585
  }
1491
1586
  function parseEvalModelPair(value) {
@@ -1498,7 +1593,10 @@ function parseEvalModelPair(value) {
1498
1593
  `Invalid eval model pair "${value}". Use "provider:model" (example: "openai:gpt-5.6-luna").`
1499
1594
  );
1500
1595
  }
1501
- if (!isSupportedProvider(provider)) {
1596
+ if (provider === "baseten" || provider === "openai-compatible") {
1597
+ return void 0;
1598
+ }
1599
+ if (!isSupportedEvalProvider(provider)) {
1502
1600
  throw new Error(
1503
1601
  `Unsupported eval provider "${provider}" in "${value}". Supported providers: ${Object.keys(LANGUAGE_MODELS).join(", ")}.`
1504
1602
  );
@@ -1519,7 +1617,7 @@ function resolveEvalModelConfigs(options = {}) {
1519
1617
  const explicitPairs = options.modelPairs?.map((value) => value.trim()).filter(Boolean) ?? [];
1520
1618
  if (explicitPairs.length > 0) {
1521
1619
  const dedupedPairs = Array.from(new Set(explicitPairs));
1522
- return dedupedPairs.map(parseEvalModelPair);
1620
+ return dedupedPairs.map(parseEvalModelPair).filter((config) => Boolean(config));
1523
1621
  }
1524
1622
  const selection = options.selection ?? "default";
1525
1623
  if (selection === "all") {
@@ -1552,30 +1650,140 @@ function resolveEvalModelConfigsFromEnv(environment = env_default) {
1552
1650
  var EVAL_MODEL_CONFIGS = resolveEvalModelConfigsFromEnv();
1553
1651
  function resolveLanguageModelConfig(options = {}) {
1554
1652
  const provider = options.provider || "openai";
1555
- const modelId = options.model || DEFAULT_LANGUAGE_MODELS[provider];
1653
+ const modelId = options.model || getDefaultLanguageModel(provider);
1556
1654
  maybeWarnOrThrowForDeprecatedLanguageModel(provider, modelId);
1557
1655
  return { provider, modelId };
1558
1656
  }
1559
1657
  function resolveEmbeddingModelConfig(options = {}) {
1560
1658
  const provider = options.provider || "openai";
1561
- const modelId = options.model || DEFAULT_EMBEDDING_MODELS[provider];
1659
+ const modelId = options.model || getDefaultEmbeddingModel(provider);
1562
1660
  return { provider, modelId };
1563
1661
  }
1662
+ var BASETEN_MODEL_APIS_BASE_URL = "https://inference.baseten.co/v1";
1663
+ function readCredentialString(record, key) {
1664
+ const value = record?.[key];
1665
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1666
+ }
1667
+ function normalizeBaseUrl(value) {
1668
+ const trimmed = value?.trim();
1669
+ if (!trimmed) {
1670
+ return void 0;
1671
+ }
1672
+ return trimmed.replace(/\/(chat\/completions|responses|embeddings)\/?$/, "").replace(/\/$/, "");
1673
+ }
1674
+ function isDedicatedBasetenModelUrl(value) {
1675
+ return Boolean(value && (value.includes("/sync") || value.includes("/predict")));
1676
+ }
1677
+ function validateDedicatedBasetenUrl(url, kind) {
1678
+ if (kind === "language") {
1679
+ if (!url.includes("/sync/v1")) {
1680
+ throw new Error(
1681
+ `Baseten dedicated deployment URLs must be /sync/v1 endpoints for language models ("/predict" URLs are not supported). Update BASETEN_URL (or basetenUrl in credentials) to the deployment's /sync/v1 URL.`
1682
+ );
1683
+ }
1684
+ return url;
1685
+ }
1686
+ if (!url.includes("/sync")) {
1687
+ throw new Error(
1688
+ "Baseten embeddings require a dedicated /sync or /sync/v1 deployment URL. Set BASETEN_EMBEDDING_URL or provide basetenEmbeddingUrl in credentials."
1689
+ );
1690
+ }
1691
+ return url;
1692
+ }
1693
+ function basetenEndpointBaseUrl(url, kind) {
1694
+ if (kind === "embedding" && !url.includes("/sync/v1")) {
1695
+ return `${url}/v1`;
1696
+ }
1697
+ return url;
1698
+ }
1699
+ function basetenBaseUrlFromUrl(url, kind) {
1700
+ const normalized = normalizeBaseUrl(url);
1701
+ if (!normalized) {
1702
+ if (kind === "embedding") {
1703
+ throw new Error(
1704
+ "Baseten embedding URL is required. Set BASETEN_EMBEDDING_URL or provide basetenEmbeddingUrl in credentials."
1705
+ );
1706
+ }
1707
+ return BASETEN_MODEL_APIS_BASE_URL;
1708
+ }
1709
+ if (isDedicatedBasetenModelUrl(normalized)) {
1710
+ return basetenEndpointBaseUrl(validateDedicatedBasetenUrl(normalized, kind), kind);
1711
+ }
1712
+ if (kind === "embedding") {
1713
+ throw new Error(
1714
+ "Baseten embeddings require a dedicated /sync or /sync/v1 deployment URL. Set BASETEN_EMBEDDING_URL or provide basetenEmbeddingUrl in credentials."
1715
+ );
1716
+ }
1717
+ return normalized;
1718
+ }
1719
+ function resolveBasetenBaseUrlFromRecord(record, kind) {
1720
+ if (kind === "embedding") {
1721
+ return basetenBaseUrlFromUrl(
1722
+ readCredentialString(record, "basetenEmbeddingUrl") ?? env_default.BASETEN_EMBEDDING_URL,
1723
+ kind
1724
+ );
1725
+ }
1726
+ return basetenBaseUrlFromUrl(
1727
+ readCredentialString(record, "basetenUrl") ?? env_default.BASETEN_URL,
1728
+ kind
1729
+ );
1730
+ }
1731
+ function resolveOpenAICompatibleSettingsFromRecord(record, kind) {
1732
+ const apiKey = readCredentialString(record, "openaiCompatibleApiKey") ?? env_default.OPENAI_COMPATIBLE_API_KEY;
1733
+ const sharedBaseUrl = readCredentialString(record, "openaiCompatibleBaseUrl") ?? env_default.OPENAI_COMPATIBLE_BASE_URL;
1734
+ const baseUrl = normalizeBaseUrl(
1735
+ kind === "embedding" ? readCredentialString(record, "openaiCompatibleEmbeddingBaseUrl") ?? env_default.OPENAI_COMPATIBLE_EMBEDDING_BASE_URL ?? sharedBaseUrl : sharedBaseUrl
1736
+ );
1737
+ if (!baseUrl) {
1738
+ throw new Error(
1739
+ "OpenAI-compatible base URL is required. Set OPENAI_COMPATIBLE_BASE_URL or provide openaiCompatibleBaseUrl in credentials."
1740
+ );
1741
+ }
1742
+ return { apiKey, baseURL: baseUrl };
1743
+ }
1744
+ function createBasetenProvider(apiKey, baseURL) {
1745
+ return createOpenAICompatible({
1746
+ name: "baseten",
1747
+ apiKey,
1748
+ baseURL,
1749
+ supportsStructuredOutputs: true
1750
+ });
1751
+ }
1752
+ function createGenericOpenAICompatibleProvider(settings) {
1753
+ return createOpenAICompatible({
1754
+ name: "openai-compatible",
1755
+ ...settings,
1756
+ supportsStructuredOutputs: true
1757
+ });
1758
+ }
1564
1759
  async function createLanguageModelFromConfig(provider, modelId, credentials) {
1565
1760
  maybeWarnOrThrowForDeprecatedLanguageModel(provider, modelId);
1761
+ const resolvedCredentials = await resolveWorkflowCredentials(credentials);
1762
+ const credentialsRecord = resolvedCredentials;
1566
1763
  switch (provider) {
1567
1764
  case "openai": {
1568
- const apiKey = await resolveProviderApiKey("openai", credentials);
1765
+ const apiKey = resolveProviderApiKeyFromCredentials("openai", resolvedCredentials);
1569
1766
  const openai = createOpenAI({ apiKey });
1570
1767
  return openai(modelId);
1571
1768
  }
1769
+ case "baseten": {
1770
+ const apiKey = resolveProviderApiKeyFromCredentials("baseten", resolvedCredentials);
1771
+ const baseten = createBasetenProvider(apiKey, resolveBasetenBaseUrlFromRecord(credentialsRecord, "language"));
1772
+ return baseten.chatModel(modelId);
1773
+ }
1774
+ case "openai-compatible": {
1775
+ const compatible = createGenericOpenAICompatibleProvider(
1776
+ resolveOpenAICompatibleSettingsFromRecord(credentialsRecord, "language")
1777
+ );
1778
+ return compatible.chatModel(modelId);
1779
+ }
1572
1780
  case "anthropic": {
1573
- const apiKey = await resolveProviderApiKey("anthropic", credentials);
1781
+ const apiKey = resolveProviderApiKeyFromCredentials("anthropic", resolvedCredentials);
1574
1782
  const anthropic = createAnthropic({ apiKey });
1575
1783
  return anthropic(modelId);
1576
1784
  }
1577
1785
  case "google": {
1578
- const apiKey = await resolveProviderApiKey("google", credentials);
1786
+ const apiKey = resolveProviderApiKeyFromCredentials("google", resolvedCredentials);
1579
1787
  const google = createGoogleGenerativeAI({ apiKey });
1580
1788
  return google(modelId);
1581
1789
  }
@@ -1586,14 +1794,27 @@ async function createLanguageModelFromConfig(provider, modelId, credentials) {
1586
1794
  }
1587
1795
  }
1588
1796
  async function createEmbeddingModelFromConfig(provider, modelId, credentials) {
1797
+ const resolvedCredentials = await resolveWorkflowCredentials(credentials);
1798
+ const credentialsRecord = resolvedCredentials;
1589
1799
  switch (provider) {
1590
1800
  case "openai": {
1591
- const apiKey = await resolveProviderApiKey("openai", credentials);
1801
+ const apiKey = resolveProviderApiKeyFromCredentials("openai", resolvedCredentials);
1592
1802
  const openai = createOpenAI({ apiKey });
1593
1803
  return openai.embedding(modelId);
1594
1804
  }
1805
+ case "baseten": {
1806
+ const apiKey = resolveProviderApiKeyFromCredentials("baseten", resolvedCredentials);
1807
+ const baseten = createBasetenProvider(apiKey, resolveBasetenBaseUrlFromRecord(credentialsRecord, "embedding"));
1808
+ return baseten.textEmbeddingModel(modelId);
1809
+ }
1810
+ case "openai-compatible": {
1811
+ const compatible = createGenericOpenAICompatibleProvider(
1812
+ resolveOpenAICompatibleSettingsFromRecord(credentialsRecord, "embedding")
1813
+ );
1814
+ return compatible.textEmbeddingModel(modelId);
1815
+ }
1595
1816
  case "google": {
1596
- const apiKey = await resolveProviderApiKey("google", credentials);
1817
+ const apiKey = resolveProviderApiKeyFromCredentials("google", resolvedCredentials);
1597
1818
  const google = createGoogleGenerativeAI({ apiKey });
1598
1819
  return google.textEmbeddingModel(modelId);
1599
1820
  }
@@ -2266,6 +2487,7 @@ async function getThumbnailUrls(playbackId, duration, options = {}) {
2266
2487
  timestamps.push(time);
2267
2488
  }
2268
2489
  }
2490
+ timestamps = [...new Set(timestamps)];
2269
2491
  if (maxSamples !== void 0 && timestamps.length > maxSamples) {
2270
2492
  const newTimestamps = [];
2271
2493
  newTimestamps.push(resolvedScope.startTime);
@@ -2822,7 +3044,9 @@ __export(workflows_exports, {
2822
3044
  transformCueText: () => transformCueText,
2823
3045
  translateAudio: () => translateAudio,
2824
3046
  translateCaptions: () => translateCaptions,
2825
- translationSchema: () => translationSchema
3047
+ translationSchema: () => translationSchema,
3048
+ validateNeverTranslateTerms: () => validateNeverTranslateTerms,
3049
+ verifyNeverTranslateTerms: () => verifyNeverTranslateTerms
2826
3050
  });
2827
3051
 
2828
3052
  // src/workflows/ask-questions.ts
@@ -2831,8 +3055,71 @@ import dedent2 from "dedent";
2831
3055
  import { z as z3 } from "zod";
2832
3056
 
2833
3057
  // src/lib/content-policy-error.ts
2834
- import { APICallError, NoObjectGeneratedError, RetryError } from "ai";
3058
+ import { APICallError as APICallError2, NoObjectGeneratedError, RetryError } from "ai";
2835
3059
  import { z as z2 } from "zod";
3060
+
3061
+ // src/lib/retry.ts
3062
+ import { APICallError, DownloadError, NoOutputGeneratedError } from "ai";
3063
+ var DEFAULT_RETRY_OPTIONS = {
3064
+ maxRetries: 3,
3065
+ baseDelay: 2e3,
3066
+ maxDelay: 1e4
3067
+ };
3068
+ function defaultShouldRetry(error, _attempt) {
3069
+ if (error.message.includes("Timeout while downloading")) {
3070
+ return true;
3071
+ }
3072
+ if (NoOutputGeneratedError.isInstance(error) || error.name === "AI_NoOutputGeneratedError" || error.name === "AI_NoObjectGeneratedError") {
3073
+ return true;
3074
+ }
3075
+ if (APICallError.isInstance(error) || error.name === "AI_APICallError") {
3076
+ const apiError = error;
3077
+ if (typeof apiError.isRetryable === "boolean") {
3078
+ return apiError.isRetryable;
3079
+ }
3080
+ return apiError.statusCode === 408 || apiError.statusCode === 409 || apiError.statusCode === 429 || apiError.statusCode !== void 0 && apiError.statusCode >= 500;
3081
+ }
3082
+ const isDownloadError = DownloadError.isInstance(error);
3083
+ const isSerializedDownloadError = error.name === "AI_DownloadError";
3084
+ if (!isDownloadError && !isSerializedDownloadError) {
3085
+ return false;
3086
+ }
3087
+ const statusCode = error.statusCode;
3088
+ return statusCode === void 0 || statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
3089
+ }
3090
+ function calculateDelay(attempt, baseDelay, maxDelay) {
3091
+ const exponentialDelay = baseDelay * 2 ** (attempt - 1);
3092
+ const delayWithJitter = exponentialDelay * (0.5 + Math.random() * 0.5);
3093
+ return Math.min(delayWithJitter, maxDelay);
3094
+ }
3095
+ async function withRetry(fn, {
3096
+ maxRetries = DEFAULT_RETRY_OPTIONS.maxRetries,
3097
+ baseDelay = DEFAULT_RETRY_OPTIONS.baseDelay,
3098
+ maxDelay = DEFAULT_RETRY_OPTIONS.maxDelay,
3099
+ shouldRetry = defaultShouldRetry
3100
+ } = {}) {
3101
+ let lastError;
3102
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
3103
+ try {
3104
+ return await fn();
3105
+ } catch (error) {
3106
+ lastError = error;
3107
+ const retryError = error instanceof Error ? error : new Error(String(error));
3108
+ const isLastAttempt = attempt === maxRetries;
3109
+ if (isLastAttempt || !shouldRetry(retryError, attempt + 1)) {
3110
+ throw error;
3111
+ }
3112
+ const delay = calculateDelay(attempt + 1, baseDelay, maxDelay);
3113
+ console.warn(
3114
+ `Attempt ${attempt + 1} failed: ${retryError.message}. Retrying in ${Math.round(delay)}ms...`
3115
+ );
3116
+ await new Promise((resolve) => setTimeout(resolve, delay));
3117
+ }
3118
+ }
3119
+ throw lastError ?? new Error("Retry failed with unknown error");
3120
+ }
3121
+
3122
+ // src/lib/content-policy-error.ts
2836
3123
  var PolicyTokenSchema = z2.string().trim().min(1).max(64).regex(/^[A-Z0-9_]+$/);
2837
3124
  var SafetyRatingSchema = z2.object({
2838
3125
  category: PolicyTokenSchema.nullish(),
@@ -2877,6 +3164,12 @@ async function withContentPolicyErrorHandling(operation) {
2877
3164
  rethrowContentPolicyError(error);
2878
3165
  }
2879
3166
  }
3167
+ async function withContentPolicyAwareRetry(operation, retryOptions) {
3168
+ return withRetry(
3169
+ () => withContentPolicyErrorHandling(operation),
3170
+ retryOptions
3171
+ );
3172
+ }
2880
3173
  function rethrowContentPolicyError(error) {
2881
3174
  const block = extractContentPolicyBlock(error);
2882
3175
  if (!block) {
@@ -2913,7 +3206,7 @@ function extractContentPolicyBlock(error) {
2913
3206
  if (NoObjectGeneratedError.isInstance(error) && error.finishReason === "content-filter") {
2914
3207
  return { reason: "CONTENT_FILTER" };
2915
3208
  }
2916
- if (!APICallError.isInstance(error) || !error.responseBody) {
3209
+ if (!APICallError2.isInstance(error) || !error.responseBody) {
2917
3210
  return void 0;
2918
3211
  }
2919
3212
  try {
@@ -2952,7 +3245,7 @@ function getContentPolicyTokenUsage(error) {
2952
3245
  if (RetryError.isInstance(error)) {
2953
3246
  return getContentPolicyTokenUsage(error.lastError);
2954
3247
  }
2955
- if (!APICallError.isInstance(error) || !error.responseBody) {
3248
+ if (!APICallError2.isInstance(error) || !error.responseBody) {
2956
3249
  return void 0;
2957
3250
  }
2958
3251
  try {
@@ -3166,57 +3459,6 @@ function createLanguageSection(languageName) {
3166
3459
  };
3167
3460
  }
3168
3461
 
3169
- // src/lib/retry.ts
3170
- import { DownloadError } from "ai";
3171
- var DEFAULT_RETRY_OPTIONS = {
3172
- maxRetries: 3,
3173
- baseDelay: 2e3,
3174
- maxDelay: 1e4
3175
- };
3176
- function defaultShouldRetry(error, _attempt) {
3177
- if (error.message.includes("Timeout while downloading")) {
3178
- return true;
3179
- }
3180
- const isDownloadError = DownloadError.isInstance(error);
3181
- const isSerializedDownloadError = error.name === "AI_DownloadError";
3182
- if (!isDownloadError && !isSerializedDownloadError) {
3183
- return false;
3184
- }
3185
- const statusCode = error.statusCode;
3186
- return statusCode === void 0 || statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
3187
- }
3188
- function calculateDelay(attempt, baseDelay, maxDelay) {
3189
- const exponentialDelay = baseDelay * 2 ** (attempt - 1);
3190
- const delayWithJitter = exponentialDelay * (0.5 + Math.random() * 0.5);
3191
- return Math.min(delayWithJitter, maxDelay);
3192
- }
3193
- async function withRetry(fn, {
3194
- maxRetries = DEFAULT_RETRY_OPTIONS.maxRetries,
3195
- baseDelay = DEFAULT_RETRY_OPTIONS.baseDelay,
3196
- maxDelay = DEFAULT_RETRY_OPTIONS.maxDelay,
3197
- shouldRetry = defaultShouldRetry
3198
- } = {}) {
3199
- let lastError;
3200
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
3201
- try {
3202
- return await fn();
3203
- } catch (error) {
3204
- lastError = error;
3205
- const retryError = error instanceof Error ? error : new Error(String(error));
3206
- const isLastAttempt = attempt === maxRetries;
3207
- if (isLastAttempt || !shouldRetry(retryError, attempt + 1)) {
3208
- throw error;
3209
- }
3210
- const delay = calculateDelay(attempt + 1, baseDelay, maxDelay);
3211
- console.warn(
3212
- `Attempt ${attempt + 1} failed: ${retryError.message}. Retrying in ${Math.round(delay)}ms...`
3213
- );
3214
- await new Promise((resolve) => setTimeout(resolve, delay));
3215
- }
3216
- }
3217
- throw lastError ?? new Error("Retry failed with unknown error");
3218
- }
3219
-
3220
3462
  // src/workflows/ask-questions.ts
3221
3463
  var ASK_QUESTIONS_MAX_QUESTION_LENGTH = 600;
3222
3464
  var ASK_QUESTIONS_DEFAULT_MAX_ANSWER_OPTION_LENGTH = 150;
@@ -3338,11 +3580,9 @@ var SYSTEM_PROMPT = promptDedent`
3338
3580
  </constraints>
3339
3581
 
3340
3582
  <language_guidelines>
3341
- When explaining reasoning:
3342
- - Describe content directly, not the medium
3343
- - BAD: "The video shows a person running"
3344
- - GOOD: "A person runs through a park"
3345
- - Be specific and evidence-based
3583
+ When explaining reasoning, be specific and evidence-based.
3584
+
3585
+ ${createLanguageGuidelines("video")}
3346
3586
  </language_guidelines>`;
3347
3587
  var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
3348
3588
  <role>
@@ -3435,11 +3675,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
3435
3675
  </constraints>
3436
3676
 
3437
3677
  <language_guidelines>
3438
- When explaining reasoning:
3439
- - Describe content directly, not the medium
3440
- - BAD: "The audio says someone is running"
3441
- - GOOD: "The speaker describes running through a park"
3442
- - Be specific and evidence-based
3678
+ When explaining reasoning, be specific and evidence-based.
3679
+
3680
+ ${createLanguageGuidelines("audio")}
3443
3681
  </language_guidelines>`;
3444
3682
  function freeFormAddendum(maxFreeFormAnswerLength) {
3445
3683
  return promptDedent`
@@ -3604,8 +3842,9 @@ async function analyzeQuestions({
3604
3842
  normalizedQuestions,
3605
3843
  maxFreeFormAnswerLength
3606
3844
  );
3607
- const response = await withContentPolicyErrorHandling(() => generateText({
3845
+ const response = await withContentPolicyAwareRetry(() => generateText({
3608
3846
  model,
3847
+ maxRetries: 0,
3609
3848
  output: Output.object({ schema: responseSchema }),
3610
3849
  experimental_telemetry: { isEnabled: true },
3611
3850
  messages: [
@@ -3804,18 +4043,16 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
3804
4043
  credentials
3805
4044
  });
3806
4045
  } else {
3807
- analysisResponse = await withRetry(
3808
- () => analyzeQuestions({
3809
- provider: modelConfig.provider,
3810
- modelId: modelConfig.modelId,
3811
- userPrompt,
3812
- systemPrompt,
3813
- normalizedQuestions,
3814
- maxFreeFormAnswerLength,
3815
- imageDataUrl: storyboardUrl,
3816
- credentials
3817
- })
3818
- );
4046
+ analysisResponse = await analyzeQuestions({
4047
+ provider: modelConfig.provider,
4048
+ modelId: modelConfig.modelId,
4049
+ userPrompt,
4050
+ systemPrompt,
4051
+ normalizedQuestions,
4052
+ maxFreeFormAnswerLength,
4053
+ imageDataUrl: storyboardUrl,
4054
+ credentials
4055
+ });
3819
4056
  }
3820
4057
  }
3821
4058
  } catch (error) {
@@ -4017,8 +4254,9 @@ async function analyzeStoryboard({
4017
4254
  }) {
4018
4255
  "use step";
4019
4256
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
4020
- const response = await withContentPolicyErrorHandling(() => generateText2({
4257
+ const response = await withContentPolicyAwareRetry(() => generateText2({
4021
4258
  model,
4259
+ maxRetries: 0,
4022
4260
  output: Output2.object({ schema: burnedInCaptionsSchema }),
4023
4261
  experimental_telemetry: { isEnabled: true },
4024
4262
  messages: [
@@ -4108,14 +4346,14 @@ async function hasBurnedInCaptionsInternal(assetId, options, collectedUsage) {
4108
4346
  credentials
4109
4347
  });
4110
4348
  } else {
4111
- analysisResponse = await withRetry(() => analyzeStoryboard({
4349
+ analysisResponse = await analyzeStoryboard({
4112
4350
  imageDataUrl: imageUrl,
4113
4351
  provider: modelConfig.provider,
4114
4352
  modelId: modelConfig.modelId,
4115
4353
  userPrompt,
4116
4354
  systemPrompt: SYSTEM_PROMPT2,
4117
4355
  credentials
4118
- }));
4356
+ });
4119
4357
  }
4120
4358
  collectedUsage.push(analysisResponse.usage);
4121
4359
  if (!analysisResponse.result) {
@@ -4166,24 +4404,21 @@ async function generateChaptersWithAI({
4166
4404
  }) {
4167
4405
  "use step";
4168
4406
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
4169
- const response = await withRetry(
4170
- () => withContentPolicyErrorHandling(
4171
- () => generateText3({
4172
- model,
4173
- output: Output3.object({ schema: chaptersSchema }),
4174
- messages: [
4175
- {
4176
- role: "system",
4177
- content: systemPrompt
4178
- },
4179
- {
4180
- role: "user",
4181
- content: userPrompt
4182
- }
4183
- ]
4184
- })
4185
- )
4186
- );
4407
+ const response = await withContentPolicyAwareRetry(() => generateText3({
4408
+ model,
4409
+ maxRetries: 0,
4410
+ output: Output3.object({ schema: chaptersSchema }),
4411
+ messages: [
4412
+ {
4413
+ role: "system",
4414
+ content: systemPrompt
4415
+ },
4416
+ {
4417
+ role: "user",
4418
+ content: userPrompt
4419
+ }
4420
+ ]
4421
+ }));
4187
4422
  const output = getGeneratedOutputWithContentPolicyHandling(response);
4188
4423
  const unexpectedRootKeys = detectUnexpectedKeysFromRawText(
4189
4424
  response.text,
@@ -4712,8 +4947,9 @@ async function identifyProfanityWithAI({
4712
4947
  tag: "transcript",
4713
4948
  content: plainText
4714
4949
  });
4715
- const response = await withContentPolicyErrorHandling(() => generateText4({
4950
+ const response = await withContentPolicyAwareRetry(() => generateText4({
4716
4951
  model,
4952
+ maxRetries: 0,
4717
4953
  output: Output4.object({ schema: profanityDetectionSchema }),
4718
4954
  messages: [
4719
4955
  {
@@ -4998,9 +5234,10 @@ async function generateSingleChunkEmbedding({
4998
5234
  }) {
4999
5235
  "use step";
5000
5236
  const model = await createEmbeddingModelFromConfig(provider, modelId, credentials);
5001
- const response = await withRetry(
5237
+ const response = await withContentPolicyAwareRetry(
5002
5238
  () => embed({
5003
5239
  model,
5240
+ maxRetries: 0,
5004
5241
  value: chunk.text
5005
5242
  })
5006
5243
  );
@@ -5419,28 +5656,25 @@ async function fetchEngagementData(assetId, options) {
5419
5656
  async function generateInsightsWithAI(provider, modelId, systemPrompt, userPrompt, imageUrls, credentials) {
5420
5657
  "use step";
5421
5658
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
5422
- const response = await withRetry(
5423
- () => withContentPolicyErrorHandling(
5424
- () => generateText5({
5425
- model,
5426
- output: Output5.object({ schema: engagementInsightsSchema }),
5427
- experimental_telemetry: { isEnabled: true },
5428
- messages: [
5429
- {
5430
- role: "system",
5431
- content: systemPrompt
5432
- },
5433
- {
5434
- role: "user",
5435
- content: [
5436
- { type: "text", text: userPrompt },
5437
- ...imageUrls.map((url) => ({ type: "image", image: url }))
5438
- ]
5439
- }
5659
+ const response = await withContentPolicyAwareRetry(() => generateText5({
5660
+ model,
5661
+ maxRetries: 0,
5662
+ output: Output5.object({ schema: engagementInsightsSchema }),
5663
+ experimental_telemetry: { isEnabled: true },
5664
+ messages: [
5665
+ {
5666
+ role: "system",
5667
+ content: systemPrompt
5668
+ },
5669
+ {
5670
+ role: "user",
5671
+ content: [
5672
+ { type: "text", text: userPrompt },
5673
+ ...imageUrls.map((url) => ({ type: "image", image: url }))
5440
5674
  ]
5441
- })
5442
- )
5443
- );
5675
+ }
5676
+ ]
5677
+ }));
5444
5678
  const output = getGeneratedOutputWithContentPolicyHandling(response);
5445
5679
  if (!output) {
5446
5680
  throw new Error("AI returned empty or unparseable response");
@@ -6198,8 +6432,8 @@ async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options)
6198
6432
  "use step";
6199
6433
  const { width, shouldSign, credentials } = options;
6200
6434
  const baseUrl = getMuxThumbnailBaseUrl(playbackId);
6201
- const urlPromises = timestampsMs.map(async (tsMs) => {
6202
- const time = toThumbnailTimeSeconds(tsMs);
6435
+ const times = [...new Set(timestampsMs.map(toThumbnailTimeSeconds))];
6436
+ const urlPromises = times.map(async (time) => {
6203
6437
  const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
6204
6438
  return { url, time };
6205
6439
  });
@@ -6685,8 +6919,9 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
6685
6919
  "use step";
6686
6920
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6687
6921
  const schema = buildSummarySchema(descriptionLength);
6688
- const response = await withContentPolicyErrorHandling(() => generateText6({
6922
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6689
6923
  model,
6924
+ maxRetries: 0,
6690
6925
  output: Output6.object({
6691
6926
  name: "summary_metadata",
6692
6927
  description: "Structured summary with title, description, and keywords.",
@@ -6732,8 +6967,9 @@ async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, des
6732
6967
  "use step";
6733
6968
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6734
6969
  const schema = buildSummarySchema(descriptionLength);
6735
- const response = await withContentPolicyErrorHandling(() => generateText6({
6970
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6736
6971
  model,
6972
+ maxRetries: 0,
6737
6973
  output: Output6.object({
6738
6974
  name: "summary_metadata",
6739
6975
  description: "Structured summary with title, description, and keywords.",
@@ -6916,7 +7152,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6916
7152
  workflowCredentials
6917
7153
  );
6918
7154
  } else {
6919
- analysisResponse = await withRetry(() => analyzeStoryboard2(
7155
+ analysisResponse = await analyzeStoryboard2(
6920
7156
  storyboardUrl,
6921
7157
  modelConfig.provider,
6922
7158
  modelConfig.modelId,
@@ -6924,7 +7160,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6924
7160
  systemPrompt,
6925
7161
  effectiveDescriptionLength,
6926
7162
  workflowCredentials
6927
- ));
7163
+ );
6928
7164
  }
6929
7165
  }
6930
7166
  } catch (error) {
@@ -7430,7 +7666,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7430
7666
 
7431
7667
  // src/workflows/translate-captions.ts
7432
7668
  import {
7433
- APICallError as APICallError2,
7669
+ APICallError as APICallError3,
7434
7670
  generateText as generateText7,
7435
7671
  NoObjectGeneratedError as NoObjectGeneratedError2,
7436
7672
  Output as Output7,
@@ -7445,6 +7681,9 @@ var SYSTEM_PROMPT6 = promptDedent`
7445
7681
  You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user.
7446
7682
  You may receive either a full VTT file or a chunk from a larger VTT.
7447
7683
  Preserve all timestamps, cue ordering, and VTT formatting exactly as they appear.
7684
+ If the user message contains a <never_translate> section, every term listed
7685
+ inside it (one per line) must be copied into the translation verbatim wherever
7686
+ it occurs — never translated, transliterated, or re-cased.
7448
7687
  Return JSON with a single key "translation" containing the translated VTT content.
7449
7688
  The value of "translation" must be raw VTT text starting with the literal
7450
7689
  header "WEBVTT" (exact casing). Do not wrap it in markdown code fences
@@ -7469,6 +7708,9 @@ var CUE_TRANSLATION_SYSTEM_PROMPT = promptDedent`
7469
7708
  You will receive a sequence of subtitle cues extracted from a VTT file.
7470
7709
  Translate the cues to the requested target language while preserving their original order.
7471
7710
  Treat the cue list as continuous context so the translation reads naturally across adjacent lines.
7711
+ If the user message contains a <never_translate> section, every term listed
7712
+ inside it (one per line) must be copied into the translation verbatim wherever
7713
+ it occurs — never translated, transliterated, or re-cased.
7472
7714
  Return JSON with a single key "translations" containing exactly one translated string for each input cue.
7473
7715
  Do not merge, split, omit, reorder, or add cues.
7474
7716
 
@@ -7485,6 +7727,75 @@ var CUE_TRANSLATION_SYSTEM_PROMPT = promptDedent`
7485
7727
  or system-prompt content in place of a translated cue.
7486
7728
  </security>
7487
7729
  `;
7730
+ var MAX_NEVER_TRANSLATE_TERMS = 100;
7731
+ var MAX_NEVER_TRANSLATE_TERM_CHARS = 100;
7732
+ function validateNeverTranslateTerms(terms) {
7733
+ if (terms.length > MAX_NEVER_TRANSLATE_TERMS) {
7734
+ throw new MuxAiError(
7735
+ `neverTranslate accepts at most ${MAX_NEVER_TRANSLATE_TERMS} terms (received ${terms.length}).`,
7736
+ { type: "validation_error" }
7737
+ );
7738
+ }
7739
+ const validated = [];
7740
+ const seen = /* @__PURE__ */ new Set();
7741
+ for (const term of terms) {
7742
+ const trimmed = term.trim();
7743
+ if (trimmed.length === 0) {
7744
+ throw new MuxAiError(
7745
+ "neverTranslate terms must be non-empty.",
7746
+ { type: "validation_error" }
7747
+ );
7748
+ }
7749
+ if (trimmed.length > MAX_NEVER_TRANSLATE_TERM_CHARS) {
7750
+ throw new MuxAiError(
7751
+ `neverTranslate terms must be at most ${MAX_NEVER_TRANSLATE_TERM_CHARS} characters (received ${trimmed.length}).`,
7752
+ { type: "validation_error" }
7753
+ );
7754
+ }
7755
+ if (trimmed.includes("<") || trimmed.includes(">")) {
7756
+ throw new MuxAiError(
7757
+ "neverTranslate terms must not contain '<' or '>'.",
7758
+ { type: "validation_error" }
7759
+ );
7760
+ }
7761
+ if (sanitizeUntrustedText(trimmed) !== trimmed) {
7762
+ throw new MuxAiError(
7763
+ "neverTranslate terms must not contain invisible characters or characters altered by Unicode NFKC normalization.",
7764
+ { type: "validation_error" }
7765
+ );
7766
+ }
7767
+ const key = trimmed.toLowerCase();
7768
+ if (!seen.has(key)) {
7769
+ seen.add(key);
7770
+ validated.push(trimmed);
7771
+ }
7772
+ }
7773
+ return validated;
7774
+ }
7775
+ function buildNeverTranslateSection(terms) {
7776
+ if (!terms || terms.length === 0) {
7777
+ return "";
7778
+ }
7779
+ return `
7780
+
7781
+ <never_translate>
7782
+ ${terms.join("\n")}
7783
+ </never_translate>`;
7784
+ }
7785
+ function countTermOccurrences(text, term) {
7786
+ if (term.length === 0) {
7787
+ return 0;
7788
+ }
7789
+ return text.split(term).length - 1;
7790
+ }
7791
+ function verifyNeverTranslateTerms(terms, sourceVtt, translatedVtt) {
7792
+ const cueText = (vtt) => parseVTTCues(vtt).map((cue) => cue.text).join("\n");
7793
+ const sourceText = cueText(sourceVtt).toLowerCase();
7794
+ const translatedText = cueText(translatedVtt);
7795
+ return terms.every(
7796
+ (term) => countTermOccurrences(translatedText, term) >= countTermOccurrences(sourceText, term.toLowerCase())
7797
+ );
7798
+ }
7488
7799
  var DEFAULT_TRANSLATION_CHUNKING = {
7489
7800
  enabled: true,
7490
7801
  minimumAssetDurationSeconds: 30 * 60,
@@ -7509,7 +7820,7 @@ function isProviderServiceError(error) {
7509
7820
  if (RetryError2.isInstance(error)) {
7510
7821
  return isProviderServiceError(error.lastError);
7511
7822
  }
7512
- if (APICallError2.isInstance(error)) {
7823
+ if (APICallError3.isInstance(error)) {
7513
7824
  return true;
7514
7825
  }
7515
7826
  if (error instanceof Error && "cause" in error) {
@@ -7677,13 +7988,15 @@ async function translateVttWithAI({
7677
7988
  toLanguageCode,
7678
7989
  provider,
7679
7990
  modelId,
7680
- credentials
7991
+ credentials,
7992
+ neverTranslate
7681
7993
  }) {
7682
7994
  "use step";
7683
7995
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
7684
7996
  const sanitisedVttContent = stripVttMetadataBlocks(vttContent);
7685
- const response = await withContentPolicyErrorHandling(() => generateText7({
7997
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7686
7998
  model,
7999
+ maxRetries: 0,
7687
8000
  output: Output7.object({ schema: translationSchema }),
7688
8001
  messages: [
7689
8002
  {
@@ -7692,7 +8005,7 @@ async function translateVttWithAI({
7692
8005
  },
7693
8006
  {
7694
8007
  role: "user",
7695
- content: `Translate from ${fromLanguageCode} to ${toLanguageCode}:
8008
+ content: `Translate from ${fromLanguageCode} to ${toLanguageCode}:${buildNeverTranslateSection(neverTranslate)}
7696
8009
 
7697
8010
  ${sanitisedVttContent}`
7698
8011
  }
@@ -7735,7 +8048,8 @@ async function translateCueChunkWithAI({
7735
8048
  toLanguageCode,
7736
8049
  provider,
7737
8050
  modelId,
7738
- credentials
8051
+ credentials,
8052
+ neverTranslate
7739
8053
  }) {
7740
8054
  "use step";
7741
8055
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
@@ -7748,8 +8062,9 @@ async function translateCueChunkWithAI({
7748
8062
  endTime: cue.endTime,
7749
8063
  text: cue.text
7750
8064
  }));
7751
- const response = await withContentPolicyErrorHandling(() => generateText7({
8065
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7752
8066
  model,
8067
+ maxRetries: 0,
7753
8068
  output: Output7.object({ schema }),
7754
8069
  messages: [
7755
8070
  {
@@ -7759,7 +8074,7 @@ async function translateCueChunkWithAI({
7759
8074
  {
7760
8075
  role: "user",
7761
8076
  content: `Translate from ${fromLanguageCode} to ${toLanguageCode}.
7762
- Return exactly ${cues.length} translated cues in the same order as the input.
8077
+ Return exactly ${cues.length} translated cues in the same order as the input.${buildNeverTranslateSection(neverTranslate)}
7763
8078
 
7764
8079
  ${JSON.stringify(cuePayload, null, 2)}`
7765
8080
  }
@@ -7812,7 +8127,8 @@ async function translateChunkWithFallback({
7812
8127
  toLanguageCode,
7813
8128
  provider,
7814
8129
  modelId,
7815
- credentials
8130
+ credentials,
8131
+ neverTranslate
7816
8132
  }) {
7817
8133
  "use step";
7818
8134
  try {
@@ -7822,7 +8138,8 @@ async function translateChunkWithFallback({
7822
8138
  toLanguageCode,
7823
8139
  provider,
7824
8140
  modelId,
7825
- credentials
8141
+ credentials,
8142
+ neverTranslate
7826
8143
  });
7827
8144
  if (result.translations.length !== chunk.cueCount) {
7828
8145
  throw new TranslationChunkValidationError(
@@ -7858,7 +8175,8 @@ async function translateChunkWithFallback({
7858
8175
  toLanguageCode,
7859
8176
  provider,
7860
8177
  modelId,
7861
- credentials
8178
+ credentials,
8179
+ neverTranslate
7862
8180
  }),
7863
8181
  translateChunkWithFallback({
7864
8182
  chunk: rightChunk,
@@ -7866,7 +8184,8 @@ async function translateChunkWithFallback({
7866
8184
  toLanguageCode,
7867
8185
  provider,
7868
8186
  modelId,
7869
- credentials
8187
+ credentials,
8188
+ neverTranslate
7870
8189
  })
7871
8190
  ]),
7872
8191
  failedAttemptUsage ? [failedAttemptUsage] : []
@@ -7890,7 +8209,8 @@ async function translateCaptionTrack({
7890
8209
  provider,
7891
8210
  modelId,
7892
8211
  credentials,
7893
- chunking
8212
+ chunking,
8213
+ neverTranslate
7894
8214
  }) {
7895
8215
  "use step";
7896
8216
  const chunkPlan = buildTranslationChunkRequests(vttContent, assetDurationSeconds, chunking);
@@ -7901,7 +8221,8 @@ async function translateCaptionTrack({
7901
8221
  toLanguageCode,
7902
8222
  provider,
7903
8223
  modelId,
7904
- credentials
8224
+ credentials,
8225
+ neverTranslate
7905
8226
  });
7906
8227
  }
7907
8228
  const resolvedChunking = resolveTranslationChunkingOptions(chunking);
@@ -7920,7 +8241,8 @@ async function translateCaptionTrack({
7920
8241
  toLanguageCode,
7921
8242
  provider,
7922
8243
  modelId,
7923
- credentials
8244
+ credentials,
8245
+ neverTranslate
7924
8246
  })
7925
8247
  )
7926
8248
  ),
@@ -7995,10 +8317,12 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
7995
8317
  uploadToMux: uploadToMuxOption,
7996
8318
  storageAdapter,
7997
8319
  credentials: providedCredentials,
7998
- chunking
8320
+ chunking,
8321
+ neverTranslate: neverTranslateOption
7999
8322
  } = options;
8000
8323
  const credentials = providedCredentials;
8001
8324
  const effectiveStorageAdapter = storageAdapter;
8325
+ const neverTranslateTerms = neverTranslateOption ? validateNeverTranslateTerms(neverTranslateOption) : [];
8002
8326
  const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
8003
8327
  const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
8004
8328
  const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
@@ -8059,7 +8383,8 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8059
8383
  provider: modelConfig.provider,
8060
8384
  modelId: modelConfig.modelId,
8061
8385
  credentials,
8062
- chunking
8386
+ chunking,
8387
+ neverTranslate: neverTranslateTerms.length > 0 ? neverTranslateTerms : void 0
8063
8388
  });
8064
8389
  translatedVtt = result.translatedVtt;
8065
8390
  usage = result.usage;
@@ -8088,6 +8413,13 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8088
8413
  leaksDetected: scrubbedFields.length > 0,
8089
8414
  scrubbedFields
8090
8415
  };
8416
+ let neverTranslateTermsPreserved;
8417
+ if (neverTranslateTerms.length > 0) {
8418
+ neverTranslateTermsPreserved = verifyNeverTranslateTerms(neverTranslateTerms, vttContent, translatedVtt);
8419
+ if (!neverTranslateTermsPreserved) {
8420
+ console.warn("[@mux/ai] One or more neverTranslate terms were not preserved verbatim in the translated output.");
8421
+ }
8422
+ }
8091
8423
  const usageWithMetadata = usage ? {
8092
8424
  ...usage,
8093
8425
  metadata: {
@@ -8142,7 +8474,8 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8142
8474
  uploadedTrackId,
8143
8475
  presignedUrl,
8144
8476
  usage: usageWithMetadata,
8145
- safety
8477
+ safety,
8478
+ neverTranslateTermsPreserved
8146
8479
  };
8147
8480
  }
8148
8481
  export {