@mux/ai 0.31.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,57 +1717,6 @@ function createLanguageSection(languageName) {
1429
1717
  };
1430
1718
  }
1431
1719
 
1432
- // src/lib/retry.ts
1433
- import { DownloadError } from "ai";
1434
- var DEFAULT_RETRY_OPTIONS = {
1435
- maxRetries: 3,
1436
- baseDelay: 2e3,
1437
- maxDelay: 1e4
1438
- };
1439
- function defaultShouldRetry(error, _attempt) {
1440
- if (error.message.includes("Timeout while downloading")) {
1441
- return true;
1442
- }
1443
- const isDownloadError = DownloadError.isInstance(error);
1444
- const isSerializedDownloadError = error.name === "AI_DownloadError";
1445
- if (!isDownloadError && !isSerializedDownloadError) {
1446
- return false;
1447
- }
1448
- const statusCode = error.statusCode;
1449
- return statusCode === void 0 || statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
1450
- }
1451
- function calculateDelay(attempt, baseDelay, maxDelay) {
1452
- const exponentialDelay = baseDelay * 2 ** (attempt - 1);
1453
- const delayWithJitter = exponentialDelay * (0.5 + Math.random() * 0.5);
1454
- return Math.min(delayWithJitter, maxDelay);
1455
- }
1456
- async function withRetry(fn, {
1457
- maxRetries = DEFAULT_RETRY_OPTIONS.maxRetries,
1458
- baseDelay = DEFAULT_RETRY_OPTIONS.baseDelay,
1459
- maxDelay = DEFAULT_RETRY_OPTIONS.maxDelay,
1460
- shouldRetry = defaultShouldRetry
1461
- } = {}) {
1462
- let lastError;
1463
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1464
- try {
1465
- return await fn();
1466
- } catch (error) {
1467
- lastError = error;
1468
- const retryError = error instanceof Error ? error : new Error(String(error));
1469
- const isLastAttempt = attempt === maxRetries;
1470
- if (isLastAttempt || !shouldRetry(retryError, attempt + 1)) {
1471
- throw error;
1472
- }
1473
- const delay = calculateDelay(attempt + 1, baseDelay, maxDelay);
1474
- console.warn(
1475
- `Attempt ${attempt + 1} failed: ${retryError.message}. Retrying in ${Math.round(delay)}ms...`
1476
- );
1477
- await new Promise((resolve) => setTimeout(resolve, delay));
1478
- }
1479
- }
1480
- throw lastError ?? new Error("Retry failed with unknown error");
1481
- }
1482
-
1483
1720
  // src/lib/workflow-scope.ts
