@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.
@@ -4,7 +4,7 @@ import dedent2 from "dedent";
4
4
  import { z as z3 } from "zod";
5
5
 
6
6
  // src/lib/content-policy-error.ts
7
- import { APICallError, NoObjectGeneratedError, RetryError } from "ai";
7
+ import { APICallError as APICallError2, NoObjectGeneratedError, RetryError } from "ai";
8
8
  import { z as z2 } from "zod";
9
9
 
10
10
  // src/lib/prompt-fragments.ts
@@ -79,6 +79,43 @@ var EnvSchema = z.object({
79
79
  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')"),
80
80
  // AI Providers
81
81
  OPENAI_API_KEY: optionalString("OpenAI API key for OpenAI-backed workflows.", "OpenAI API key"),
82
+ BASETEN_API_KEY: optionalString("Baseten API key for Baseten-backed workflows.", "Baseten API key"),
83
+ BASETEN_URL: optionalString(
84
+ "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.",
85
+ "Baseten URL"
86
+ ),
87
+ BASETEN_EMBEDDING_URL: optionalString(
88
+ "Baseten dedicated embedding deployment URL, such as https://model-{MODEL_ID}.api.baseten.co/environments/production/sync. Required for Baseten embeddings.",
89
+ "Baseten embedding URL"
90
+ ),
91
+ BASETEN_MODEL: optionalString(
92
+ "Default Baseten language model identifier used when provider='baseten' and no explicit model is passed.",
93
+ "Baseten model"
94
+ ),
95
+ BASETEN_EMBEDDING_MODEL: optionalString(
96
+ "Default Baseten embedding model identifier used when provider='baseten' and no explicit model is passed.",
97
+ "Baseten embedding model"
98
+ ),
99
+ OPENAI_COMPATIBLE_API_KEY: optionalString(
100
+ "Optional API key for OpenAI-compatible endpoints. Some local or self-hosted endpoints do not require one.",
101
+ "OpenAI-compatible API key"
102
+ ),
103
+ OPENAI_COMPATIBLE_BASE_URL: optionalString(
104
+ "Base URL of an OpenAI-compatible API, such as https://my-endpoint.example.com/v1.",
105
+ "OpenAI-compatible base URL"
106
+ ),
107
+ OPENAI_COMPATIBLE_EMBEDDING_BASE_URL: optionalString(
108
+ "Optional embedding-specific OpenAI-compatible base URL. Falls back to OPENAI_COMPATIBLE_BASE_URL.",
109
+ "OpenAI-compatible embedding base URL"
110
+ ),
111
+ OPENAI_COMPATIBLE_MODEL: optionalString(
112
+ "Default language model identifier used when provider='openai-compatible' and no explicit model is passed.",
113
+ "OpenAI-compatible model"
114
+ ),
115
+ OPENAI_COMPATIBLE_EMBEDDING_MODEL: optionalString(
116
+ "Default embedding model identifier used when provider='openai-compatible' and no explicit model is passed.",
117
+ "OpenAI-compatible embedding model"
118
+ ),
82
119
  ANTHROPIC_API_KEY: optionalString("Anthropic API key for Claude-backed workflows.", "Anthropic API key"),
83
120
  GOOGLE_GENERATIVE_AI_API_KEY: optionalString("Google Generative AI API key for Gemini-backed workflows.", "Google Generative AI API key"),
84
121
  ELEVENLABS_API_KEY: optionalString("ElevenLabs API key for audio translation.", "ElevenLabs API key"),
@@ -507,6 +544,67 @@ function wrapError(error, message) {
507
544
  throw wrapped;
508
545
  }
509
546
 
547
+ // src/lib/retry.ts
548
+ import { APICallError, DownloadError, NoOutputGeneratedError } from "ai";
549
+ var DEFAULT_RETRY_OPTIONS = {
550
+ maxRetries: 3,
551
+ baseDelay: 2e3,
552
+ maxDelay: 1e4
553
+ };
554
+ function defaultShouldRetry(error, _attempt) {
555
+ if (error.message.includes("Timeout while downloading")) {
556
+ return true;
557
+ }
558
+ if (NoOutputGeneratedError.isInstance(error) || error.name === "AI_NoOutputGeneratedError" || error.name === "AI_NoObjectGeneratedError") {
559
+ return true;
560
+ }
561
+ if (APICallError.isInstance(error) || error.name === "AI_APICallError") {
562
+ const apiError = error;
563
+ if (typeof apiError.isRetryable === "boolean") {
564
+ return apiError.isRetryable;
565
+ }
566
+ return apiError.statusCode === 408 || apiError.statusCode === 409 || apiError.statusCode === 429 || apiError.statusCode !== void 0 && apiError.statusCode >= 500;
567
+ }
568
+ const isDownloadError = DownloadError.isInstance(error);
569
+ const isSerializedDownloadError = error.name === "AI_DownloadError";
570
+ if (!isDownloadError && !isSerializedDownloadError) {
571
+ return false;
572
+ }
573
+ const statusCode = error.statusCode;
574
+ return statusCode === void 0 || statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
575
+ }
576
+ function calculateDelay(attempt, baseDelay, maxDelay) {
577
+ const exponentialDelay = baseDelay * 2 ** (attempt - 1);
578
+ const delayWithJitter = exponentialDelay * (0.5 + Math.random() * 0.5);
579
+ return Math.min(delayWithJitter, maxDelay);
580
+ }
581
+ async function withRetry(fn, {
582
+ maxRetries = DEFAULT_RETRY_OPTIONS.maxRetries,
583
+ baseDelay = DEFAULT_RETRY_OPTIONS.baseDelay,
584
+ maxDelay = DEFAULT_RETRY_OPTIONS.maxDelay,
585
+ shouldRetry = defaultShouldRetry
586
+ } = {}) {
587
+ let lastError;
588
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
589
+ try {
590
+ return await fn();
591
+ } catch (error) {
592
+ lastError = error;
593
+ const retryError = error instanceof Error ? error : new Error(String(error));
594
+ const isLastAttempt = attempt === maxRetries;
595
+ if (isLastAttempt || !shouldRetry(retryError, attempt + 1)) {
596
+ throw error;
597
+ }
598
+ const delay = calculateDelay(attempt + 1, baseDelay, maxDelay);
599
+ console.warn(
600
+ `Attempt ${attempt + 1} failed: ${retryError.message}. Retrying in ${Math.round(delay)}ms...`
601
+ );
602
+ await new Promise((resolve) => setTimeout(resolve, delay));
603
+ }
604
+ }
605
+ throw lastError ?? new Error("Retry failed with unknown error");
606
+ }
607
+
510
608
  // src/lib/content-policy-error.ts
511
609
  var PolicyTokenSchema = z2.string().trim().min(1).max(64).regex(/^[A-Z0-9_]+$/);
512
610
  var SafetyRatingSchema = z2.object({
@@ -552,6 +650,12 @@ async function withContentPolicyErrorHandling(operation) {
552
650
  rethrowContentPolicyError(error);
553
651
  }
554
652
  }
653
+ async function withContentPolicyAwareRetry(operation, retryOptions) {
654
+ return withRetry(
655
+ () => withContentPolicyErrorHandling(operation),
656
+ retryOptions
657
+ );
658
+ }
555
659
  function rethrowContentPolicyError(error) {
556
660
  const block = extractContentPolicyBlock(error);
557
661
  if (!block) {
@@ -588,7 +692,7 @@ function extractContentPolicyBlock(error) {
588
692
  if (NoObjectGeneratedError.isInstance(error) && error.finishReason === "content-filter") {
589
693
  return { reason: "CONTENT_FILTER" };
590
694
  }
591
- if (!APICallError.isInstance(error) || !error.responseBody) {
695
+ if (!APICallError2.isInstance(error) || !error.responseBody) {
592
696
  return void 0;
593
697
  }
594
698
  try {
@@ -627,7 +731,7 @@ function getContentPolicyTokenUsage(error) {
627
731
  if (RetryError.isInstance(error)) {
628
732
  return getContentPolicyTokenUsage(error.lastError);
629
733
  }
630
- if (!APICallError.isInstance(error) || !error.responseBody) {
734
+ if (!APICallError2.isInstance(error) || !error.responseBody) {
631
735
  return void 0;
632
736
  }
633
737
  try {
@@ -765,6 +869,7 @@ async function downloadImagesAsBase64(urls, options = {}, maxConcurrent = 5) {
765
869
  import { createAnthropic } from "@ai-sdk/anthropic";
766
870
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
767
871
  import { createOpenAI } from "@ai-sdk/openai";
872
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
768
873
 
769
874
  // src/lib/workflow-crypto.ts
770
875
  import { gcm } from "@noble/ciphers/aes.js";
@@ -1051,6 +1156,7 @@ async function resolveMuxClient(credentials) {
1051
1156
  function resolveProviderApiKeyFromCredentials(provider, resolved) {
1052
1157
  const record = resolved;
1053
1158
  const openaiApiKey = readString(record, "openaiApiKey");
1159
+ const basetenApiKey = readString(record, "basetenApiKey");
1054
1160
  const anthropicApiKey = readString(record, "anthropicApiKey");
1055
1161
  const googleApiKey = readString(record, "googleApiKey");
1056
1162
  const googleVisionApiKey = readString(record, "googleVisionApiKey");
@@ -1058,6 +1164,7 @@ function resolveProviderApiKeyFromCredentials(provider, resolved) {
1058
1164
  const elevenLabsApiKey = readString(record, "elevenLabsApiKey");
1059
1165
  const apiKeyMap = {
1060
1166
  "openai": openaiApiKey ?? env_default.OPENAI_API_KEY,
1167
+ "baseten": basetenApiKey ?? env_default.BASETEN_API_KEY,
1061
1168
  "anthropic": anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
1062
1169
  "google": googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
1063
1170
  "google-vision-api": googleVisionApiKey ?? env_default.GOOGLE_VISION_API_KEY,
@@ -1068,6 +1175,7 @@ function resolveProviderApiKeyFromCredentials(provider, resolved) {
1068
1175
  if (!apiKey) {
1069
1176
  const envVarNames = {
1070
1177
  "openai": "OPENAI_API_KEY",
1178
+ "baseten": "BASETEN_API_KEY",
1071
1179
  "anthropic": "ANTHROPIC_API_KEY",
1072
1180
  "google": "GOOGLE_GENERATIVE_AI_API_KEY",
1073
1181
  "google-vision-api": "GOOGLE_VISION_API_KEY",
@@ -1106,6 +1214,60 @@ var DEFAULT_EMBEDDING_MODELS = {
1106
1214
  openai: "text-embedding-3-small",
1107
1215
  google: "gemini-embedding-001"
1108
1216
  };
1217
+ function resolveBasetenLanguageModelId(model) {
1218
+ const resolved = model ?? env_default.BASETEN_MODEL;
1219
+ if (!resolved) {
1220
+ throw new Error(
1221
+ 'Baseten model is required. Pass `model` when provider is "baseten" or set BASETEN_MODEL.'
1222
+ );
1223
+ }
1224
+ return resolved;
1225
+ }
1226
+ function resolveBasetenEmbeddingModelId(model) {
1227
+ const resolved = model ?? env_default.BASETEN_EMBEDDING_MODEL;
1228
+ if (!resolved) {
1229
+ throw new Error(
1230
+ 'Baseten embedding model is required. Pass `model` when provider is "baseten" or set BASETEN_EMBEDDING_MODEL.'
1231
+ );
1232
+ }
1233
+ return resolved;
1234
+ }
1235
+ function resolveOpenAICompatibleLanguageModelId(model) {
1236
+ const resolved = model ?? env_default.OPENAI_COMPATIBLE_MODEL;
1237
+ if (!resolved) {
1238
+ throw new Error(
1239
+ 'OpenAI-compatible model is required. Pass `model` when provider is "openai-compatible" or set OPENAI_COMPATIBLE_MODEL.'
1240
+ );
1241
+ }
1242
+ return resolved;
1243
+ }
1244
+ function resolveOpenAICompatibleEmbeddingModelId(model) {
1245
+ const resolved = model ?? env_default.OPENAI_COMPATIBLE_EMBEDDING_MODEL;
1246
+ if (!resolved) {
1247
+ throw new Error(
1248
+ 'OpenAI-compatible embedding model is required. Pass `model` when provider is "openai-compatible" or set OPENAI_COMPATIBLE_EMBEDDING_MODEL.'
1249
+ );
1250
+ }
1251
+ return resolved;
1252
+ }
1253
+ function getDefaultLanguageModel(provider) {
1254
+ if (provider === "baseten") {
1255
+ return resolveBasetenLanguageModelId();
1256
+ }
1257
+ if (provider === "openai-compatible") {
1258
+ return resolveOpenAICompatibleLanguageModelId();
1259
+ }
1260
+ return DEFAULT_LANGUAGE_MODELS[provider];
1261
+ }
1262
+ function getDefaultEmbeddingModel(provider) {
1263
+ if (provider === "baseten") {
1264
+ return resolveBasetenEmbeddingModelId();
1265
+ }
1266
+ if (provider === "openai-compatible") {
1267
+ return resolveOpenAICompatibleEmbeddingModelId();
1268
+ }
1269
+ return DEFAULT_EMBEDDING_MODELS[provider];
1270
+ }
1109
1271
  var LANGUAGE_MODELS = {
1110
1272
  openai: ["gpt-5.6-luna", "gpt-5.1", "gpt-5-mini"],
1111
1273
  anthropic: ["claude-sonnet-4-5"],
@@ -1171,7 +1333,7 @@ function getDefaultEvalModelConfigs() {
1171
1333
  function getAllEvalModelConfigs() {
1172
1334
  return Object.entries(LANGUAGE_MODELS).flatMap(([provider, models]) => models.map((modelId) => ({ provider, modelId })));
1173
1335
  }
1174
- function isSupportedProvider(value) {
1336
+ function isSupportedEvalProvider(value) {
1175
1337
  return value === "openai" || value === "anthropic" || value === "google";
1176
1338
  }
1177
1339
  function parseEvalModelPair(value) {
@@ -1184,7 +1346,10 @@ function parseEvalModelPair(value) {
1184
1346
  `Invalid eval model pair "${value}". Use "provider:model" (example: "openai:gpt-5.6-luna").`
1185
1347
  );
1186
1348
  }
1187
- if (!isSupportedProvider(provider)) {
1349
+ if (provider === "baseten" || provider === "openai-compatible") {
1350
+ return void 0;
1351
+ }
1352
+ if (!isSupportedEvalProvider(provider)) {
1188
1353
  throw new Error(
1189
1354
  `Unsupported eval provider "${provider}" in "${value}". Supported providers: ${Object.keys(LANGUAGE_MODELS).join(", ")}.`
1190
1355
  );
@@ -1205,7 +1370,7 @@ function resolveEvalModelConfigs(options = {}) {
1205
1370
  const explicitPairs = options.modelPairs?.map((value) => value.trim()).filter(Boolean) ?? [];
1206
1371
  if (explicitPairs.length > 0) {
1207
1372
  const dedupedPairs = Array.from(new Set(explicitPairs));
1208
- return dedupedPairs.map(parseEvalModelPair);
1373
+ return dedupedPairs.map(parseEvalModelPair).filter((config) => Boolean(config));
1209
1374
  }
1210
1375
  const selection = options.selection ?? "default";
1211
1376
  if (selection === "all") {
@@ -1238,30 +1403,140 @@ function resolveEvalModelConfigsFromEnv(environment = env_default) {
1238
1403
  var EVAL_MODEL_CONFIGS = resolveEvalModelConfigsFromEnv();
1239
1404
  function resolveLanguageModelConfig(options = {}) {
1240
1405
  const provider = options.provider || "openai";
1241
- const modelId = options.model || DEFAULT_LANGUAGE_MODELS[provider];
1406
+ const modelId = options.model || getDefaultLanguageModel(provider);
1242
1407
  maybeWarnOrThrowForDeprecatedLanguageModel(provider, modelId);
1243
1408
  return { provider, modelId };
1244
1409
  }
1245
1410
  function resolveEmbeddingModelConfig(options = {}) {
1246
1411
  const provider = options.provider || "openai";
1247
- const modelId = options.model || DEFAULT_EMBEDDING_MODELS[provider];
1412
+ const modelId = options.model || getDefaultEmbeddingModel(provider);
1248
1413
  return { provider, modelId };
1249
1414
  }
1415
+ var BASETEN_MODEL_APIS_BASE_URL = "https://inference.baseten.co/v1";
1416
+ function readCredentialString(record, key) {
1417
+ const value = record?.[key];
1418
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1419
+ }
1420
+ function normalizeBaseUrl(value) {
1421
+ const trimmed = value?.trim();
1422
+ if (!trimmed) {
1423
+ return void 0;
1424
+ }
1425
+ return trimmed.replace(/\/(chat\/completions|responses|embeddings)\/?$/, "").replace(/\/$/, "");
1426
+ }
1427
+ function isDedicatedBasetenModelUrl(value) {
1428
+ return Boolean(value && (value.includes("/sync") || value.includes("/predict")));
1429
+ }
1430
+ function validateDedicatedBasetenUrl(url, kind) {
1431
+ if (kind === "language") {
1432
+ if (!url.includes("/sync/v1")) {
1433
+ throw new Error(
1434
+ `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.`
1435
+ );
1436
+ }
1437
+ return url;
1438
+ }
1439
+ if (!url.includes("/sync")) {
1440
+ throw new Error(
1441
+ "Baseten embeddings require a dedicated /sync or /sync/v1 deployment URL. Set BASETEN_EMBEDDING_URL or provide basetenEmbeddingUrl in credentials."
1442
+ );
1443
+ }
1444
+ return url;
1445
+ }
1446
+ function basetenEndpointBaseUrl(url, kind) {
1447
+ if (kind === "embedding" && !url.includes("/sync/v1")) {
1448
+ return `${url}/v1`;
1449
+ }
1450
+ return url;
1451
+ }
1452
+ function basetenBaseUrlFromUrl(url, kind) {
1453
+ const normalized = normalizeBaseUrl(url);
1454
+ if (!normalized) {
1455
+ if (kind === "embedding") {
1456
+ throw new Error(
1457
+ "Baseten embedding URL is required. Set BASETEN_EMBEDDING_URL or provide basetenEmbeddingUrl in credentials."
1458
+ );
1459
+ }
1460
+ return BASETEN_MODEL_APIS_BASE_URL;
1461
+ }
1462
+ if (isDedicatedBasetenModelUrl(normalized)) {
1463
+ return basetenEndpointBaseUrl(validateDedicatedBasetenUrl(normalized, kind), kind);
1464
+ }
1465
+ if (kind === "embedding") {
1466
+ throw new Error(
1467
+ "Baseten embeddings require a dedicated /sync or /sync/v1 deployment URL. Set BASETEN_EMBEDDING_URL or provide basetenEmbeddingUrl in credentials."
1468
+ );
1469
+ }
1470
+ return normalized;
1471
+ }
1472
+ function resolveBasetenBaseUrlFromRecord(record, kind) {
1473
+ if (kind === "embedding") {
1474
+ return basetenBaseUrlFromUrl(
1475
+ readCredentialString(record, "basetenEmbeddingUrl") ?? env_default.BASETEN_EMBEDDING_URL,
1476
+ kind
1477
+ );
1478
+ }
1479
+ return basetenBaseUrlFromUrl(
1480
+ readCredentialString(record, "basetenUrl") ?? env_default.BASETEN_URL,
1481
+ kind
1482
+ );
1483
+ }
1484
+ function resolveOpenAICompatibleSettingsFromRecord(record, kind) {
1485
+ const apiKey = readCredentialString(record, "openaiCompatibleApiKey") ?? env_default.OPENAI_COMPATIBLE_API_KEY;
1486
+ const sharedBaseUrl = readCredentialString(record, "openaiCompatibleBaseUrl") ?? env_default.OPENAI_COMPATIBLE_BASE_URL;
1487
+ const baseUrl = normalizeBaseUrl(
1488
+ kind === "embedding" ? readCredentialString(record, "openaiCompatibleEmbeddingBaseUrl") ?? env_default.OPENAI_COMPATIBLE_EMBEDDING_BASE_URL ?? sharedBaseUrl : sharedBaseUrl
1489
+ );
1490
+ if (!baseUrl) {
1491
+ throw new Error(
1492
+ "OpenAI-compatible base URL is required. Set OPENAI_COMPATIBLE_BASE_URL or provide openaiCompatibleBaseUrl in credentials."
1493
+ );
1494
+ }
1495
+ return { apiKey, baseURL: baseUrl };
1496
+ }
1497
+ function createBasetenProvider(apiKey, baseURL) {
1498
+ return createOpenAICompatible({
1499
+ name: "baseten",
1500
+ apiKey,
1501
+ baseURL,
1502
+ supportsStructuredOutputs: true
1503
+ });
1504
+ }
1505
+ function createGenericOpenAICompatibleProvider(settings) {
1506
+ return createOpenAICompatible({
1507
+ name: "openai-compatible",
1508
+ ...settings,
1509
+ supportsStructuredOutputs: true
1510
+ });
1511
+ }
1250
1512
  async function createLanguageModelFromConfig(provider, modelId, credentials) {
1251
1513
  maybeWarnOrThrowForDeprecatedLanguageModel(provider, modelId);
1514
+ const resolvedCredentials = await resolveWorkflowCredentials(credentials);
1515
+ const credentialsRecord = resolvedCredentials;
1252
1516
  switch (provider) {
1253
1517
  case "openai": {
1254
- const apiKey = await resolveProviderApiKey("openai", credentials);
1518
+ const apiKey = resolveProviderApiKeyFromCredentials("openai", resolvedCredentials);
1255
1519
  const openai = createOpenAI({ apiKey });
1256
1520
  return openai(modelId);
1257
1521
  }
1522
+ case "baseten": {
1523
+ const apiKey = resolveProviderApiKeyFromCredentials("baseten", resolvedCredentials);
1524
+ const baseten = createBasetenProvider(apiKey, resolveBasetenBaseUrlFromRecord(credentialsRecord, "language"));
1525
+ return baseten.chatModel(modelId);
1526
+ }
1527
+ case "openai-compatible": {
1528
+ const compatible = createGenericOpenAICompatibleProvider(
1529
+ resolveOpenAICompatibleSettingsFromRecord(credentialsRecord, "language")
1530
+ );
1531
+ return compatible.chatModel(modelId);
1532
+ }
1258
1533
  case "anthropic": {
1259
- const apiKey = await resolveProviderApiKey("anthropic", credentials);
1534
+ const apiKey = resolveProviderApiKeyFromCredentials("anthropic", resolvedCredentials);
1260
1535
  const anthropic = createAnthropic({ apiKey });
1261
1536
  return anthropic(modelId);
1262
1537
  }
1263
1538
  case "google": {
1264
- const apiKey = await resolveProviderApiKey("google", credentials);
1539
+ const apiKey = resolveProviderApiKeyFromCredentials("google", resolvedCredentials);
1265
1540
  const google = createGoogleGenerativeAI({ apiKey });
1266
1541
  return google(modelId);
1267
1542
  }
@@ -1272,14 +1547,27 @@ async function createLanguageModelFromConfig(provider, modelId, credentials) {
1272
1547
  }
1273
1548
  }
1274
1549
  async function createEmbeddingModelFromConfig(provider, modelId, credentials) {
1550
+ const resolvedCredentials = await resolveWorkflowCredentials(credentials);
1551
+ const credentialsRecord = resolvedCredentials;
1275
1552
  switch (provider) {
1276
1553
  case "openai": {
1277
- const apiKey = await resolveProviderApiKey("openai", credentials);
1554
+ const apiKey = resolveProviderApiKeyFromCredentials("openai", resolvedCredentials);
1278
1555
  const openai = createOpenAI({ apiKey });
1279
1556
  return openai.embedding(modelId);
1280
1557
  }
1558
+ case "baseten": {
1559
+ const apiKey = resolveProviderApiKeyFromCredentials("baseten", resolvedCredentials);
1560
+ const baseten = createBasetenProvider(apiKey, resolveBasetenBaseUrlFromRecord(credentialsRecord, "embedding"));
1561
+ return baseten.textEmbeddingModel(modelId);
1562
+ }
1563
+ case "openai-compatible": {
1564
+ const compatible = createGenericOpenAICompatibleProvider(
1565
+ resolveOpenAICompatibleSettingsFromRecord(credentialsRecord, "embedding")
1566
+ );
1567
+ return compatible.textEmbeddingModel(modelId);
1568
+ }
1281
1569
  case "google": {
1282
- const apiKey = await resolveProviderApiKey("google", credentials);
1570
+ const apiKey = resolveProviderApiKeyFromCredentials("google", resolvedCredentials);
1283
1571
  const google = createGoogleGenerativeAI({ apiKey });
1284
1572
  return google.textEmbeddingModel(modelId);
1285
1573
  }
@@ -1429,47 +1717,6 @@ function createLanguageSection(languageName) {
1429
1717
  };
1430
1718
  }
1431
1719
 
1432
- // src/lib/retry.ts
1433
- var DEFAULT_RETRY_OPTIONS = {
1434
- maxRetries: 3,
1435
- baseDelay: 2e3,
1436
- maxDelay: 1e4
1437
- };
1438
- function defaultShouldRetry(error, _attempt) {
1439
- return Boolean(error.message && error.message.includes("Timeout while downloading"));
1440
- }
1441
- function calculateDelay(attempt, baseDelay, maxDelay) {
1442
- const exponentialDelay = baseDelay * 2 ** (attempt - 1);
1443
- const delayWithJitter = exponentialDelay * (0.5 + Math.random() * 0.5);
1444
- return Math.min(delayWithJitter, maxDelay);
1445
- }
1446
- async function withRetry(fn, {
1447
- maxRetries = DEFAULT_RETRY_OPTIONS.maxRetries,
1448
- baseDelay = DEFAULT_RETRY_OPTIONS.baseDelay,
1449
- maxDelay = DEFAULT_RETRY_OPTIONS.maxDelay,
1450
- shouldRetry = defaultShouldRetry
1451
- } = {}) {
1452
- let lastError;
1453
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1454
- try {
1455
- return await fn();
1456
- } catch (error) {
1457
- lastError = error;
1458
- const retryError = error instanceof Error ? error : new Error(String(error));
1459
- const isLastAttempt = attempt === maxRetries;
1460
- if (isLastAttempt || !shouldRetry(retryError, attempt + 1)) {
1461
- throw error;
1462
- }
1463
- const delay = calculateDelay(attempt + 1, baseDelay, maxDelay);
1464
- console.warn(
1465
- `Attempt ${attempt + 1} failed: ${retryError.message}. Retrying in ${Math.round(delay)}ms...`
1466
- );
1467
- await new Promise((resolve) => setTimeout(resolve, delay));
1468
- }
1469
- }
1470
- throw lastError ?? new Error("Retry failed with unknown error");
1471
- }
1472
-
1473
1720
  // src/lib/workflow-scope.ts
1474
1721
  function hasWorkflowScopeBoundaries(scope) {
1475
1722
  return scope?.startTime !== void 0 || scope?.endTime !== void 0;
@@ -2632,8 +2879,9 @@ async function analyzeQuestions({
2632
2879
  normalizedQuestions,
2633
2880
  maxFreeFormAnswerLength
2634
2881
  );
2635
- const response = await withContentPolicyErrorHandling(() => generateText({
2882
+ const response = await withContentPolicyAwareRetry(() => generateText({
2636
2883
  model,
2884
+ maxRetries: 0,
2637
2885
  output: Output.object({ schema: responseSchema }),
2638
2886
  experimental_telemetry: { isEnabled: true },
2639
2887
  messages: [
@@ -2832,18 +3080,16 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
2832
3080
  credentials
2833
3081
  });
2834
3082
  } else {
2835
- analysisResponse = await withRetry(
2836
- () => analyzeQuestions({
2837
- provider: modelConfig.provider,
2838
- modelId: modelConfig.modelId,
2839
- userPrompt,
2840
- systemPrompt,
2841
- normalizedQuestions,
2842
- maxFreeFormAnswerLength,
2843
- imageDataUrl: storyboardUrl,
2844
- credentials
2845
- })
2846
- );
3083
+ analysisResponse = await analyzeQuestions({
3084
+ provider: modelConfig.provider,
3085
+ modelId: modelConfig.modelId,
3086
+ userPrompt,
3087
+ systemPrompt,
3088
+ normalizedQuestions,
3089
+ maxFreeFormAnswerLength,
3090
+ imageDataUrl: storyboardUrl,
3091
+ credentials
3092
+ });
2847
3093
  }
2848
3094
  }
2849
3095
  } catch (error) {
@@ -3045,8 +3291,9 @@ async function analyzeStoryboard({
3045
3291
  }) {
3046
3292
  "use step";
3047
3293
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
3048
- const response = await withContentPolicyErrorHandling(() => generateText2({
3294
+ const response = await withContentPolicyAwareRetry(() => generateText2({
3049
3295
  model,
3296
+ maxRetries: 0,
3050
3297
  output: Output2.object({ schema: burnedInCaptionsSchema }),
3051
3298
  experimental_telemetry: { isEnabled: true },
3052
3299
  messages: [
@@ -3194,24 +3441,21 @@ async function generateChaptersWithAI({
3194
3441
  }) {
3195
3442
  "use step";
3196
3443
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
3197
- const response = await withRetry(
3198
- () => withContentPolicyErrorHandling(
3199
- () => generateText3({
3200
- model,
3201
- output: Output3.object({ schema: chaptersSchema }),
3202
- messages: [
3203
- {
3204
- role: "system",
3205
- content: systemPrompt
3206
- },
3207
- {
3208
- role: "user",
3209
- content: userPrompt
3210
- }
3211
- ]
3212
- })
3213
- )
3214
- );
3444
+ const response = await withContentPolicyAwareRetry(() => generateText3({
3445
+ model,
3446
+ maxRetries: 0,
3447
+ output: Output3.object({ schema: chaptersSchema }),
3448
+ messages: [
3449
+ {
3450
+ role: "system",
3451
+ content: systemPrompt
3452
+ },
3453
+ {
3454
+ role: "user",
3455
+ content: userPrompt
3456
+ }
3457
+ ]
3458
+ }));
3215
3459
  const output = getGeneratedOutputWithContentPolicyHandling(response);
3216
3460
  const unexpectedRootKeys = detectUnexpectedKeysFromRawText(
3217
3461
  response.text,
@@ -3952,8 +4196,9 @@ async function identifyProfanityWithAI({
3952
4196
  tag: "transcript",
3953
4197
  content: plainText
3954
4198
  });
3955
- const response = await withContentPolicyErrorHandling(() => generateText4({
4199
+ const response = await withContentPolicyAwareRetry(() => generateText4({
3956
4200
  model,
4201
+ maxRetries: 0,
3957
4202
  output: Output4.object({ schema: profanityDetectionSchema }),
3958
4203
  messages: [
3959
4204
  {
@@ -4478,9 +4723,10 @@ async function generateSingleChunkEmbedding({
4478
4723
  }) {
4479
4724
  "use step";
4480
4725
  const model = await createEmbeddingModelFromConfig(provider, modelId, credentials);
4481
- const response = await withRetry(
4726
+ const response = await withContentPolicyAwareRetry(
4482
4727
  () => embed({
4483
4728
  model,
4729
+ maxRetries: 0,
4484
4730
  value: chunk.text
4485
4731
  })
4486
4732
  );
@@ -5045,28 +5291,25 @@ async function fetchEngagementData(assetId, options) {
5045
5291
  async function generateInsightsWithAI(provider, modelId, systemPrompt, userPrompt, imageUrls, credentials) {
5046
5292
  "use step";
5047
5293
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
5048
- const response = await withRetry(
5049
- () => withContentPolicyErrorHandling(
5050
- () => generateText5({
5051
- model,
5052
- output: Output5.object({ schema: engagementInsightsSchema }),
5053
- experimental_telemetry: { isEnabled: true },
5054
- messages: [
5055
- {
5056
- role: "system",
5057
- content: systemPrompt
5058
- },
5059
- {
5060
- role: "user",
5061
- content: [
5062
- { type: "text", text: userPrompt },
5063
- ...imageUrls.map((url) => ({ type: "image", image: url }))
5064
- ]
5065
- }
5294
+ const response = await withContentPolicyAwareRetry(() => generateText5({
5295
+ model,
5296
+ maxRetries: 0,
5297
+ output: Output5.object({ schema: engagementInsightsSchema }),
5298
+ experimental_telemetry: { isEnabled: true },
5299
+ messages: [
5300
+ {
5301
+ role: "system",
5302
+ content: systemPrompt
5303
+ },
5304
+ {
5305
+ role: "user",
5306
+ content: [
5307
+ { type: "text", text: userPrompt },
5308
+ ...imageUrls.map((url) => ({ type: "image", image: url }))
5066
5309
  ]
5067
- })
5068
- )
5069
- );
5310
+ }
5311
+ ]
5312
+ }));
5070
5313
  const output = getGeneratedOutputWithContentPolicyHandling(response);
5071
5314
  if (!output) {
5072
5315
  throw new Error("AI returned empty or unparseable response");
@@ -5386,6 +5629,7 @@ async function getThumbnailUrls(playbackId, duration, options = {}) {
5386
5629
  timestamps.push(time);
5387
5630
  }
5388
5631
  }
5632
+ timestamps = [...new Set(timestamps)];
5389
5633
  if (maxSamples !== void 0 && timestamps.length > maxSamples) {
5390
5634
  const newTimestamps = [];
5391
5635
  newTimestamps.push(resolvedScope.startTime);
@@ -5864,8 +6108,8 @@ async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options)
5864
6108
  "use step";
5865
6109
  const { width, shouldSign, credentials } = options;
5866
6110
  const baseUrl = getMuxThumbnailBaseUrl(playbackId);
5867
- const urlPromises = timestampsMs.map(async (tsMs) => {
5868
- const time = toThumbnailTimeSeconds(tsMs);
6111
+ const times = [...new Set(timestampsMs.map(toThumbnailTimeSeconds))];
6112
+ const urlPromises = times.map(async (time) => {
5869
6113
  const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
5870
6114
  return { url, time };
5871
6115
  });
@@ -6351,8 +6595,9 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
6351
6595
  "use step";
6352
6596
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6353
6597
  const schema = buildSummarySchema(descriptionLength);
6354
- const response = await withContentPolicyErrorHandling(() => generateText6({
6598
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6355
6599
  model,
6600
+ maxRetries: 0,
6356
6601
  output: Output6.object({
6357
6602
  name: "summary_metadata",
6358
6603
  description: "Structured summary with title, description, and keywords.",
@@ -6398,8 +6643,9 @@ async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, des
6398
6643
  "use step";
6399
6644
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6400
6645
  const schema = buildSummarySchema(descriptionLength);
6401
- const response = await withContentPolicyErrorHandling(() => generateText6({
6646
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6402
6647
  model,
6648
+ maxRetries: 0,
6403
6649
  output: Output6.object({
6404
6650
  name: "summary_metadata",
6405
6651
  description: "Structured summary with title, description, and keywords.",
@@ -6582,7 +6828,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6582
6828
  workflowCredentials
6583
6829
  );
6584
6830
  } else {
6585
- analysisResponse = await withRetry(() => analyzeStoryboard2(
6831
+ analysisResponse = await analyzeStoryboard2(
6586
6832
  storyboardUrl,
6587
6833
  modelConfig.provider,
6588
6834
  modelConfig.modelId,
@@ -6590,7 +6836,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6590
6836
  systemPrompt,
6591
6837
  effectiveDescriptionLength,
6592
6838
  workflowCredentials
6593
- ));
6839
+ );
6594
6840
  }
6595
6841
  }
6596
6842
  } catch (error) {
@@ -6834,6 +7080,59 @@ async function downloadAndUploadDubbedAudio({
6834
7080
  console.warn(`\u{1F517} Generated presigned URL (expires in ${expiryHours} hour${expiryHours === 1 ? "" : "s"})`);
6835
7081
  return presignedUrl;
6836
7082
  }
7083
+ async function downloadAndUploadDubTranscript({
7084
+ dubbingId,
7085
+ languageCode,
7086
+ assetId,
7087
+ toLanguageCode,
7088
+ s3Endpoint,
7089
+ s3Region,
7090
+ s3Bucket,
7091
+ storageAdapter,
7092
+ s3SignedUrlExpirySeconds,
7093
+ credentials
7094
+ }) {
7095
+ "use step";
7096
+ const elevenLabsApiKey = await getApiKeyFromEnv("elevenlabs", credentials);
7097
+ const transcriptUrl = `https://api.elevenlabs.io/v1/dubbing/${dubbingId}/transcripts/${languageCode}/format/webvtt`;
7098
+ const transcriptResponse = await fetch(transcriptUrl, {
7099
+ headers: {
7100
+ "xi-api-key": elevenLabsApiKey
7101
+ }
7102
+ });
7103
+ if (!transcriptResponse.ok) {
7104
+ throw new Error(`Failed to fetch dub transcript: ${transcriptResponse.statusText}`);
7105
+ }
7106
+ const transcriptBody = await transcriptResponse.json();
7107
+ const transcriptVtt = transcriptBody.webvtt;
7108
+ if (!transcriptVtt) {
7109
+ throw new Error("Dub transcript response did not include webvtt content");
7110
+ }
7111
+ const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
7112
+ const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
7113
+ const vttKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.vtt`;
7114
+ await putObjectWithStorageAdapter({
7115
+ accessKeyId: s3AccessKeyId,
7116
+ secretAccessKey: s3SecretAccessKey,
7117
+ endpoint: s3Endpoint,
7118
+ region: s3Region,
7119
+ bucket: s3Bucket,
7120
+ key: vttKey,
7121
+ body: transcriptVtt,
7122
+ contentType: "text/vtt"
7123
+ }, storageAdapter);
7124
+ const presignedUrl = await createPresignedGetUrlWithStorageAdapter({
7125
+ accessKeyId: s3AccessKeyId,
7126
+ secretAccessKey: s3SecretAccessKey,
7127
+ endpoint: s3Endpoint,
7128
+ region: s3Region,
7129
+ bucket: s3Bucket,
7130
+ key: vttKey,
7131
+ expiresInSeconds: s3SignedUrlExpirySeconds ?? 86400
7132
+ }, storageAdapter);
7133
+ console.warn(`\u2705 Dub transcript uploaded successfully to: ${vttKey}`);
7134
+ return presignedUrl;
7135
+ }
6837
7136
  async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
6838
7137
  "use step";
6839
7138
  const muxClient = await resolveMuxClient(credentials);
@@ -6860,6 +7159,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
6860
7159
  // 0 = auto-detect
6861
7160
  uploadToS3: uploadToS3Option,
6862
7161
  uploadToMux: uploadToMuxOption,
7162
+ uploadCaptionsToMux = false,
6863
7163
  storageAdapter,
6864
7164
  credentials: providedCredentials
6865
7165
  } = options;
@@ -6869,7 +7169,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
6869
7169
  const credentials = providedCredentials;
6870
7170
  const effectiveStorageAdapter = storageAdapter;
6871
7171
  const uploadToMux = uploadToMuxOption !== false;
6872
- const uploadToS3 = uploadToS3Option || uploadToMux;
7172
+ const uploadToS3 = uploadToS3Option || uploadToMux || uploadCaptionsToMux;
6873
7173
  const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
6874
7174
  const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
6875
7175
  const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
@@ -6950,10 +7250,12 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
6950
7250
  console.warn("\u2705 Dubbing completed successfully!");
6951
7251
  let presignedUrl;
6952
7252
  let uploadedTrackId;
7253
+ let captionsPresignedUrl;
7254
+ let captionsTrackId;
6953
7255
  if (uploadToS3) {
6954
7256
  console.warn("\u{1F4E5} Downloading dubbed audio from ElevenLabs and staging to S3...");
7257
+ const downloadLangCode = targetLanguages[0] ?? toISO639_1(toLanguageCode);
6955
7258
  try {
6956
- const downloadLangCode = targetLanguages[0] ?? toISO639_1(toLanguageCode);
6957
7259
  presignedUrl = await downloadAndUploadDubbedAudio({
6958
7260
  dubbingId,
6959
7261
  languageCode: downloadLangCode,
@@ -6985,6 +7287,40 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
6985
7287
  console.warn(presignedUrl);
6986
7288
  }
6987
7289
  }
7290
+ try {
7291
+ console.warn("\u{1F4E5} Downloading dub transcript from ElevenLabs and staging to S3...");
7292
+ captionsPresignedUrl = await downloadAndUploadDubTranscript({
7293
+ dubbingId,
7294
+ languageCode: downloadLangCode,
7295
+ assetId,
7296
+ toLanguageCode,
7297
+ s3Endpoint,
7298
+ s3Region,
7299
+ s3Bucket,
7300
+ storageAdapter: effectiveStorageAdapter,
7301
+ s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds,
7302
+ credentials
7303
+ });
7304
+ if (uploadCaptionsToMux) {
7305
+ console.warn("\u{1F4F9} Adding dubbed captions track to Mux asset...");
7306
+ const muxLangCode = toISO639_1(toLanguageCode);
7307
+ const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7308
+ captionsTrackId = await createTextTrackOnMux(
7309
+ assetId,
7310
+ muxLangCode,
7311
+ `${languageName} (auto-dubbed)`,
7312
+ captionsPresignedUrl,
7313
+ credentials
7314
+ );
7315
+ console.warn(`\u2705 Captions track added to Mux asset with ID: ${captionsTrackId}`);
7316
+ }
7317
+ } catch (error) {
7318
+ console.warn(`\u26A0\uFE0F Failed to attach dubbed captions: ${error instanceof Error ? error.message : "Unknown error"}`);
7319
+ if (captionsPresignedUrl) {
7320
+ console.warn("\u{1F517} You can manually add the captions track using this presigned URL:");
7321
+ console.warn(captionsPresignedUrl);
7322
+ }
7323
+ }
6988
7324
  }
6989
7325
  const targetLanguage = getLanguageCodePair(toLanguageCode);
6990
7326
  return {
@@ -6994,6 +7330,8 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
6994
7330
  dubbingId,
6995
7331
  uploadedTrackId,
6996
7332
  presignedUrl,
7333
+ captionsTrackId,
7334
+ captionsPresignedUrl,
6997
7335
  usage: {
6998
7336
  metadata: {
6999
7337
  assetDurationSeconds
@@ -7004,7 +7342,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7004
7342
 
7005
7343
  // src/workflows/translate-captions.ts
7006
7344
  import {
7007
- APICallError as APICallError2,
7345
+ APICallError as APICallError3,
7008
7346
  generateText as generateText7,
7009
7347
  NoObjectGeneratedError as NoObjectGeneratedError2,
7010
7348
  Output as Output7,
@@ -7083,7 +7421,7 @@ function isProviderServiceError(error) {
7083
7421
  if (RetryError2.isInstance(error)) {
7084
7422
  return isProviderServiceError(error.lastError);
7085
7423
  }
7086
- if (APICallError2.isInstance(error)) {
7424
+ if (APICallError3.isInstance(error)) {
7087
7425
  return true;
7088
7426
  }
7089
7427
  if (error instanceof Error && "cause" in error) {
@@ -7256,8 +7594,9 @@ async function translateVttWithAI({
7256
7594
  "use step";
7257
7595
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
7258
7596
  const sanitisedVttContent = stripVttMetadataBlocks(vttContent);
7259
- const response = await withContentPolicyErrorHandling(() => generateText7({
7597
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7260
7598
  model,
7599
+ maxRetries: 0,
7261
7600
  output: Output7.object({ schema: translationSchema }),
7262
7601
  messages: [
7263
7602
  {
@@ -7322,8 +7661,9 @@ async function translateCueChunkWithAI({
7322
7661
  endTime: cue.endTime,
7323
7662
  text: cue.text
7324
7663
  }));
7325
- const response = await withContentPolicyErrorHandling(() => generateText7({
7664
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7326
7665
  model,
7666
+ maxRetries: 0,
7327
7667
  output: Output7.object({ schema }),
7328
7668
  messages: [
7329
7669
  {