@mux/ai 0.30.0 → 0.32.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.30.0";
8
+ var version = "0.32.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);
@@ -2831,8 +3053,71 @@ import dedent2 from "dedent";
2831
3053
  import { z as z3 } from "zod";
2832
3054
 
2833
3055
  // src/lib/content-policy-error.ts
2834
- import { APICallError, NoObjectGeneratedError, RetryError } from "ai";
3056
+ import { APICallError as APICallError2, NoObjectGeneratedError, RetryError } from "ai";
2835
3057
  import { z as z2 } from "zod";
3058
+
3059
+ // src/lib/retry.ts
3060
+ import { APICallError, DownloadError, NoOutputGeneratedError } from "ai";
3061
+ var DEFAULT_RETRY_OPTIONS = {
3062
+ maxRetries: 3,
3063
+ baseDelay: 2e3,
3064
+ maxDelay: 1e4
3065
+ };
3066
+ function defaultShouldRetry(error, _attempt) {
3067
+ if (error.message.includes("Timeout while downloading")) {
3068
+ return true;
3069
+ }
3070
+ if (NoOutputGeneratedError.isInstance(error) || error.name === "AI_NoOutputGeneratedError" || error.name === "AI_NoObjectGeneratedError") {
3071
+ return true;
3072
+ }
3073
+ if (APICallError.isInstance(error) || error.name === "AI_APICallError") {
3074
+ const apiError = error;
3075
+ if (typeof apiError.isRetryable === "boolean") {
3076
+ return apiError.isRetryable;
3077
+ }
3078
+ return apiError.statusCode === 408 || apiError.statusCode === 409 || apiError.statusCode === 429 || apiError.statusCode !== void 0 && apiError.statusCode >= 500;
3079
+ }
3080
+ const isDownloadError = DownloadError.isInstance(error);
3081
+ const isSerializedDownloadError = error.name === "AI_DownloadError";
3082
+ if (!isDownloadError && !isSerializedDownloadError) {
3083
+ return false;
3084
+ }
3085
+ const statusCode = error.statusCode;
3086
+ return statusCode === void 0 || statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
3087
+ }
3088
+ function calculateDelay(attempt, baseDelay, maxDelay) {
3089
+ const exponentialDelay = baseDelay * 2 ** (attempt - 1);
3090
+ const delayWithJitter = exponentialDelay * (0.5 + Math.random() * 0.5);
3091
+ return Math.min(delayWithJitter, maxDelay);
3092
+ }
3093
+ async function withRetry(fn, {
3094
+ maxRetries = DEFAULT_RETRY_OPTIONS.maxRetries,
3095
+ baseDelay = DEFAULT_RETRY_OPTIONS.baseDelay,
3096
+ maxDelay = DEFAULT_RETRY_OPTIONS.maxDelay,
3097
+ shouldRetry = defaultShouldRetry
3098
+ } = {}) {
3099
+ let lastError;
3100
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
3101
+ try {
3102
+ return await fn();
3103
+ } catch (error) {
3104
+ lastError = error;
3105
+ const retryError = error instanceof Error ? error : new Error(String(error));
3106
+ const isLastAttempt = attempt === maxRetries;
3107
+ if (isLastAttempt || !shouldRetry(retryError, attempt + 1)) {
3108
+ throw error;
3109
+ }
3110
+ const delay = calculateDelay(attempt + 1, baseDelay, maxDelay);
3111
+ console.warn(
3112
+ `Attempt ${attempt + 1} failed: ${retryError.message}. Retrying in ${Math.round(delay)}ms...`
3113
+ );
3114
+ await new Promise((resolve) => setTimeout(resolve, delay));
3115
+ }
3116
+ }
3117
+ throw lastError ?? new Error("Retry failed with unknown error");
3118
+ }
3119
+
3120
+ // src/lib/content-policy-error.ts
2836
3121
  var PolicyTokenSchema = z2.string().trim().min(1).max(64).regex(/^[A-Z0-9_]+$/);