1484
1721
  function hasWorkflowScopeBoundaries(scope) {
1485
1722
  return scope?.startTime !== void 0 || scope?.endTime !== void 0;
@@ -2642,8 +2879,9 @@ async function analyzeQuestions({
2642
2879
  normalizedQuestions,
2643
2880
  maxFreeFormAnswerLength
2644
2881
  );
2645
- const response = await withContentPolicyErrorHandling(() => generateText({
2882
+ const response = await withContentPolicyAwareRetry(() => generateText({
2646
2883
  model,
2884
+ maxRetries: 0,
2647
2885
  output: Output.object({ schema: responseSchema }),
2648
2886
  experimental_telemetry: { isEnabled: true },
2649
2887
  messages: [
@@ -2842,18 +3080,16 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
2842
3080
  credentials
2843
3081
  });
2844
3082
  } else {
2845
- analysisResponse = await withRetry(
2846
- () => analyzeQuestions({
2847
- provider: modelConfig.provider,
2848
- modelId: modelConfig.modelId,
2849
- userPrompt,
2850
- systemPrompt,
2851
- normalizedQuestions,
2852
- maxFreeFormAnswerLength,
2853
- imageDataUrl: storyboardUrl,
2854
- credentials
2855
- })
2856
- );
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
+ });
2857
3093
  }
2858
3094
  }
2859
3095
  } catch (error) {
@@ -3055,8 +3291,9 @@ async function analyzeStoryboard({
3055
3291
  }) {
3056
3292
  "use step";
3057
3293
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
3058
- const response = await withContentPolicyErrorHandling(() => generateText2({
3294
+ const response = await withContentPolicyAwareRetry(() => generateText2({
3059
3295
  model,
3296
+ maxRetries: 0,
3060
3297
  output: Output2.object({ schema: burnedInCaptionsSchema }),
3061
3298
  experimental_telemetry: { isEnabled: true },
3062
3299
  messages: [
@@ -3146,14 +3383,14 @@ async function hasBurnedInCaptionsInternal(assetId, options, collectedUsage) {
3146
3383
  credentials
3147
3384
  });
3148
3385
  } else {
3149
- analysisResponse = await withRetry(() => analyzeStoryboard({
3386
+ analysisResponse = await analyzeStoryboard({
3150
3387
  imageDataUrl: imageUrl,
3151
3388
  provider: modelConfig.provider,
3152
3389
  modelId: modelConfig.modelId,
3153
3390
  userPrompt,
3154
3391
  systemPrompt: SYSTEM_PROMPT2,
3155
3392
  credentials
3156
- }));
3393
+ });
3157
3394
  }
3158
3395
  collectedUsage.push(analysisResponse.usage);
3159
3396
  if (!analysisResponse.result) {
@@ -3204,24 +3441,21 @@ async function generateChaptersWithAI({
3204
3441
  }) {
3205
3442
  "use step";
3206
3443
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
3207
- const response = await withRetry(
3208
- () => withContentPolicyErrorHandling(
3209
- () => generateText3({
3210
- model,
3211
- output: Output3.object({ schema: chaptersSchema }),
3212
- messages: [
3213
- {
3214
- role: "system",
3215
- content: systemPrompt
3216
- },
3217
- {
3218
- role: "user",
3219
- content: userPrompt
3220
- }
3221
- ]
3222
- })
3223
- )
3224
- );
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
+ }));
3225
3459
  const output = getGeneratedOutputWithContentPolicyHandling(response);
3226
3460
  const unexpectedRootKeys = detectUnexpectedKeysFromRawText(
3227
3461
  response.text,
@@ -3962,8 +4196,9 @@ async function identifyProfanityWithAI({
3962
4196
  tag: "transcript",
3963
4197
  content: plainText
3964
4198
  });
3965
- const response = await withContentPolicyErrorHandling(() => generateText4({
4199
+ const response = await withContentPolicyAwareRetry(() => generateText4({
3966
4200
  model,
4201
+ maxRetries: 0,
3967
4202
  output: Output4.object({ schema: profanityDetectionSchema }),
3968
4203
  messages: [
3969
4204
  {
@@ -4488,9 +4723,10 @@ async function generateSingleChunkEmbedding({
4488
4723
  }) {
4489
4724
  "use step";
4490
4725
  const model = await createEmbeddingModelFromConfig(provider, modelId, credentials);
4491
- const response = await withRetry(
4726
+ const response = await withContentPolicyAwareRetry(
4492
4727
  () => embed({
4493
4728
  model,
4729
+ maxRetries: 0,
4494
4730
  value: chunk.text
4495
4731
  })
4496
4732
  );
@@ -5055,28 +5291,25 @@ async function fetchEngagementData(assetId, options) {
5055
5291
  async function generateInsightsWithAI(provider, modelId, systemPrompt, userPrompt, imageUrls, credentials) {
5056
5292
  "use step";
5057
5293
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
5058
- const response = await withRetry(
5059
- () => withContentPolicyErrorHandling(
5060
- () => generateText5({
5061
- model,
5062
- output: Output5.object({ schema: engagementInsightsSchema }),
5063
- experimental_telemetry: { isEnabled: true },
5064
- messages: [
5065
- {
5066
- role: "system",
5067
- content: systemPrompt
5068
- },
5069
- {
5070
- role: "user",
5071
- content: [
5072
- { type: "text", text: userPrompt },
5073
- ...imageUrls.map((url) => ({ type: "image", image: url }))
5074
- ]
5075
- }
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 }))
5076
5309
  ]
5077
- })
5078
- )
5079
- );
5310
+ }
5311
+ ]
5312
+ }));
5080
5313
  const output = getGeneratedOutputWithContentPolicyHandling(response);
5081
5314
  if (!output) {
5082
5315
  throw new Error("AI returned empty or unparseable response");
@@ -5396,6 +5629,7 @@ async function getThumbnailUrls(playbackId, duration, options = {}) {
5396
5629
  timestamps.push(time);
5397
5630
  }
5398
5631
  }
5632
+ timestamps = [...new Set(timestamps)];
5399
5633
  if (maxSamples !== void 0 && timestamps.length > maxSamples) {
5400
5634
  const newTimestamps = [];
5401
5635
  newTimestamps.push(resolvedScope.startTime);
@@ -5874,8 +6108,8 @@ async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options)
5874
6108
  "use step";
5875
6109
  const { width, shouldSign, credentials } = options;
5876
6110
  const baseUrl = getMuxThumbnailBaseUrl(playbackId);
5877
- const urlPromises = timestampsMs.map(async (tsMs) => {
5878
- const time = toThumbnailTimeSeconds(tsMs);
6111
+ const times = [...new Set(timestampsMs.map(toThumbnailTimeSeconds))];
6112
+ const urlPromises = times.map(async (time) => {
5879
6113
  const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
5880
6114
  return { url, time };
5881
6115
  });
@@ -6361,8 +6595,9 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
6361
6595
  "use step";
6362
6596
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6363
6597
  const schema = buildSummarySchema(descriptionLength);
6364
- const response = await withContentPolicyErrorHandling(() => generateText6({
6598
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6365
6599
  model,
6600
+ maxRetries: 0,
6366
6601
  output: Output6.object({
6367
6602
  name: "summary_metadata",
6368
6603
  description: "Structured summary with title, description, and keywords.",
@@ -6408,8 +6643,9 @@ async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, des
6408
6643
  "use step";
6409
6644
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6410
6645
  const schema = buildSummarySchema(descriptionLength);
6411
- const response = await withContentPolicyErrorHandling(() => generateText6({
6646
+ const response = await withContentPolicyAwareRetry(() => generateText6({
6412
6647
  model,
6648
+ maxRetries: 0,
6413
6649
  output: Output6.object({
6414
6650
  name: "summary_metadata",
6415
6651
  description: "Structured summary with title, description, and keywords.",
@@ -6592,7 +6828,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6592
6828
  workflowCredentials
6593
6829
  );
6594
6830
  } else {
6595
- analysisResponse = await withRetry(() => analyzeStoryboard2(
6831
+ analysisResponse = await analyzeStoryboard2(
6596
6832
  storyboardUrl,
6597
6833
  modelConfig.provider,
6598
6834
  modelConfig.modelId,
@@ -6600,7 +6836,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6600
6836
  systemPrompt,
6601
6837
  effectiveDescriptionLength,
6602
6838
  workflowCredentials
6603
- ));
6839
+ );
6604
6840
  }
6605
6841
  }
6606
6842
  } catch (error) {
@@ -7106,7 +7342,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7106
7342
 
7107
7343
  // src/workflows/translate-captions.ts
7108
7344
  import {
7109
- APICallError as APICallError2,
7345
+ APICallError as APICallError3,
7110
7346
  generateText as generateText7,
7111
7347
  NoObjectGeneratedError as NoObjectGeneratedError2,
7112
7348
  Output as Output7,
@@ -7185,7 +7421,7 @@ function isProviderServiceError(error) {
7185
7421
  if (RetryError2.isInstance(error)) {
7186
7422
  return isProviderServiceError(error.lastError);
7187
7423
  }
7188
- if (APICallError2.isInstance(error)) {
7424
+ if (APICallError3.isInstance(error)) {
7189
7425
  return true;
7190
7426
  }
7191
7427
  if (error instanceof Error && "cause" in error) {
@@ -7358,8 +7594,9 @@ async function translateVttWithAI({
7358
7594
  "use step";
7359
7595
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
7360
7596
  const sanitisedVttContent = stripVttMetadataBlocks(vttContent);
7361
- const response = await withContentPolicyErrorHandling(() => generateText7({
7597
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7362
7598
  model,
7599
+ maxRetries: 0,
7363
7600
  output: Output7.object({ schema: translationSchema }),
7364
7601
  messages: [
7365
7602
  {
@@ -7424,8 +7661,9 @@ async function translateCueChunkWithAI({
7424
7661
  endTime: cue.endTime,
7425
7662
  text: cue.text
7426
7663
  }));
7427
- const response = await withContentPolicyErrorHandling(() => generateText7({
7664
+ const response = await withContentPolicyAwareRetry(() => generateText7({
7428
7665
  model,
7666
+ maxRetries: 0,
7429
7667
  output: Output7.object({ schema }),
7430
7668
  messages: [
7431
7669
  {