@umituz/web-ai 1.0.1 → 1.0.3

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.
@@ -196,6 +196,14 @@ var ProviderQuotaExhaustedError = class extends AIError {
196
196
  }
197
197
  };
198
198
 
199
+ // src/core/errors/groq-error.ts
200
+ var GroqError = class extends AIError {
201
+ constructor(type, message, cause, metadata = {}) {
202
+ super(type, message, cause, metadata);
203
+ this.name = "GroqError";
204
+ }
205
+ };
206
+
199
207
  // src/core/errors/error-classifier.ts
200
208
  var DEFAULT_RETRYABLE_TYPES = /* @__PURE__ */ new Set([
201
209
  "NETWORK_ERROR" /* NETWORK_ERROR */,
@@ -1392,6 +1400,27 @@ var RequestDeduplicator = class {
1392
1400
  }
1393
1401
  };
1394
1402
 
1403
+ // src/core/utils/api-key-validator.ts
1404
+ var DEFAULT_API_KEY_RULE = {
1405
+ minLength: 10
1406
+ };
1407
+ function validateApiKey(apiKey, rule = DEFAULT_API_KEY_RULE) {
1408
+ if (!apiKey) {
1409
+ return { valid: false, reason: "API key is required" };
1410
+ }
1411
+ const trimmed = apiKey.trim();
1412
+ if (trimmed.length === 0) {
1413
+ return { valid: false, reason: "API key cannot be empty" };
1414
+ }
1415
+ if (trimmed.length < rule.minLength) {
1416
+ return {
1417
+ valid: false,
1418
+ reason: `API key must be at least ${rule.minLength} characters`
1419
+ };
1420
+ }
1421
+ return { valid: true };
1422
+ }
1423
+
1395
1424
  // src/providers/groq/groq.constants.ts
1396
1425
  var GROQ_MODELS = {
1397
1426
  LLAMA_3_1_8B_INSTANT: "llama-3.1-8b-instant",
@@ -1408,11 +1437,13 @@ var DEFAULT_GENERATION_CONFIG = {
1408
1437
  topP: 1
1409
1438
  };
1410
1439
  var GROQ_BASE_URL = "https://api.groq.com/openai/v1";
1440
+ var DEFAULT_BASE_URL = GROQ_BASE_URL;
1411
1441
  var CHAT_COMPLETIONS_PATH = "/chat/completions";
1412
1442
  var GROQ_TIMEOUTS = {
1413
1443
  DEFAULT: 3e4,
1414
1444
  STREAMING: 6e4
1415
1445
  };
1446
+ var TIMEOUTS = GROQ_TIMEOUTS;
1416
1447
  var HTTP_HEADERS = {
1417
1448
  CONTENT_TYPE: "Content-Type",
1418
1449
  APPLICATION_JSON: "application/json",
@@ -1471,13 +1502,65 @@ var GroqChatRequestBuilder = class _GroqChatRequestBuilder {
1471
1502
  }
1472
1503
  };
1473
1504
 
1505
+ // src/providers/groq/groq-config-store.ts
1506
+ var GroqConfigStore = class {
1507
+ config = null;
1508
+ initialize(config) {
1509
+ const validation = validateApiKey(config.apiKey, { minLength: 1 });
1510
+ if (!validation.valid) {
1511
+ throw new GroqError("INVALID_API_KEY" /* INVALID_API_KEY */, validation.reason ?? "Invalid API key");
1512
+ }
1513
+ const normalized = {
1514
+ apiKey: config.apiKey.trim(),
1515
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
1516
+ timeoutMs: config.timeoutMs ?? TIMEOUTS.DEFAULT,
1517
+ ...config.models !== void 0 ? { models: config.models } : {},
1518
+ ...config.textModel !== void 0 ? { textModel: config.textModel } : {}
1519
+ };
1520
+ this.config = normalized;
1521
+ return normalized;
1522
+ }
1523
+ get() {
1524
+ if (!this.config) {
1525
+ throw new GroqError(
1526
+ "MISSING_CONFIG" /* MISSING_CONFIG */,
1527
+ "Client not initialized. Call configureProvider first."
1528
+ );
1529
+ }
1530
+ return this.config;
1531
+ }
1532
+ getOrNull() {
1533
+ return this.config;
1534
+ }
1535
+ isInitialized() {
1536
+ return this.config !== null;
1537
+ }
1538
+ setApiKey(apiKey) {
1539
+ const current = this.get();
1540
+ const validation = validateApiKey(apiKey, { minLength: 1 });
1541
+ if (!validation.valid) {
1542
+ throw new GroqError("INVALID_API_KEY" /* INVALID_API_KEY */, validation.reason ?? "Invalid API key");
1543
+ }
1544
+ this.config = { ...current, apiKey: apiKey.trim() };
1545
+ }
1546
+ reset() {
1547
+ this.config = null;
1548
+ }
1549
+ };
1550
+
1474
1551
  // src/providers/groq/groq-http-transport.ts
1475
1552
  var GroqHttpTransport = class {
1476
- constructor(config) {
1477
- this.config = config;
1553
+ constructor(configSource) {
1554
+ this.configSource = configSource;
1478
1555
  }
1479
- config;
1556
+ configSource;
1480
1557
  pendingAbortControllers = /* @__PURE__ */ new Set();
1558
+ getConfig() {
1559
+ if ("get" in this.configSource && typeof this.configSource.get === "function") {
1560
+ return this.configSource.get();
1561
+ }
1562
+ return this.configSource;
1563
+ }
1481
1564
  cancelPendingRequests() {
1482
1565
  for (const controller of this.pendingAbortControllers) {
1483
1566
  controller.abort();
@@ -1492,7 +1575,7 @@ var GroqHttpTransport = class {
1492
1575
  }
1493
1576
  async streamChatCompletion(request, options = {}) {
1494
1577
  const url = `${this.baseUrl()}${CHAT_COMPLETIONS_PATH}`;
1495
- const timeout = this.config.timeoutMs ?? GROQ_TIMEOUTS.STREAMING;
1578
+ const timeout = this.getConfig().timeoutMs ?? GROQ_TIMEOUTS.STREAMING;
1496
1579
  const controller = new AbortController();
1497
1580
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1498
1581
  const onExternalAbort = () => controller.abort();
@@ -1519,11 +1602,11 @@ var GroqHttpTransport = class {
1519
1602
  }
1520
1603
  }
1521
1604
  baseUrl() {
1522
- return this.config.baseUrl ?? GROQ_BASE_URL;
1605
+ return this.getConfig().baseUrl ?? GROQ_BASE_URL;
1523
1606
  }
1524
1607
  async request(endpoint, body) {
1525
1608
  const url = `${this.baseUrl()}${endpoint}`;
1526
- const timeout = this.config.timeoutMs ?? GROQ_TIMEOUTS.DEFAULT;
1609
+ const timeout = this.getConfig().timeoutMs ?? GROQ_TIMEOUTS.DEFAULT;
1527
1610
  const controller = new AbortController();
1528
1611
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1529
1612
  this.pendingAbortControllers.add(controller);
@@ -1546,7 +1629,7 @@ var GroqHttpTransport = class {
1546
1629
  buildHeaders() {
1547
1630
  return {
1548
1631
  [HTTP_HEADERS.CONTENT_TYPE]: HTTP_HEADERS.APPLICATION_JSON,
1549
- [HTTP_HEADERS.AUTHORIZATION]: `${HTTP_HEADERS.BEARER_PREFIX}${this.config.apiKey}`
1632
+ [HTTP_HEADERS.AUTHORIZATION]: `${HTTP_HEADERS.BEARER_PREFIX}${this.getConfig().apiKey}`
1550
1633
  };
1551
1634
  }
1552
1635
  async throwFromErrorResponse(response) {
@@ -1648,21 +1731,40 @@ var CompositeExecutor = class {
1648
1731
 
1649
1732
  // src/providers/groq/groq-http-client.ts
1650
1733
  var GroqHttpClient = class {
1734
+ configStore;
1651
1735
  transport;
1652
1736
  executor;
1653
- constructor(config, deps = {}) {
1654
- this.transport = deps.transport ?? new GroqHttpTransport(config);
1737
+ constructor(configOrDeps, maybeDeps = {}) {
1738
+ const isConfig = typeof configOrDeps === "object" && configOrDeps !== null && "apiKey" in configOrDeps && typeof configOrDeps.apiKey === "string";
1739
+ const deps = isConfig ? maybeDeps : configOrDeps ?? {};
1740
+ this.configStore = deps.configStore ?? new GroqConfigStore();
1741
+ if (isConfig) {
1742
+ this.configStore.initialize(configOrDeps);
1743
+ }
1744
+ this.transport = deps.transport ?? new GroqHttpTransport(this.configStore);
1655
1745
  const retryManager2 = deps.retryManager ?? new RetryManager();
1656
1746
  const queue = deps.queue ?? new RequestQueue();
1657
1747
  const deduplicator = deps.deduplicator ?? new RequestDeduplicator();
1658
1748
  this.executor = new CompositeExecutor([
1659
1749
  new DedupeExecutor(deduplicator, {
1660
- generateKey: (request) => deduplicatorKey(request, config.apiKey)
1750
+ generateKey: (request) => deduplicatorKey(request, this.configStore.isInitialized() ? this.configStore.get().apiKey : "")
1661
1751
  }),
1662
1752
  new RetryExecutor(retryManager2),
1663
1753
  new QueueExecutor(queue, QUEUE_PRIORITY_USER_REQUEST)
1664
1754
  ]);
1665
1755
  }
1756
+ initialize(config) {
1757
+ this.configStore.initialize(config);
1758
+ }
1759
+ isInitialized() {
1760
+ return this.configStore.isInitialized();
1761
+ }
1762
+ getApiKey() {
1763
+ return this.configStore.get().apiKey;
1764
+ }
1765
+ setApiKey(apiKey) {
1766
+ this.configStore.setApiKey(apiKey);
1767
+ }
1666
1768
  cancelPendingRequests() {
1667
1769
  this.transport.cancelPendingRequests();
1668
1770
  }
@@ -1675,6 +1777,10 @@ var GroqHttpClient = class {
1675
1777
  streamChatCompletion(request, options) {
1676
1778
  return this.transport.streamChatCompletion(request, options);
1677
1779
  }
1780
+ reset() {
1781
+ this.transport.cancelPendingRequests();
1782
+ this.configStore.reset();
1783
+ }
1678
1784
  };
1679
1785
  function deduplicatorKey(request, apiKey) {
1680
1786
  const { model, messages, stream: _stream, ...rest } = request;
@@ -2560,39 +2666,810 @@ var FalProvider = class extends BaseProvider {
2560
2666
  }
2561
2667
  };
2562
2668
 
2563
- // src/providers/pruna/pruna-generate.ts
2564
- var prunaCorePromise = null;
2565
- async function loadPrunaCore() {
2566
- if (!prunaCorePromise) {
2567
- const specifier = "@umituz/pruna-provider/core";
2568
- prunaCorePromise = import(specifier);
2669
+ // src/providers/pruna/client/core/constants/limits.ts
2670
+ var DEFAULT_ASPECT_RATIO = "16:9";
2671
+ var P_VIDEO_DEFAULTS = {
2672
+ duration: 5,
2673
+ resolution: "720p",
2674
+ fps: 24,
2675
+ draft: false,
2676
+ promptUpsampling: true
2677
+ };
2678
+ var POLL_DEFAULTS = {
2679
+ intervalMs: 3e3,
2680
+ maxAttempts: 120,
2681
+ maxIntervalMs: 1e4,
2682
+ backoffBase: 1.5,
2683
+ jitterPercent: 0.1
2684
+ };
2685
+ var DEFAULT_DEDUPLICATOR_TTL_MS = 3e4;
2686
+ var DEDUPLICATOR_CLEANUP_GRACE_MS = 100;
2687
+
2688
+ // src/providers/pruna/client/core/constants/network.ts
2689
+ var HTTPS_PREFIX = "https://";
2690
+ var HTTP_PREFIX = "http://";
2691
+ var BASE64_MARKER = "base64,";
2692
+ var PRUNA_BASE_URL = "https://api.pruna.ai";
2693
+ var PRUNA_PREDICTIONS_URL = `${PRUNA_BASE_URL}/v1/predictions`;
2694
+ var PRUNA_FILES_URL = `${PRUNA_BASE_URL}/v1/files`;
2695
+ var AUTH_FAILURE_STATUSES = [401, 403];
2696
+ var MIN_API_KEY_LENGTH = 1;
2697
+ var MAX_API_KEY_LENGTH = 256;
2698
+
2699
+ // src/providers/pruna/client/core/constants/mime-types.ts
2700
+ var MIME_PNG = "image/png";
2701
+ var MIME_JPEG = "image/jpeg";
2702
+ var MIME_WEBP = "image/webp";
2703
+ var DEFAULT_IMAGE_MIME = MIME_PNG;
2704
+ var MIME_FILE_EXTENSION = {
2705
+ [MIME_PNG]: "png",
2706
+ [MIME_JPEG]: "jpg",
2707
+ [MIME_WEBP]: "webp"
2708
+ };
2709
+ var IMAGE_SIGNATURES = {
2710
+ PNG: [137, 80],
2711
+ JPEG: [255, 216],
2712
+ WEBP_RIFF: [82, 73]
2713
+ };
2714
+
2715
+ // src/providers/pruna/client/core/errors/cancellation-error.ts
2716
+ var CANCELLATION_MESSAGE = "Request cancelled by user";
2717
+ var CancellationError = class extends Error {
2718
+ constructor(message = CANCELLATION_MESSAGE) {
2719
+ super(message);
2720
+ this.name = "CancellationError";
2721
+ }
2722
+ };
2723
+
2724
+ // src/providers/pruna/client/core/errors/authentication-error.ts
2725
+ var AUTHENTICATION_MESSAGE = "Authentication failed. Please check your API key.";
2726
+ var AuthenticationError = class extends Error {
2727
+ constructor(message = AUTHENTICATION_MESSAGE) {
2728
+ super(message);
2729
+ this.name = "AuthenticationError";
2569
2730
  }
2570
- return prunaCorePromise;
2731
+ };
2732
+
2733
+ // src/providers/pruna/client/core/errors/timeout-error.ts
2734
+ var TIMEOUT_MESSAGE = "Generation timed out. Maximum polling attempts reached.";
2735
+ var TimeoutError = class extends Error {
2736
+ constructor(message = TIMEOUT_MESSAGE) {
2737
+ super(message);
2738
+ this.name = "TimeoutError";
2739
+ }
2740
+ };
2741
+
2742
+ // src/providers/pruna/client/core/errors/validation-error.ts
2743
+ var ValidationError2 = class extends Error {
2744
+ field;
2745
+ reason;
2746
+ receivedValue;
2747
+ constructor(context) {
2748
+ super(`Invalid ${context.field}: ${context.reason}`);
2749
+ this.name = "ValidationError";
2750
+ this.field = context.field;
2751
+ this.reason = context.reason;
2752
+ this.receivedValue = context.receivedValue;
2753
+ }
2754
+ };
2755
+
2756
+ // src/providers/pruna/client/core/utils/base64.ts
2757
+ var globalScope = globalThis;
2758
+ var hasBuffer = typeof globalScope.Buffer?.from === "function";
2759
+ function base64ToBytes(raw) {
2760
+ if (hasBuffer) {
2761
+ const bufferFactory = globalScope.Buffer;
2762
+ if (!bufferFactory) {
2763
+ throw new Error("Buffer unavailable despite detection flag");
2764
+ }
2765
+ const buf = bufferFactory.from(raw, "base64");
2766
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
2767
+ }
2768
+ const decoded = atob(raw);
2769
+ return Uint8Array.from(decoded, (char) => char.charCodeAt(0));
2571
2770
  }
2572
- async function* generateWithPruna(request, apiKey, signal, onProgress) {
2573
- const prunaCore = await loadPrunaCore();
2574
- if (prunaCore.generateWithPruna) {
2575
- yield* prunaCore.generateWithPruna(request, apiKey, signal, onProgress);
2576
- return;
2577
- }
2578
- if (!prunaCore.submitPrediction || !prunaCore.pollForResult) {
2579
- throw new Error("@umituz/pruna-provider/core does not export required functions");
2580
- }
2581
- const modelId = request.model || (request.image ? "p-image-edit" : "p-image");
2582
- const prediction = await prunaCore.submitPrediction(modelId, request, apiKey, signal, onProgress);
2583
- if (prediction.output && typeof prediction.output === "object" && "url" in prediction.output) {
2584
- yield {
2585
- stage: "predicting",
2586
- result: { url: prediction.output.url, model: modelId }
2771
+
2772
+ // src/providers/pruna/client/core/utils/mime-detector.ts
2773
+ var MIN_SIGNATURE_BYTES = 2;
2774
+ function matchesSignature(bytes, signature) {
2775
+ if (bytes.length < signature.length) return false;
2776
+ return signature.every((value, index) => bytes[index] === value);
2777
+ }
2778
+ function detectImageMime(bytes) {
2779
+ if (bytes.length < MIN_SIGNATURE_BYTES) return DEFAULT_IMAGE_MIME;
2780
+ if (matchesSignature(bytes, IMAGE_SIGNATURES.PNG)) return MIME_PNG;
2781
+ if (matchesSignature(bytes, IMAGE_SIGNATURES.JPEG)) return MIME_JPEG;
2782
+ if (matchesSignature(bytes, IMAGE_SIGNATURES.WEBP_RIFF)) return MIME_WEBP;
2783
+ return DEFAULT_IMAGE_MIME;
2784
+ }
2785
+
2786
+ // src/providers/pruna/client/core/utils/api-error-parser.ts
2787
+ async function parseApiError(response, fallback) {
2788
+ const payload = await response.json().catch(() => ({ message: response.statusText }));
2789
+ const message = payload.message ?? `${fallback}: ${response.status}`;
2790
+ const error = new Error(message);
2791
+ error.statusCode = response.status;
2792
+ return error;
2793
+ }
2794
+
2795
+ // src/providers/pruna/client/core/protocol/http-status.ts
2796
+ function isAuthFailureStatus(status) {
2797
+ return AUTH_FAILURE_STATUSES.includes(status);
2798
+ }
2799
+
2800
+ // src/providers/pruna/client/core/protocol/job-status.ts
2801
+ var TERMINAL_SUCCESS_STATUSES = ["succeeded", "completed"];
2802
+ var TERMINAL_FAILURE_STATUSES = ["failed"];
2803
+ function isTerminalSuccessStatus(status) {
2804
+ if (!status) return false;
2805
+ return TERMINAL_SUCCESS_STATUSES.includes(status);
2806
+ }
2807
+ function isTerminalFailureStatus(status) {
2808
+ if (!status) return false;
2809
+ return TERMINAL_FAILURE_STATUSES.includes(status);
2810
+ }
2811
+
2812
+ // src/providers/pruna/client/core/protocol/abort-signal-classifier.ts
2813
+ function isAbortError(err) {
2814
+ if (err instanceof DOMException && err.code === DOMException.ABORT_ERR) return true;
2815
+ if (err instanceof Error && err.name === "AbortError") return true;
2816
+ return false;
2817
+ }
2818
+
2819
+ // src/providers/pruna/client/core/protocol/pruna-url-builder.ts
2820
+ function isAbsoluteHttpUrl(value) {
2821
+ return value.startsWith(HTTPS_PREFIX) || value.startsWith(HTTP_PREFIX);
2822
+ }
2823
+ function isHttpsUrl(value) {
2824
+ return value.startsWith(HTTPS_PREFIX);
2825
+ }
2826
+ function buildPrunaApiUrl(pathOrUrl) {
2827
+ if (isAbsoluteHttpUrl(pathOrUrl)) return pathOrUrl;
2828
+ return `${PRUNA_BASE_URL}${pathOrUrl}`;
2829
+ }
2830
+ function buildFileUrlById(fileId) {
2831
+ return `${PRUNA_FILES_URL}/${fileId}`;
2832
+ }
2833
+
2834
+ // src/providers/pruna/client/core/math/exponential-backoff.ts
2835
+ function calculateExponentialBackoff2(inputs) {
2836
+ const {
2837
+ attempt,
2838
+ baseIntervalMs,
2839
+ maxIntervalMs = POLL_DEFAULTS.maxIntervalMs,
2840
+ jitterPercent = POLL_DEFAULTS.jitterPercent,
2841
+ growthBase = POLL_DEFAULTS.backoffBase
2842
+ } = inputs;
2843
+ const safeAttempt = Math.max(0, attempt);
2844
+ const safeBase = Math.max(0, baseIntervalMs);
2845
+ const exponentialDelay = Math.min(safeBase * Math.pow(growthBase, safeAttempt), maxIntervalMs);
2846
+ const jitter = exponentialDelay * jitterPercent * Math.random();
2847
+ return exponentialDelay + jitter;
2848
+ }
2849
+
2850
+ // src/providers/pruna/client/core/utils/uri-resolver.ts
2851
+ var uriCache = /* @__PURE__ */ new WeakMap();
2852
+ function stripBase64Prefix(image) {
2853
+ if (isHttpsUrl(image)) return image;
2854
+ const base64Index = image.indexOf(BASE64_MARKER);
2855
+ return base64Index !== -1 ? image.slice(base64Index + BASE64_MARKER.length) : image;
2856
+ }
2857
+ function readOutput(output) {
2858
+ if (typeof output === "string") return output;
2859
+ if (Array.isArray(output) && output.length > 0) return output[0] ?? null;
2860
+ if (output !== null && typeof output === "object" && "url" in output) {
2861
+ return output.url;
2862
+ }
2863
+ return null;
2864
+ }
2865
+ function extractUri(data) {
2866
+ const cached = uriCache.get(data);
2867
+ if (cached !== void 0) return cached;
2868
+ if (!isTerminalSuccessStatus(data.status)) {
2869
+ uriCache.set(data, null);
2870
+ return null;
2871
+ }
2872
+ const result = data.generation_url ?? readOutput(data.output) ?? data.video_url ?? data.data ?? null;
2873
+ uriCache.set(data, result);
2874
+ return result;
2875
+ }
2876
+ function resolveUri(uri) {
2877
+ return uri.startsWith("/") ? `${PRUNA_BASE_URL}${uri}` : uri;
2878
+ }
2879
+
2880
+ // src/providers/pruna/client/core/services/pruna-client.service.ts
2881
+ function buildFileExtension(mime) {
2882
+ return MIME_FILE_EXTENSION[mime] ?? "bin";
2883
+ }
2884
+ async function uploadImage(base64OrUrl, apiKey, onProgress) {
2885
+ if (isHttpsUrl(base64OrUrl)) return base64OrUrl;
2886
+ if (base64OrUrl.startsWith(HTTP_PREFIX)) {
2887
+ throw new Error("Only HTTPS URLs are supported for security.");
2888
+ }
2889
+ onProgress?.("uploading");
2890
+ const raw = stripBase64Prefix(base64OrUrl);
2891
+ let bytes;
2892
+ try {
2893
+ bytes = base64ToBytes(raw);
2894
+ } catch {
2895
+ throw new Error("Invalid image format. Provide base64 or a valid HTTPS URL.");
2896
+ }
2897
+ const mime = detectImageMime(bytes);
2898
+ const buffer = bytes.buffer.slice(
2899
+ bytes.byteOffset,
2900
+ bytes.byteOffset + bytes.byteLength
2901
+ );
2902
+ const blob = new Blob([buffer], { type: mime });
2903
+ const formData = new FormData();
2904
+ formData.append("content", blob, `upload.${buildFileExtension(mime)}`);
2905
+ const res = await fetch(PRUNA_FILES_URL, {
2906
+ method: "POST",
2907
+ headers: { apikey: apiKey },
2908
+ body: formData
2909
+ });
2910
+ if (!res.ok) {
2911
+ throw await parseApiError(res, "File upload error");
2912
+ }
2913
+ const data = await res.json();
2914
+ return data.urls?.get ?? buildFileUrlById(data.id ?? "");
2915
+ }
2916
+ async function submitPrediction(model, input, apiKey, signal, onProgress) {
2917
+ onProgress?.("predicting");
2918
+ const res = await fetch(PRUNA_PREDICTIONS_URL, {
2919
+ method: "POST",
2920
+ headers: {
2921
+ apikey: apiKey,
2922
+ Model: model,
2923
+ "Try-Sync": "true",
2924
+ "Content-Type": "application/json"
2925
+ },
2926
+ body: JSON.stringify({ input }),
2927
+ signal
2928
+ });
2929
+ if (!res.ok) {
2930
+ throw await parseApiError(res, "API error");
2931
+ }
2932
+ return res.json();
2933
+ }
2934
+ var GenerationFailedError = class extends Error {
2935
+ constructor(message) {
2936
+ super(message);
2937
+ this.name = "GenerationFailedError";
2938
+ }
2939
+ };
2940
+ async function attemptPoll(fullUrl, apiKey, signal) {
2941
+ let res;
2942
+ try {
2943
+ res = await fetch(fullUrl, { headers: { apikey: apiKey }, signal });
2944
+ } catch (err) {
2945
+ if (isAbortError(err)) throw new CancellationError();
2946
+ return { outcome: "transient-failure" };
2947
+ }
2948
+ if (!res.ok) {
2949
+ if (isAuthFailureStatus(res.status)) throw new AuthenticationError();
2950
+ return { outcome: "transient-failure" };
2951
+ }
2952
+ let data;
2953
+ try {
2954
+ data = await res.json();
2955
+ } catch {
2956
+ return { outcome: "transient-failure" };
2957
+ }
2958
+ if (isTerminalSuccessStatus(data.status)) {
2959
+ const uri = extractUri(data);
2960
+ if (uri) return { outcome: "succeeded", uri: resolveUri(uri) };
2961
+ return { outcome: "transient-failure" };
2962
+ }
2963
+ if (isTerminalFailureStatus(data.status)) {
2964
+ throw new GenerationFailedError(data.error ?? "Generation failed during processing.");
2965
+ }
2966
+ return { outcome: "continue" };
2967
+ }
2968
+ async function pollForResult(pollUrl, apiKey, maxAttempts = POLL_DEFAULTS.maxAttempts, intervalMs = POLL_DEFAULTS.intervalMs, signal, onProgress) {
2969
+ const fullUrl = isAbsoluteHttpUrl(pollUrl) ? pollUrl : buildPrunaApiUrl(pollUrl);
2970
+ const controller = new AbortController();
2971
+ const onExternalAbort = () => controller.abort();
2972
+ signal?.addEventListener("abort", onExternalAbort);
2973
+ try {
2974
+ for (let i = 0; i < maxAttempts; i++) {
2975
+ if (signal?.aborted) throw new CancellationError();
2976
+ const delay = calculateExponentialBackoff2({ attempt: i, baseIntervalMs: intervalMs });
2977
+ await new Promise((resolve) => setTimeout(resolve, delay));
2978
+ if (signal?.aborted) throw new CancellationError();
2979
+ onProgress?.("polling", i + 1);
2980
+ const result = await attemptPoll(fullUrl, apiKey, controller.signal);
2981
+ if (result.outcome === "succeeded" && result.uri) {
2982
+ return result.uri;
2983
+ }
2984
+ }
2985
+ throw new TimeoutError();
2986
+ } finally {
2987
+ signal?.removeEventListener("abort", onExternalAbort);
2988
+ controller.abort();
2989
+ }
2990
+ }
2991
+
2992
+ // src/providers/pruna/client/core/math/comparison-hash.ts
2993
+ function sortKeysDeep(value) {
2994
+ if (Array.isArray(value)) {
2995
+ return value.map(sortKeysDeep);
2996
+ }
2997
+ if (value !== null && typeof value === "object") {
2998
+ const record = value;
2999
+ return Object.keys(record).sort().reduce((acc, key) => {
3000
+ acc[key] = sortKeysDeep(record[key]);
3001
+ return acc;
3002
+ }, {});
3003
+ }
3004
+ return value;
3005
+ }
3006
+ function hashValue(value) {
3007
+ if (value === null || typeof value !== "object") {
3008
+ return String(value);
3009
+ }
3010
+ try {
3011
+ return JSON.stringify(sortKeysDeep(value));
3012
+ } catch {
3013
+ return String(value);
3014
+ }
3015
+ }
3016
+ function compositeHash(...values) {
3017
+ return values.map(hashValue).join("|");
3018
+ }
3019
+
3020
+ // src/providers/pruna/client/core/math/cache-key-derivation.ts
3021
+ function deriveGenerationCacheKey(apiKey, input) {
3022
+ return `${apiKey}:${compositeHash(input)}`;
3023
+ }
3024
+
3025
+ // src/providers/pruna/client/core/validation/api-key-validator.ts
3026
+ function assertValidApiKey(apiKey) {
3027
+ if (typeof apiKey !== "string") {
3028
+ throw new ValidationError2({
3029
+ field: "apiKey",
3030
+ reason: "must be a string",
3031
+ receivedValue: typeof apiKey
3032
+ });
3033
+ }
3034
+ if (apiKey.length < MIN_API_KEY_LENGTH) {
3035
+ throw new ValidationError2({
3036
+ field: "apiKey",
3037
+ reason: "must not be empty",
3038
+ receivedValue: apiKey
3039
+ });
3040
+ }
3041
+ if (apiKey.length > MAX_API_KEY_LENGTH) {
3042
+ throw new ValidationError2({
3043
+ field: "apiKey",
3044
+ reason: `exceeds maximum length of ${MAX_API_KEY_LENGTH}`,
3045
+ receivedValue: apiKey.length
3046
+ });
3047
+ }
3048
+ }
3049
+
3050
+ // src/providers/pruna/client/core/validation/image-source-validator.ts
3051
+ function classifyImageSource(source) {
3052
+ if (source.startsWith(HTTPS_PREFIX)) return "https-url";
3053
+ if (source.startsWith(HTTP_PREFIX)) return "http-url";
3054
+ if (source.includes(BASE64_MARKER) || /^[A-Za-z0-9+/=]+$/.test(source)) return "base64";
3055
+ return "unknown";
3056
+ }
3057
+ function assertSecureImageSource(image, fieldName = "image") {
3058
+ if (typeof image !== "string") {
3059
+ throw new ValidationError2({
3060
+ field: fieldName,
3061
+ reason: "must be a string",
3062
+ receivedValue: typeof image
3063
+ });
3064
+ }
3065
+ const kind = classifyImageSource(image);
3066
+ if (kind === "http-url") {
3067
+ throw new ValidationError2({
3068
+ field: fieldName,
3069
+ reason: "HTTP URLs are not supported; use HTTPS or a base64 data URI",
3070
+ receivedValue: image
3071
+ });
3072
+ }
3073
+ if (kind === "unknown") {
3074
+ throw new ValidationError2({
3075
+ field: fieldName,
3076
+ reason: "must be an HTTPS URL or a base64 data URI",
3077
+ receivedValue: typeof image
3078
+ });
3079
+ }
3080
+ }
3081
+
3082
+ // src/providers/pruna/client/core/validation/aspect-ratio-validator.ts
3083
+ var SUPPORTED_ASPECT_RATIOS = [
3084
+ "16:9",
3085
+ "9:16",
3086
+ "1:1",
3087
+ "4:3",
3088
+ "3:4",
3089
+ "3:2",
3090
+ "2:3"
3091
+ ];
3092
+ function isSupportedAspectRatio(value) {
3093
+ return typeof value === "string" && SUPPORTED_ASPECT_RATIOS.includes(value);
3094
+ }
3095
+ function assertValidAspectRatio(value, fieldName = "aspect_ratio") {
3096
+ if (value === void 0 || value === null) return;
3097
+ if (!isSupportedAspectRatio(value)) {
3098
+ throw new ValidationError2({
3099
+ field: fieldName,
3100
+ reason: `must be one of ${SUPPORTED_ASPECT_RATIOS.join(", ")}`,
3101
+ receivedValue: value
3102
+ });
3103
+ }
3104
+ }
3105
+
3106
+ // src/providers/pruna/client/core/validation/resolution-validator.ts
3107
+ var SUPPORTED_RESOLUTIONS = ["720p", "1080p"];
3108
+ function isSupportedResolution(value) {
3109
+ return typeof value === "string" && SUPPORTED_RESOLUTIONS.includes(value);
3110
+ }
3111
+ function assertValidResolution(value, fieldName = "resolution") {
3112
+ if (value === void 0 || value === null) return;
3113
+ if (!isSupportedResolution(value)) {
3114
+ throw new ValidationError2({
3115
+ field: fieldName,
3116
+ reason: `must be one of ${SUPPORTED_RESOLUTIONS.join(", ")}`,
3117
+ receivedValue: value
3118
+ });
3119
+ }
3120
+ }
3121
+
3122
+ // src/providers/pruna/client/core/validation/duration-validator.ts
3123
+ var MIN_VIDEO_DURATION_SECONDS = 1;
3124
+ var MAX_VIDEO_DURATION_SECONDS = 30;
3125
+ function isValidDuration(value) {
3126
+ return typeof value === "number" && Number.isFinite(value) && value >= MIN_VIDEO_DURATION_SECONDS && value <= MAX_VIDEO_DURATION_SECONDS && Number.isInteger(value);
3127
+ }
3128
+ function assertValidDuration(value, fieldName = "duration") {
3129
+ if (value === void 0 || value === null) return;
3130
+ if (!isValidDuration(value)) {
3131
+ throw new ValidationError2({
3132
+ field: fieldName,
3133
+ reason: `must be an integer between ${MIN_VIDEO_DURATION_SECONDS} and ${MAX_VIDEO_DURATION_SECONDS} seconds`,
3134
+ receivedValue: value
3135
+ });
3136
+ }
3137
+ }
3138
+
3139
+ // src/providers/pruna/client/core/validation/prompt-validator.ts
3140
+ var MIN_PROMPT_LENGTH = 1;
3141
+ var MAX_PROMPT_LENGTH = 4e3;
3142
+ function assertValidPrompt(value, fieldName = "prompt") {
3143
+ if (typeof value !== "string") {
3144
+ throw new ValidationError2({
3145
+ field: fieldName,
3146
+ reason: "must be a string",
3147
+ receivedValue: typeof value
3148
+ });
3149
+ }
3150
+ if (value.trim().length < MIN_PROMPT_LENGTH) {
3151
+ throw new ValidationError2({
3152
+ field: fieldName,
3153
+ reason: "must not be empty or whitespace-only",
3154
+ receivedValue: value
3155
+ });
3156
+ }
3157
+ if (value.length > MAX_PROMPT_LENGTH) {
3158
+ throw new ValidationError2({
3159
+ field: fieldName,
3160
+ reason: `exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`,
3161
+ receivedValue: value.length
3162
+ });
3163
+ }
3164
+ }
3165
+
3166
+ // src/providers/pruna/client/core/validation/seed-validator.ts
3167
+ var MIN_SEED = -2147483648;
3168
+ var MAX_SEED = 2147483647;
3169
+ function isValidSeed(value) {
3170
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= MIN_SEED && value <= MAX_SEED;
3171
+ }
3172
+ function assertValidSeed(value, fieldName = "seed") {
3173
+ if (value === void 0 || value === null) return;
3174
+ if (!isValidSeed(value)) {
3175
+ throw new ValidationError2({
3176
+ field: fieldName,
3177
+ reason: `must be an integer in [${MIN_SEED}, ${MAX_SEED}]`,
3178
+ receivedValue: value
3179
+ });
3180
+ }
3181
+ }
3182
+
3183
+ // src/providers/pruna/client/core/protocol/input-type-guard.ts
3184
+ function isTextToImageInput(input) {
3185
+ return input.model === "p-image";
3186
+ }
3187
+ function isImageToImageInput(input) {
3188
+ return input.model === "p-image-edit";
3189
+ }
3190
+ function isImageToVideoInput(input) {
3191
+ return input.model === "p-video";
3192
+ }
3193
+
3194
+ // src/providers/pruna/client/core/utils/request-deduplicator.ts
3195
+ var RequestDeduplicator2 = class {
3196
+ pendingRequests = /* @__PURE__ */ new Map();
3197
+ ttl;
3198
+ constructor(ttl = DEFAULT_DEDUPLICATOR_TTL_MS) {
3199
+ this.ttl = ttl;
3200
+ }
3201
+ execute(key, fn) {
3202
+ const existing = this.pendingRequests.get(key);
3203
+ if (existing) {
3204
+ if (Date.now() - existing.timestamp < this.ttl) {
3205
+ return existing.promise;
3206
+ }
3207
+ this.pendingRequests.delete(key);
3208
+ }
3209
+ const promise = fn().finally(() => {
3210
+ setTimeout(() => {
3211
+ this.pendingRequests.delete(key);
3212
+ }, DEDUPLICATOR_CLEANUP_GRACE_MS);
3213
+ });
3214
+ this.pendingRequests.set(key, { promise, timestamp: Date.now() });
3215
+ return promise;
3216
+ }
3217
+ clear() {
3218
+ this.pendingRequests.clear();
3219
+ }
3220
+ size() {
3221
+ this.purgeExpired();
3222
+ return this.pendingRequests.size;
3223
+ }
3224
+ purgeExpired() {
3225
+ const now = Date.now();
3226
+ for (const [key, value] of this.pendingRequests) {
3227
+ if (now - value.timestamp >= this.ttl) {
3228
+ this.pendingRequests.delete(key);
3229
+ }
3230
+ }
3231
+ }
3232
+ };
3233
+ var globalDeduplicator = new RequestDeduplicator2();
3234
+
3235
+ // src/providers/pruna/client/generation/builders/text-to-image.builder.ts
3236
+ var TextToImageInputBuilder = class {
3237
+ model = "p-image";
3238
+ build(input, _context) {
3239
+ const payload = {
3240
+ prompt: input.prompt,
3241
+ aspect_ratio: input.aspect_ratio ?? DEFAULT_ASPECT_RATIO
3242
+ };
3243
+ if (input.seed !== void 0) {
3244
+ payload.seed = input.seed;
3245
+ }
3246
+ return Promise.resolve(payload);
3247
+ }
3248
+ };
3249
+
3250
+ // src/providers/pruna/client/generation/builders/image-to-image.builder.ts
3251
+ var ImageToImageInputBuilder = class {
3252
+ model = "p-image-edit";
3253
+ build(input, _context) {
3254
+ const payload = {
3255
+ images: [stripBase64Prefix(input.image)],
3256
+ prompt: input.prompt,
3257
+ aspect_ratio: input.aspect_ratio ?? DEFAULT_ASPECT_RATIO
2587
3258
  };
2588
- return;
3259
+ if (input.seed !== void 0) {
3260
+ payload.seed = input.seed;
3261
+ }
3262
+ return Promise.resolve(payload);
3263
+ }
3264
+ };
3265
+
3266
+ // src/providers/pruna/client/generation/builders/image-to-video.builder.ts
3267
+ var ImageToVideoInputBuilder = class {
3268
+ model = "p-video";
3269
+ async build(input, context) {
3270
+ if (context.signal?.aborted) {
3271
+ throw new CancellationError();
3272
+ }
3273
+ const fileUrl = await uploadImage(
3274
+ input.image,
3275
+ context.apiKey,
3276
+ context.onProgress
3277
+ );
3278
+ return {
3279
+ image: fileUrl,
3280
+ prompt: input.prompt,
3281
+ duration: input.duration ?? P_VIDEO_DEFAULTS.duration,
3282
+ resolution: input.resolution ?? P_VIDEO_DEFAULTS.resolution,
3283
+ fps: P_VIDEO_DEFAULTS.fps,
3284
+ draft: input.draft ?? P_VIDEO_DEFAULTS.draft,
3285
+ aspect_ratio: input.aspect_ratio ?? DEFAULT_ASPECT_RATIO,
3286
+ prompt_upsampling: P_VIDEO_DEFAULTS.promptUpsampling
3287
+ };
3288
+ }
3289
+ };
3290
+
3291
+ // src/providers/pruna/client/generation/builders/model-input-builder.ts
3292
+ var ModelInputBuilderRegistry = class {
3293
+ builders = /* @__PURE__ */ new Map();
3294
+ register(builder) {
3295
+ this.builders.set(builder.model, builder);
3296
+ return this;
3297
+ }
3298
+ resolve(model) {
3299
+ const builder = this.builders.get(model);
3300
+ if (!builder) {
3301
+ throw new Error(`No input builder registered for model: ${model}`);
3302
+ }
3303
+ return builder;
3304
+ }
3305
+ };
3306
+
3307
+ // src/providers/pruna/client/generation/builders/builder-factory.ts
3308
+ function createDefaultBuilderRegistry() {
3309
+ return new ModelInputBuilderRegistry().register(new TextToImageInputBuilder()).register(new ImageToImageInputBuilder()).register(new ImageToVideoInputBuilder());
3310
+ }
3311
+
3312
+ // src/providers/pruna/client/generation/services/generation.service.ts
3313
+ var builderRegistry = createDefaultBuilderRegistry();
3314
+ function validateGenerateInput(input) {
3315
+ assertValidPrompt(input.prompt, `${input.model}.prompt`);
3316
+ assertValidAspectRatio(input.aspect_ratio, `${input.model}.aspect_ratio`);
3317
+ if (isTextToImageInput(input)) {
3318
+ assertValidSeed(input.seed, `${input.model}.seed`);
3319
+ } else if (isImageToImageInput(input)) {
3320
+ assertSecureImageSource(input.image, `${input.model}.image`);
3321
+ assertValidSeed(input.seed, `${input.model}.seed`);
3322
+ } else if (isImageToVideoInput(input)) {
3323
+ assertSecureImageSource(input.image, `${input.model}.image`);
3324
+ assertValidDuration(input.duration, `${input.model}.duration`);
3325
+ assertValidResolution(input.resolution, `${input.model}.resolution`);
3326
+ }
3327
+ }
3328
+ async function generate(apiKey, input, options = {}) {
3329
+ assertValidApiKey(apiKey);
3330
+ validateGenerateInput(input);
3331
+ const { signal, onProgress, useCache = true } = options;
3332
+ if (useCache) {
3333
+ const cacheKey = deriveGenerationCacheKey(apiKey, input);
3334
+ return globalDeduplicator.execute(
3335
+ cacheKey,
3336
+ () => generateInternal(apiKey, input, { signal, onProgress })
3337
+ );
2589
3338
  }
2590
- const pollUrl = prediction.status_url || prediction.generation_url;
3339
+ return generateInternal(apiKey, input, { signal, onProgress });
3340
+ }
3341
+ async function generateInternal(apiKey, input, options) {
3342
+ const { signal, onProgress } = options;
3343
+ const builder = builderRegistry.resolve(input.model);
3344
+ const modelInput = await builder.build(input, { apiKey, signal, onProgress });
3345
+ const response = await submitPrediction(input.model, modelInput, apiKey, signal, onProgress);
3346
+ const syncUri = extractUri(response);
3347
+ if (syncUri) {
3348
+ return { url: resolveUri(syncUri), model: input.model };
3349
+ }
3350
+ const pollUrl = response.get_url ?? response.status_url;
2591
3351
  if (!pollUrl) {
2592
- throw new Error("Pruna prediction response missing polling URL");
3352
+ throw new Error("Pruna API returned no result and no polling URL.");
3353
+ }
3354
+ const url = await pollForResult(
3355
+ pollUrl,
3356
+ apiKey,
3357
+ POLL_DEFAULTS.maxAttempts,
3358
+ POLL_DEFAULTS.intervalMs,
3359
+ signal,
3360
+ onProgress
3361
+ );
3362
+ return { url, model: input.model };
3363
+ }
3364
+ async function generateImageThenVideo(apiKey, input, options = {}) {
3365
+ assertValidApiKey(apiKey);
3366
+ assertValidPrompt(input.prompt, "text-to-video.prompt");
3367
+ assertValidAspectRatio(input.aspect_ratio, "text-to-video.aspect_ratio");
3368
+ assertValidDuration(input.duration, "text-to-video.duration");
3369
+ assertValidResolution(input.resolution, "text-to-video.resolution");
3370
+ const { signal, onProgress } = options;
3371
+ const aspectRatio = input.aspect_ratio ?? DEFAULT_ASPECT_RATIO;
3372
+ onProgress?.("predicting", 1);
3373
+ const imageResult = await generate(
3374
+ apiKey,
3375
+ { model: "p-image", prompt: input.prompt, aspect_ratio: aspectRatio },
3376
+ { signal }
3377
+ );
3378
+ onProgress?.("predicting", 2);
3379
+ return generate(
3380
+ apiKey,
3381
+ {
3382
+ model: "p-video",
3383
+ prompt: input.prompt,
3384
+ image: imageResult.url,
3385
+ duration: input.duration ?? P_VIDEO_DEFAULTS.duration,
3386
+ resolution: input.resolution ?? P_VIDEO_DEFAULTS.resolution,
3387
+ aspect_ratio: aspectRatio,
3388
+ draft: input.draft ?? P_VIDEO_DEFAULTS.draft
3389
+ },
3390
+ { signal, onProgress }
3391
+ );
3392
+ }
3393
+
3394
+ // src/providers/pruna/pruna-generate.ts
3395
+ async function* generateWithPruna(request, apiKey, signal, onProgress) {
3396
+ const stages = [];
3397
+ let notify = null;
3398
+ let done = false;
3399
+ const wake = () => {
3400
+ const fn = notify;
3401
+ if (fn) {
3402
+ notify = null;
3403
+ fn();
3404
+ }
3405
+ };
3406
+ const handleProgress = (stage) => {
3407
+ stages.push({ stage });
3408
+ wake();
3409
+ };
3410
+ const promise = (async () => {
3411
+ try {
3412
+ const model = request.model ?? (request.image ? "p-image-edit" : "p-image");
3413
+ if (model === "p-video" && !request.image && request.prompt) {
3414
+ const videoRes = await generateImageThenVideo(
3415
+ apiKey,
3416
+ {
3417
+ prompt: request.prompt,
3418
+ duration: request.duration ?? 5,
3419
+ aspect_ratio: request.aspectRatio ?? request.aspect_ratio ?? "16:9"
3420
+ },
3421
+ { signal, onProgress: handleProgress }
3422
+ );
3423
+ stages.push({
3424
+ stage: "polling",
3425
+ result: { url: videoRes.url, model: "p-video" }
3426
+ });
3427
+ return;
3428
+ }
3429
+ const input = {
3430
+ model,
3431
+ prompt: request.prompt ?? "",
3432
+ aspect_ratio: request.aspectRatio ?? request.aspect_ratio ?? "1:1",
3433
+ ...request.image ? { image: request.image } : {},
3434
+ ...request.duration ? { duration: request.duration } : {},
3435
+ ...request.resolution ? { resolution: request.resolution } : {},
3436
+ ...request.seed !== void 0 ? { seed: request.seed } : {}
3437
+ };
3438
+ const res = await generate(apiKey, input, {
3439
+ signal,
3440
+ onProgress: handleProgress
3441
+ });
3442
+ stages.push({
3443
+ stage: "polling",
3444
+ result: { url: res.url, model: res.model }
3445
+ });
3446
+ } catch (err) {
3447
+ stages.push({
3448
+ stage: "processing",
3449
+ error: err instanceof Error ? err : new Error(String(err))
3450
+ });
3451
+ } finally {
3452
+ done = true;
3453
+ wake();
3454
+ }
3455
+ })();
3456
+ while (!done || stages.length > 0) {
3457
+ if (stages.length === 0 && !done) {
3458
+ await new Promise((resolve) => {
3459
+ notify = resolve;
3460
+ });
3461
+ notify = null;
3462
+ }
3463
+ while (stages.length > 0) {
3464
+ const nextStage = stages.shift();
3465
+ if (!nextStage) continue;
3466
+ yield nextStage;
3467
+ if (nextStage.error) {
3468
+ throw nextStage.error;
3469
+ }
3470
+ }
2593
3471
  }
2594
- const resultUrl = await prunaCore.pollForResult(pollUrl, apiKey, 60, 2e3, signal, onProgress);
2595
- yield { stage: "polling", result: { url: resultUrl, model: modelId } };
3472
+ await promise;
2596
3473
  }
2597
3474
 
2598
3475
  // src/providers/pruna/pruna.provider.ts