2837
3122
  var SafetyRatingSchema = z2.object({
2838
3123
  category: PolicyTokenSchema.nullish(),
@@ -2877,6 +3162,12 @@ async function withContentPolicyErrorHandling(operation) {
2877
3162
  rethrowContentPolicyError(error);
2878
3163
  }
2879
3164
  }
3165
+ async function withContentPolicyAwareRetry(operation, retryOptions) {
3166
+ return withRetry(
3167
+ () => withContentPolicyErrorHandling(operation),
3168
+ retryOptions
3169
+ );
3170
+ }
2880
3171
  function rethrowContentPolicyError(error) {
2881
3172
  const block = extractContentPolicyBlock(error);
2882
3173
  if (!block) {
@@ -2913,7 +3204,7 @@ function extractContentPolicyBlock(error) {
2913
3204
  if (NoObjectGeneratedError.isInstance(error) && error.finishReason === "content-filter") {
2914
3205
  return { reason: "CONTENT_FILTER" };
2915
3206
  }
2916
- if (!APICallError.isInstance(error) || !error.responseBody) {
3207
+ if (!APICallError2.isInstance(error) || !error.responseBody) {
2917
3208
  return void 0;
2918
3209
  }
2919
3210
  try {
@@ -2952,7 +3243,7 @@ function getContentPolicyTokenUsage(error) {
2952
3243
  if (RetryError.isInstance(error)) {
2953
3244
  return getContentPolicyTokenUsage(error.lastError);
2954
3245
  }
2955
- if (!APICallError.isInstance(error) || !error.responseBody) {
3246
+ if (!APICallError2.isInstance(error) || !error.responseBody) {
2956
3247
  return void 0;
2957
3248
  }
2958
3249
  try {
@@ -3166,47 +3457,6 @@ function createLanguageSection(languageName) {
3166
3457
  };
3167
3458
  }
3168
3459
 
3169
- // src/lib/retry.ts
3170
- var DEFAULT_RETRY_OPTIONS = {
3171
- maxRetries: 3,
3172
- baseDelay: 2e3,
3173
- maxDelay: 1e4
3174
- };
3175
- function defaultShouldRetry(error, _attempt) {
3176
- return Boolean(error.message && error.message.includes("Timeout while downloading"));
3177
- }
3178
- function calculateDelay(attempt, baseDelay, maxDelay) {
3179
- const exponentialDelay = baseDelay * 2 ** (attempt - 1);
3180
- const delayWithJitter = exponentialDelay * (0.5 + Math.random() * 0.5);
3181
- return Math.min(delayWithJitter, maxDelay);
3182
- }
3183
- async function withRetry(fn, {
3184
- maxRetries = DEFAULT_RETRY_OPTIONS.maxRetries,
3185
- baseDelay = DEFAULT_RETRY_OPTIONS.baseDelay,
3186
- maxDelay = DEFAULT_RETRY_OPTIONS.maxDelay,
3187
- shouldRetry = defaultShouldRetry
3188
- } = {}) {
3189
- let lastError;
3190
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
3191
- try {
3192
- return await fn();
3193
- } catch (error) {
3194
- lastError = error;
3195
- const retryError = error instanceof Error ? error : new Error(String(error));
3196
- const isLastAttempt = attempt === maxRetries;
3197
- if (isLastAttempt || !shouldRetry(retryError, attempt + 1)) {
3198
- throw error;
3199
- }
3200
- const delay = calculateDelay(attempt + 1, baseDelay, maxDelay);
3201
- console.warn(
3202
- `Attempt ${attempt + 1} failed: ${retryError.message}. Retrying in ${Math.round(delay)}ms...`
3203
- );
3204
- await new Promise((resolve) => setTimeout(resolve, delay));
3205
- }
3206
- }
3207
- throw lastError ?? new Error("Retry failed with unknown error");
3208
- }
3209
-
3210
3460
  // src/workflows/ask-questions.ts
3211
3461
  var ASK_QUESTIONS_MAX_QUESTION_LENGTH = 600;
3212
3462
  var ASK_QUESTIONS_DEFAULT_MAX_ANSWER_OPTION_LENGTH = 150;
@@ -3594,8 +3844,9 @@ async function analyzeQuestions({
3594
3844
  normalizedQuestions,
3595
3845
  maxFreeFormAnswerLength
3596
3846
  );
3597
- const response = await withContentPolicyErrorHandling(() => generateText({
3847
+ const response = await withContentPolicyAwareRetry(() => generateText({
3598
3848
  model,
3849
+ maxRetries: 0,
3599
3850
  output: Output.object({ schema: responseSchema }),
3600
3851
  experimental_telemetry: { isEnabled: true },
3601
3852
  messages: [
@@ -3794,18 +4045,16 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
3794
4045
  credentials
3795
4046
  });
3796
4047
  } else {
3797
- analysisResponse = await withRetry(
3798
- () => analyzeQuestions({
3799
- provider: modelConfig.provider,
3800
- modelId: modelConfig.modelId,
3801
- userPrompt,
3802
- systemPrompt,
3803
- normalizedQuestions,
3804
- maxFreeFormAnswerLength,
3805
- imageDataUrl: storyboardUrl,
3806
- credentials
3807
- })
3808
- );
4048
+ analysisResponse = await analyzeQuestions({
4049
+ provider: modelConfig.provider,
4050
+ modelId: modelConfig.modelId,
4051
+ userPrompt,
4052
+ systemPrompt,
4053
+ normalizedQuestions,
4054
+ maxFreeFormAnswerLength,
4055
+ imageDataUrl: storyboardUrl,
4056
+ credentials
4057
+ });
3809
4058
  }
3810
4059
  }
3811
4060
  } catch (error) {
@@ -4007,8 +4256,9 @@ async function analyzeStoryboard({
4007
4256
  }) {
4008
4257
  "use step";
4009
4258
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
4010
- const response = await withContentPolicyErrorHandling(() => generateText2({
4259
+ const response = await withContentPolicyAwareRetry(() => generateText2({
4011
4260
  model,
4261
+ maxRetries: 0,
4012
4262
  output: Output2.object({ schema: burnedInCaptionsSchema }),
4013
4263
  experimental_telemetry: { isEnabled: true },
4014
4264
  messages: [
@@ -4156,24 +4406,21 @@ async function generateChaptersWithAI({
4156
4406
  }) {
4157
4407
  "use step";
4158
4408
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
4159
- const response = await withRetry(
4160
- () => withContentPolicyErrorHandling(
4161
- () => generateText3({
4162
- model,
4163
- output: Output3.object({ schema: chaptersSchema }),
4164
- messages: [
4165
- {
4166
- role: "system",
4167
- content: systemPrompt
4168
- },
4169
- {
4170
- role: "user",
4171
- content: userPrompt
4172
- }
4173
- ]
4174
- })
4175
- )
4176
- );
4409
+ const response = await withContentPolicyAwareRetry(() => generateText3({
4410
+ model,
4411
+ maxRetries: 0,
4412
+ output: Output3.object({ schema: chaptersSchema }),
4413
+ messages: [
4414
+ {
4415
+ role: "system",
4416
+ content: systemPrompt
4417
+ },
4418
+ {
4419
+ role: "user",
4420
+ content: userPrompt
4421
+ }
4422
+ ]
4423
+ }));
4177
4424
  const output = getGeneratedOutputWithContentPolicyHandling(response);
4178
4425
  const unexpectedRootKeys = detectUnexpectedKeysFromRawText(
4179
4426
  response.text,
@@ -4702,8 +4949,9 @@ async function identifyProfanityWithAI({
4702
4949
  tag: "transcript",
4703
4950
  content: plainText
4704
4951
  });
4705
- const response = await withContentPolicyErrorHandling(() => generateText4({
4952
+ const response = await withContentPolicyAwareRetry(() => generateText4({
4706
4953
  model,
4954
+ maxRetries: 0,
4707
4955
  output: Output4.object({ schema: profanityDetectionSchema }),
4708
4956
  messages: [
4709
4957
  {
@@ -4988,9 +5236,10 @@ async function generateSingleChunkEmbedding({
4988
5236
  }) {
4989
5237
  "use step";
4990
5238
  const model = await createEmbeddingModelFromConfig(provider, modelId, credentials);
4991
- const response = await withRetry(
5239
+ const response = await withContentPolicyAwareRetry(
4992
5240
  () => embed({
4993
5241
  model,
5242
+ maxRetries: 0,
4994
5243
  value: chunk.text
4995
5244
  })
4996
5245
  );
@@ -5409,28 +5658,25 @@ async function fetchEngagementData(assetId, options) {
5409
5658
  async function generateInsightsWithAI(provider, modelId, systemPrompt, userPrompt, imageUrls, credentials) {
5410
5659
  "use step";
5411
5660
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
5412
- const response = await withRetry(
5413
- () => withContentPolicyErrorHandling(
5414
- () => generateText5({
5415
- model,
5416
- output: Output5.object({ schema: engagementInsightsSchema }),
5417
- experimental_telemetry: { isEnabled: true },
5418
- messages: [
5419
- {
5420
- role: "system",
5421
- content: systemPrompt
5422
- },
5423
- {
5424
- role: "user",
5425
- content: [
5426
- { type: "text", text: userPrompt },
5427
- ...imageUrls.map((url) => ({ type: "image", image: url }))
5428
- ]
5429
- }
5661
+ const response = await withContentPolicyAwareRetry(() => generateText5({
5662
+ model,
5663
+ maxRetries: 0,
5664
+ output: Output5.object({ schema: engagementInsightsSchema }),
5665
+ experimental_telemetry: { isEnabled: true },
5666
+ messages: [
5667
+ {
5668
+ role: "system",
5669
+ content: systemPrompt
5670
+ },
5671
+ {
5672
+ role: "user",
5673
+ content: [
5674
+ { type: "text", text: userPrompt },
5675
+ ...imageUrls.map((url) => ({ type: "image", image: url }))
5430
5676
  ]
5431
- })
5432
- )
5433
- );
5677
+ }
5678
+ ]
5679
+ }));
5434
5680
  const output = getGeneratedOutputWithContentPolicyHandling(response);
5435
5681
  if (!output) {
5436
5682
  throw new Error("AI returned empty or unparseable response");
@@ -6188,8 +6434,8 @@ async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options)
6188
6434
  "use step";
6189
6435
  const { width, shouldSign, credentials } = options;
6190
6436
  const baseUrl = getMuxThumbnailBaseUrl(playbackId);
6191
- const urlPromises = timestampsMs.map(async (tsMs) => {
6192
- const time = toThumbnailTimeSeconds(tsMs);
6437
+ const times = [...new Set(timestampsMs.map(toThumbnailTimeSeconds))];
6438
+ const urlPromises = times.map(async (time) => {
6193
6439
  const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
6194
6440
  return { url, time };
6195
6441
  });
@@ -6675,8 +6921,9 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
6675
6921
  "use step";
6676
6922
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6677
6923
  const schema = buildSummarySchema(descriptionLength);
6678
- const response = await withContentPolicyErrorHandling(() => generateText6({
6924
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6679
6925
  model,
6926
+ maxRetries: 0,
6680
6927
  output: Output6.object({
6681
6928
  name: "summary_metadata",
6682
6929
  description: "Structured summary with title, description, and keywords.",
@@ -6722,8 +6969,9 @@ async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, des
6722
6969
  "use step";
6723
6970
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6724
6971
  const schema = buildSummarySchema(descriptionLength);
6725
- const response = await withContentPolicyErrorHandling(() => generateText6({
6972
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6726
6973
  model,
6974
+ maxRetries: 0,
6727
6975
  output: Output6.object({
6728
6976
  name: "summary_metadata",
6729
6977
  description: "Structured summary with title, description, and keywords.",
@@ -6906,7 +7154,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6906
7154
  workflowCredentials
6907
7155
  );
6908
7156
  } else {
6909
- analysisResponse = await withRetry(() => analyzeStoryboard2(
7157
+ analysisResponse = await analyzeStoryboard2(
6910
7158
  storyboardUrl,
6911
7159
  modelConfig.provider,
6912
7160
  modelConfig.modelId,
@@ -6914,7 +7162,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6914
7162
  systemPrompt,
6915
7163
  effectiveDescriptionLength,
6916
7164
  workflowCredentials
6917
- ));
7165
+ );
6918
7166
  }
6919
7167
  }
6920
7168
  } catch (error) {
@@ -7158,6 +7406,59 @@ async function downloadAndUploadDubbedAudio({
7158
7406
  console.warn(`\u{1F517} Generated presigned URL (expires in ${expiryHours} hour${expiryHours === 1 ? "" : "s"})`);
7159
7407
  return presignedUrl;
7160
7408
  }
7409
+ async function downloadAndUploadDubTranscript({
7410
+ dubbingId,
7411
+ languageCode,
7412
+ assetId,
7413
+ toLanguageCode,
7414
+ s3Endpoint,
7415
+ s3Region,
7416
+ s3Bucket,
7417
+ storageAdapter,
7418
+ s3SignedUrlExpirySeconds,
7419
+ credentials
7420
+ }) {
7421
+ "use step";
7422
+ const elevenLabsApiKey = await getApiKeyFromEnv("elevenlabs", credentials);
7423
+ const transcriptUrl = `https://api.elevenlabs.io/v1/dubbing/${dubbingId}/transcripts/${languageCode}/format/webvtt`;
7424
+ const transcriptResponse = await fetch(transcriptUrl, {
7425
+ headers: {
7426
+ "xi-api-key": elevenLabsApiKey
7427
+ }
7428
+ });
7429
+ if (!transcriptResponse.ok) {
7430
+ throw new Error(`Failed to fetch dub transcript: ${transcriptResponse.statusText}`);
7431
+ }
7432
+ const transcriptBody = await transcriptResponse.json();
7433
+ const transcriptVtt = transcriptBody.webvtt;
7434
+ if (!transcriptVtt) {
7435
+ throw new Error("Dub transcript response did not include webvtt content");
7436
+ }
7437
+ const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
7438
+ const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
7439
+ const vttKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.vtt`;
7440
+ await putObjectWithStorageAdapter({
7441
+ accessKeyId: s3AccessKeyId,
7442
+ secretAccessKey: s3SecretAccessKey,
7443
+ endpoint: s3Endpoint,
7444
+ region: s3Region,
7445
+ bucket: s3Bucket,
7446
+ key: vttKey,
7447
+ body: transcriptVtt,
7448
+ contentType: "text/vtt"
7449
+ }, storageAdapter);
7450
+ const presignedUrl = await createPresignedGetUrlWithStorageAdapter({
7451
+ accessKeyId: s3AccessKeyId,
7452
+ secretAccessKey: s3SecretAccessKey,
7453
+ endpoint: s3Endpoint,
7454
+ region: s3Region,
7455
+ bucket: s3Bucket,
7456
+ key: vttKey,
7457
+ expiresInSeconds: s3SignedUrlExpirySeconds ?? 86400
7458
+ }, storageAdapter);
7459
+ console.warn(`\u2705 Dub transcript uploaded successfully to: ${vttKey}`);
7460
+ return presignedUrl;
7461
+ }
7161
7462
  async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
7162
7463
  "use step";
7163
7464
  const muxClient = await resolveMuxClient(credentials);
@@ -7184,6 +7485,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7184
7485
  // 0 = auto-detect
7185
7486
  uploadToS3: uploadToS3Option,
7186
7487
  uploadToMux: uploadToMuxOption,
7488
+ uploadCaptionsToMux = false,
7187
7489
  storageAdapter,
7188
7490
  credentials: providedCredentials
7189
7491
  } = options;
@@ -7193,7 +7495,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7193
7495
  const credentials = providedCredentials;
7194
7496
  const effectiveStorageAdapter = storageAdapter;
7195
7497
  const uploadToMux = uploadToMuxOption !== false;
7196
- const uploadToS3 = uploadToS3Option || uploadToMux;
7498
+ const uploadToS3 = uploadToS3Option || uploadToMux || uploadCaptionsToMux;
7197
7499
  const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
7198
7500
  const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
7199
7501
  const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
@@ -7274,10 +7576,12 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7274
7576
  console.warn("\u2705 Dubbing completed successfully!");
7275
7577
  let presignedUrl;
7276
7578
  let uploadedTrackId;
7579
+ let captionsPresignedUrl;
7580
+ let captionsTrackId;
7277
7581
  if (uploadToS3) {
7278
7582
  console.warn("\u{1F4E5} Downloading dubbed audio from ElevenLabs and staging to S3...");
7583
+ const downloadLangCode = targetLanguages[0] ?? toISO639_1(toLanguageCode);
7279
7584
  try {
7280
- const downloadLangCode = targetLanguages[0] ?? toISO639_1(toLanguageCode);
7281
7585
  presignedUrl = await downloadAndUploadDubbedAudio({
7282
7586
  dubbingId,
7283
7587
  languageCode: downloadLangCode,
@@ -7309,6 +7613,40 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7309
7613
  console.warn(presignedUrl);
7310
7614
  }
7311
7615
  }
7616
+ try {
7617
+ console.warn("\u{1F4E5} Downloading dub transcript from ElevenLabs and staging to S3...");
7618
+ captionsPresignedUrl = await downloadAndUploadDubTranscript({
7619
+ dubbingId,
7620
+ languageCode: downloadLangCode,
7621
+ assetId,
7622
+ toLanguageCode,
7623
+ s3Endpoint,
7624
+ s3Region,
7625
+ s3Bucket,
7626
+ storageAdapter: effectiveStorageAdapter,
7627
+ s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds,
7628
+ credentials
7629
+ });
7630
+ if (uploadCaptionsToMux) {
7631
+ console.warn("\u{1F4F9} Adding dubbed captions track to Mux asset...");
7632
+ const muxLangCode = toISO639_1(toLanguageCode);
7633
+ const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7634
+ captionsTrackId = await createTextTrackOnMux(
7635
+ assetId,
7636
+ muxLangCode,
7637
+ `${languageName} (auto-dubbed)`,
7638
+ captionsPresignedUrl,
7639
+ credentials
7640
+ );
7641
+ console.warn(`\u2705 Captions track added to Mux asset with ID: ${captionsTrackId}`);
7642
+ }
7643
+ } catch (error) {
7644
+ console.warn(`\u26A0\uFE0F Failed to attach dubbed captions: ${error instanceof Error ? error.message : "Unknown error"}`);
7645
+ if (captionsPresignedUrl) {
7646
+ console.warn("\u{1F517} You can manually add the captions track using this presigned URL:");
7647
+ console.warn(captionsPresignedUrl);
7648
+ }
7649
+ }
7312
7650
  }
7313
7651
  const targetLanguage = getLanguageCodePair(toLanguageCode);
7314
7652
  return {
@@ -7318,6 +7656,8 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7318
7656
  dubbingId,
7319
7657
  uploadedTrackId,
7320
7658
  presignedUrl,
7659
+ captionsTrackId,
7660
+ captionsPresignedUrl,
7321
7661
  usage: {
7322
7662
  metadata: {
7323
7663
  assetDurationSeconds
@@ -7328,7 +7668,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7328
7668
 
7329
7669
  // src/workflows/translate-captions.ts
7330
7670
  import {
7331
- APICallError as APICallError2,
7671
+ APICallError as APICallError3,
7332
7672
  generateText as generateText7,
7333
7673
  NoObjectGeneratedError as NoObjectGeneratedError2,
7334
7674
  Output as Output7,
@@ -7407,7 +7747,7 @@ function isProviderServiceError(error) {
7407
7747
  if (RetryError2.isInstance(error)) {
7408
7748
  return isProviderServiceError(error.lastError);
7409
7749
  }
7410
- if (APICallError2.isInstance(error)) {
7750
+ if (APICallError3.isInstance(error)) {
7411
7751
  return true;
7412
7752
  }
7413
7753
  if (error instanceof Error && "cause" in error) {
@@ -7580,8 +7920,9 @@ async function translateVttWithAI({
7580
7920
  "use step";
7581
7921
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
7582
7922
  const sanitisedVttContent = stripVttMetadataBlocks(vttContent);
7583
- const response = await withContentPolicyErrorHandling(() => generateText7({
7923
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7584
7924
  model,
7925
+ maxRetries: 0,
7585
7926
  output: Output7.object({ schema: translationSchema }),
7586
7927
  messages: [
7587
7928
  {
@@ -7646,8 +7987,9 @@ async function translateCueChunkWithAI({
7646
7987
  endTime: cue.endTime,
7647
7988
  text: cue.text
7648
7989
  }));
7649
- const response = await withContentPolicyErrorHandling(() => generateText7({
7990
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7650
7991
  model,
7992
+ maxRetries: 0,
7651
7993
  output: Output7.object({ schema }),
7652
7994
  messages: [
7653
7995
  {