@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.
@@ -115,6 +115,14 @@ var AIError = class _AIError extends Error {
115
115
  }
116
116
  };
117
117
 
118
+ // src/core/errors/groq-error.ts
119
+ var GroqError = class extends AIError {
120
+ constructor(type, message, cause, metadata = {}) {
121
+ super(type, message, cause, metadata);
122
+ this.name = "GroqError";
123
+ }
124
+ };
125
+
118
126
  // src/core/errors/error-classifier.ts
119
127
  var DEFAULT_RETRYABLE_TYPES = /* @__PURE__ */ new Set([
120
128
  "NETWORK_ERROR" /* NETWORK_ERROR */,
@@ -1339,6 +1347,27 @@ var RequestDeduplicator = class {
1339
1347
  }
1340
1348
  };
1341
1349
 
1350
+ // src/core/utils/api-key-validator.ts
1351
+ var DEFAULT_API_KEY_RULE = {
1352
+ minLength: 10
1353
+ };
1354
+ function validateApiKey(apiKey, rule = DEFAULT_API_KEY_RULE) {
1355
+ if (!apiKey) {
1356
+ return { valid: false, reason: "API key is required" };
1357
+ }
1358
+ const trimmed = apiKey.trim();
1359
+ if (trimmed.length === 0) {
1360
+ return { valid: false, reason: "API key cannot be empty" };
1361
+ }
1362
+ if (trimmed.length < rule.minLength) {
1363
+ return {
1364
+ valid: false,
1365
+ reason: `API key must be at least ${rule.minLength} characters`
1366
+ };
1367
+ }
1368
+ return { valid: true };
1369
+ }
1370
+
1342
1371
  // src/providers/groq/groq.constants.ts
1343
1372
  var GROQ_MODELS = {
1344
1373
  LLAMA_3_1_8B_INSTANT: "llama-3.1-8b-instant",
@@ -1355,11 +1384,13 @@ var DEFAULT_GENERATION_CONFIG = {
1355
1384
  topP: 1
1356
1385
  };
1357
1386
  var GROQ_BASE_URL = "https://api.groq.com/openai/v1";
1387
+ var DEFAULT_BASE_URL = GROQ_BASE_URL;
1358
1388
  var CHAT_COMPLETIONS_PATH = "/chat/completions";
1359
1389
  var GROQ_TIMEOUTS = {
1360
1390
  DEFAULT: 3e4,
1361
1391
  STREAMING: 6e4
1362
1392
  };
1393
+ var TIMEOUTS = GROQ_TIMEOUTS;
1363
1394
  var HTTP_HEADERS = {
1364
1395
  CONTENT_TYPE: "Content-Type",
1365
1396
  APPLICATION_JSON: "application/json",
@@ -1418,13 +1449,65 @@ var GroqChatRequestBuilder = class _GroqChatRequestBuilder {
1418
1449
  }
1419
1450
  };
1420
1451
 
1452
+ // src/providers/groq/groq-config-store.ts
1453
+ var GroqConfigStore = class {
1454
+ config = null;
1455
+ initialize(config) {
1456
+ const validation = validateApiKey(config.apiKey, { minLength: 1 });
1457
+ if (!validation.valid) {
1458
+ throw new GroqError("INVALID_API_KEY" /* INVALID_API_KEY */, validation.reason ?? "Invalid API key");
1459
+ }
1460
+ const normalized = {
1461
+ apiKey: config.apiKey.trim(),
1462
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
1463
+ timeoutMs: config.timeoutMs ?? TIMEOUTS.DEFAULT,
1464
+ ...config.models !== void 0 ? { models: config.models } : {},
1465
+ ...config.textModel !== void 0 ? { textModel: config.textModel } : {}
1466
+ };
1467
+ this.config = normalized;
1468
+ return normalized;
1469
+ }
1470
+ get() {
1471
+ if (!this.config) {
1472
+ throw new GroqError(
1473
+ "MISSING_CONFIG" /* MISSING_CONFIG */,
1474
+ "Client not initialized. Call configureProvider first."
1475
+ );
1476
+ }
1477
+ return this.config;
1478
+ }
1479
+ getOrNull() {
1480
+ return this.config;
1481
+ }
1482
+ isInitialized() {
1483
+ return this.config !== null;
1484
+ }
1485
+ setApiKey(apiKey) {
1486
+ const current = this.get();
1487
+ const validation = validateApiKey(apiKey, { minLength: 1 });
1488
+ if (!validation.valid) {
1489
+ throw new GroqError("INVALID_API_KEY" /* INVALID_API_KEY */, validation.reason ?? "Invalid API key");
1490
+ }
1491
+ this.config = { ...current, apiKey: apiKey.trim() };
1492
+ }
1493
+ reset() {
1494
+ this.config = null;
1495
+ }
1496
+ };
1497
+
1421
1498
  // src/providers/groq/groq-http-transport.ts
1422
1499
  var GroqHttpTransport = class {
1423
- constructor(config) {
1424
- this.config = config;
1500
+ constructor(configSource) {
1501
+ this.configSource = configSource;
1425
1502
  }
1426
- config;
1503
+ configSource;
1427
1504
  pendingAbortControllers = /* @__PURE__ */ new Set();
1505
+ getConfig() {
1506
+ if ("get" in this.configSource && typeof this.configSource.get === "function") {
1507
+ return this.configSource.get();
1508
+ }
1509
+ return this.configSource;
1510
+ }
1428
1511
  cancelPendingRequests() {
1429
1512
  for (const controller of this.pendingAbortControllers) {
1430
1513
  controller.abort();
@@ -1439,7 +1522,7 @@ var GroqHttpTransport = class {
1439
1522
  }
1440
1523
  async streamChatCompletion(request, options = {}) {
1441
1524
  const url = `${this.baseUrl()}${CHAT_COMPLETIONS_PATH}`;
1442
- const timeout = this.config.timeoutMs ?? GROQ_TIMEOUTS.STREAMING;
1525
+ const timeout = this.getConfig().timeoutMs ?? GROQ_TIMEOUTS.STREAMING;
1443
1526
  const controller = new AbortController();
1444
1527
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1445
1528
  const onExternalAbort = () => controller.abort();
@@ -1466,11 +1549,11 @@ var GroqHttpTransport = class {
1466
1549
  }
1467
1550
  }
1468
1551
  baseUrl() {
1469
- return this.config.baseUrl ?? GROQ_BASE_URL;
1552
+ return this.getConfig().baseUrl ?? GROQ_BASE_URL;
1470
1553
  }
1471
1554
  async request(endpoint, body) {
1472
1555
  const url = `${this.baseUrl()}${endpoint}`;
1473
- const timeout = this.config.timeoutMs ?? GROQ_TIMEOUTS.DEFAULT;
1556
+ const timeout = this.getConfig().timeoutMs ?? GROQ_TIMEOUTS.DEFAULT;
1474
1557
  const controller = new AbortController();
1475
1558
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1476
1559
  this.pendingAbortControllers.add(controller);
@@ -1493,7 +1576,7 @@ var GroqHttpTransport = class {
1493
1576
  buildHeaders() {
1494
1577
  return {
1495
1578
  [HTTP_HEADERS.CONTENT_TYPE]: HTTP_HEADERS.APPLICATION_JSON,
1496
- [HTTP_HEADERS.AUTHORIZATION]: `${HTTP_HEADERS.BEARER_PREFIX}${this.config.apiKey}`
1579
+ [HTTP_HEADERS.AUTHORIZATION]: `${HTTP_HEADERS.BEARER_PREFIX}${this.getConfig().apiKey}`
1497
1580
  };
1498
1581
  }
1499
1582
  async throwFromErrorResponse(response) {
@@ -1595,21 +1678,40 @@ var CompositeExecutor = class {
1595
1678
 
1596
1679
  // src/providers/groq/groq-http-client.ts
1597
1680
  var GroqHttpClient = class {
1681
+ configStore;
1598
1682
  transport;
1599
1683
  executor;
1600
- constructor(config, deps = {}) {
1601
- this.transport = deps.transport ?? new GroqHttpTransport(config);
1684
+ constructor(configOrDeps, maybeDeps = {}) {
1685
+ const isConfig = typeof configOrDeps === "object" && configOrDeps !== null && "apiKey" in configOrDeps && typeof configOrDeps.apiKey === "string";
1686
+ const deps = isConfig ? maybeDeps : configOrDeps ?? {};
1687
+ this.configStore = deps.configStore ?? new GroqConfigStore();
1688
+ if (isConfig) {
1689
+ this.configStore.initialize(configOrDeps);
1690
+ }
1691
+ this.transport = deps.transport ?? new GroqHttpTransport(this.configStore);
1602
1692
  const retryManager2 = deps.retryManager ?? new RetryManager();
1603
1693
  const queue = deps.queue ?? new RequestQueue();
1604
1694
  const deduplicator = deps.deduplicator ?? new RequestDeduplicator();
1605
1695
  this.executor = new CompositeExecutor([
1606
1696
  new DedupeExecutor(deduplicator, {
1607
- generateKey: (request) => deduplicatorKey(request, config.apiKey)
1697
+ generateKey: (request) => deduplicatorKey(request, this.configStore.isInitialized() ? this.configStore.get().apiKey : "")
1608
1698
  }),
1609
1699
  new RetryExecutor(retryManager2),
1610
1700
  new QueueExecutor(queue, QUEUE_PRIORITY_USER_REQUEST)
1611
1701
  ]);
1612
1702
  }
1703
+ initialize(config) {
1704
+ this.configStore.initialize(config);
1705
+ }
1706
+ isInitialized() {
1707
+ return this.configStore.isInitialized();
1708
+ }
1709
+ getApiKey() {
1710
+ return this.configStore.get().apiKey;
1711
+ }
1712
+ setApiKey(apiKey) {
1713
+ this.configStore.setApiKey(apiKey);
1714
+ }
1613
1715
  cancelPendingRequests() {
1614
1716
  this.transport.cancelPendingRequests();
1615
1717
  }
@@ -1622,6 +1724,10 @@ var GroqHttpClient = class {
1622
1724
  streamChatCompletion(request, options) {
1623
1725
  return this.transport.streamChatCompletion(request, options);
1624
1726
  }
1727
+ reset() {
1728
+ this.transport.cancelPendingRequests();
1729
+ this.configStore.reset();
1730
+ }
1625
1731
  };
1626
1732
  function deduplicatorKey(request, apiKey) {
1627
1733
  const { model, messages, stream: _stream, ...rest } = request;
@@ -2519,39 +2625,814 @@ var FalProvider = class extends BaseProvider {
2519
2625
  }
2520
2626
  };
2521
2627
 
2522
- // src/providers/pruna/pruna-generate.ts
2523
- var prunaCorePromise = null;
2524
- async function loadPrunaCore() {
2525
- if (!prunaCorePromise) {
2526
- const specifier = "@umituz/pruna-provider/core";
2527
- prunaCorePromise = import(specifier);
2628
+ // src/providers/pruna/client/core/constants/limits.ts
2629
+ var DEFAULT_ASPECT_RATIO = "16:9";
2630
+ var P_VIDEO_DEFAULTS = {
2631
+ duration: 5,
2632
+ resolution: "720p",
2633
+ fps: 24,
2634
+ draft: false,
2635
+ promptUpsampling: true
2636
+ };
2637
+ var POLL_DEFAULTS = {
2638
+ intervalMs: 3e3,
2639
+ maxAttempts: 120,
2640
+ maxIntervalMs: 1e4,
2641
+ backoffBase: 1.5,
2642
+ jitterPercent: 0.1
2643
+ };
2644
+ var DEFAULT_DEDUPLICATOR_TTL_MS = 3e4;
2645
+ var DEDUPLICATOR_CLEANUP_GRACE_MS = 100;
2646
+
2647
+ // src/providers/pruna/client/core/constants/network.ts
2648
+ var HTTPS_PREFIX = "https://";
2649
+ var HTTP_PREFIX = "http://";
2650
+ var BASE64_MARKER = "base64,";
2651
+ var PRUNA_BASE_URL = "https://api.pruna.ai";
2652
+ var PRUNA_PREDICTIONS_URL = `${PRUNA_BASE_URL}/v1/predictions`;
2653
+ var PRUNA_FILES_URL = `${PRUNA_BASE_URL}/v1/files`;
2654
+ var AUTH_FAILURE_STATUSES = [401, 403];
2655
+ var MIN_API_KEY_LENGTH = 1;
2656
+ var MAX_API_KEY_LENGTH = 256;
2657
+
2658
+ // src/providers/pruna/client/core/constants/mime-types.ts
2659
+ var MIME_PNG = "image/png";
2660
+ var MIME_JPEG = "image/jpeg";
2661
+ var MIME_WEBP = "image/webp";
2662
+ var DEFAULT_IMAGE_MIME = MIME_PNG;
2663
+ var MIME_FILE_EXTENSION = {
2664
+ [MIME_PNG]: "png",
2665
+ [MIME_JPEG]: "jpg",
2666
+ [MIME_WEBP]: "webp"
2667
+ };
2668
+ var IMAGE_SIGNATURES = {
2669
+ PNG: [137, 80],
2670
+ JPEG: [255, 216],
2671
+ WEBP_RIFF: [82, 73]
2672
+ };
2673
+
2674
+ // src/providers/pruna/client/core/errors/cancellation-error.ts
2675
+ var CANCELLATION_MESSAGE = "Request cancelled by user";
2676
+ var CancellationError = class extends Error {
2677
+ constructor(message = CANCELLATION_MESSAGE) {
2678
+ super(message);
2679
+ this.name = "CancellationError";
2528
2680
  }
2529
- return prunaCorePromise;
2681
+ };
2682
+ function isCancellationError(value) {
2683
+ return value instanceof CancellationError;
2530
2684
  }
2531
- async function* generateWithPruna(request, apiKey, signal, onProgress) {
2532
- const prunaCore = await loadPrunaCore();
2533
- if (prunaCore.generateWithPruna) {
2534
- yield* prunaCore.generateWithPruna(request, apiKey, signal, onProgress);
2535
- return;
2536
- }
2537
- if (!prunaCore.submitPrediction || !prunaCore.pollForResult) {
2538
- throw new Error("@umituz/pruna-provider/core does not export required functions");
2539
- }
2540
- const modelId = request.model || (request.image ? "p-image-edit" : "p-image");
2541
- const prediction = await prunaCore.submitPrediction(modelId, request, apiKey, signal, onProgress);
2542
- if (prediction.output && typeof prediction.output === "object" && "url" in prediction.output) {
2543
- yield {
2544
- stage: "predicting",
2545
- result: { url: prediction.output.url, model: modelId }
2685
+
2686
+ // src/providers/pruna/client/core/errors/authentication-error.ts
2687
+ var AUTHENTICATION_MESSAGE = "Authentication failed. Please check your API key.";
2688
+ var AuthenticationError = class extends Error {
2689
+ constructor(message = AUTHENTICATION_MESSAGE) {
2690
+ super(message);
2691
+ this.name = "AuthenticationError";
2692
+ }
2693
+ };
2694
+
2695
+ // src/providers/pruna/client/core/errors/timeout-error.ts
2696
+ var TIMEOUT_MESSAGE = "Generation timed out. Maximum polling attempts reached.";
2697
+ var TimeoutError = class extends Error {
2698
+ constructor(message = TIMEOUT_MESSAGE) {
2699
+ super(message);
2700
+ this.name = "TimeoutError";
2701
+ }
2702
+ };
2703
+
2704
+ // src/providers/pruna/client/core/errors/validation-error.ts
2705
+ var ValidationError2 = class extends Error {
2706
+ field;
2707
+ reason;
2708
+ receivedValue;
2709
+ constructor(context) {
2710
+ super(`Invalid ${context.field}: ${context.reason}`);
2711
+ this.name = "ValidationError";
2712
+ this.field = context.field;
2713
+ this.reason = context.reason;
2714
+ this.receivedValue = context.receivedValue;
2715
+ }
2716
+ };
2717
+
2718
+ // src/providers/pruna/client/core/utils/base64.ts
2719
+ var globalScope = globalThis;
2720
+ var hasBuffer = typeof globalScope.Buffer?.from === "function";
2721
+ function base64ToBytes(raw) {
2722
+ if (hasBuffer) {
2723
+ const bufferFactory = globalScope.Buffer;
2724
+ if (!bufferFactory) {
2725
+ throw new Error("Buffer unavailable despite detection flag");
2726
+ }
2727
+ const buf = bufferFactory.from(raw, "base64");
2728
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
2729
+ }
2730
+ const decoded = atob(raw);
2731
+ return Uint8Array.from(decoded, (char) => char.charCodeAt(0));
2732
+ }
2733
+
2734
+ // src/providers/pruna/client/core/utils/mime-detector.ts
2735
+ var MIN_SIGNATURE_BYTES = 2;
2736
+ function matchesSignature(bytes, signature) {
2737
+ if (bytes.length < signature.length) return false;
2738
+ return signature.every((value, index) => bytes[index] === value);
2739
+ }
2740
+ function detectImageMime(bytes) {
2741
+ if (bytes.length < MIN_SIGNATURE_BYTES) return DEFAULT_IMAGE_MIME;
2742
+ if (matchesSignature(bytes, IMAGE_SIGNATURES.PNG)) return MIME_PNG;
2743
+ if (matchesSignature(bytes, IMAGE_SIGNATURES.JPEG)) return MIME_JPEG;
2744
+ if (matchesSignature(bytes, IMAGE_SIGNATURES.WEBP_RIFF)) return MIME_WEBP;
2745
+ return DEFAULT_IMAGE_MIME;
2746
+ }
2747
+
2748
+ // src/providers/pruna/client/core/utils/api-error-parser.ts
2749
+ async function parseApiError(response, fallback) {
2750
+ const payload = await response.json().catch(() => ({ message: response.statusText }));
2751
+ const message = payload.message ?? `${fallback}: ${response.status}`;
2752
+ const error = new Error(message);
2753
+ error.statusCode = response.status;
2754
+ return error;
2755
+ }
2756
+
2757
+ // src/providers/pruna/client/core/protocol/http-status.ts
2758
+ function isAuthFailureStatus(status) {
2759
+ return AUTH_FAILURE_STATUSES.includes(status);
2760
+ }
2761
+
2762
+ // src/providers/pruna/client/core/protocol/job-status.ts
2763
+ var TERMINAL_SUCCESS_STATUSES = ["succeeded", "completed"];
2764
+ var TERMINAL_FAILURE_STATUSES = ["failed"];
2765
+ function isTerminalSuccessStatus(status) {
2766
+ if (!status) return false;
2767
+ return TERMINAL_SUCCESS_STATUSES.includes(status);
2768
+ }
2769
+ function isTerminalFailureStatus(status) {
2770
+ if (!status) return false;
2771
+ return TERMINAL_FAILURE_STATUSES.includes(status);
2772
+ }
2773
+
2774
+ // src/providers/pruna/client/core/protocol/abort-signal-classifier.ts
2775
+ function isAbortError(err) {
2776
+ if (err instanceof DOMException && err.code === DOMException.ABORT_ERR) return true;
2777
+ if (err instanceof Error && err.name === "AbortError") return true;
2778
+ return false;
2779
+ }
2780
+
2781
+ // src/providers/pruna/client/core/protocol/pruna-url-builder.ts
2782
+ function isAbsoluteHttpUrl(value) {
2783
+ return value.startsWith(HTTPS_PREFIX) || value.startsWith(HTTP_PREFIX);
2784
+ }
2785
+ function isHttpsUrl(value) {
2786
+ return value.startsWith(HTTPS_PREFIX);
2787
+ }
2788
+ function buildPrunaApiUrl(pathOrUrl) {
2789
+ if (isAbsoluteHttpUrl(pathOrUrl)) return pathOrUrl;
2790
+ return `${PRUNA_BASE_URL}${pathOrUrl}`;
2791
+ }
2792
+ function buildFileUrlById(fileId) {
2793
+ return `${PRUNA_FILES_URL}/${fileId}`;
2794
+ }
2795
+
2796
+ // src/providers/pruna/client/core/math/exponential-backoff.ts
2797
+ function calculateExponentialBackoff2(inputs) {
2798
+ const {
2799
+ attempt,
2800
+ baseIntervalMs,
2801
+ maxIntervalMs = POLL_DEFAULTS.maxIntervalMs,
2802
+ jitterPercent = POLL_DEFAULTS.jitterPercent,
2803
+ growthBase = POLL_DEFAULTS.backoffBase
2804
+ } = inputs;
2805
+ const safeAttempt = Math.max(0, attempt);
2806
+ const safeBase = Math.max(0, baseIntervalMs);
2807
+ const exponentialDelay = Math.min(safeBase * Math.pow(growthBase, safeAttempt), maxIntervalMs);
2808
+ const jitter = exponentialDelay * jitterPercent * Math.random();
2809
+ return exponentialDelay + jitter;
2810
+ }
2811
+
2812
+ // src/providers/pruna/client/core/utils/uri-resolver.ts
2813
+ var uriCache = /* @__PURE__ */ new WeakMap();
2814
+ function stripBase64Prefix(image) {
2815
+ if (isHttpsUrl(image)) return image;
2816
+ const base64Index = image.indexOf(BASE64_MARKER);
2817
+ return base64Index !== -1 ? image.slice(base64Index + BASE64_MARKER.length) : image;
2818
+ }
2819
+ function readOutput(output) {
2820
+ if (typeof output === "string") return output;
2821
+ if (Array.isArray(output) && output.length > 0) return output[0] ?? null;
2822
+ if (output !== null && typeof output === "object" && "url" in output) {
2823
+ return output.url;
2824
+ }
2825
+ return null;
2826
+ }
2827
+ function extractUri(data) {
2828
+ const cached = uriCache.get(data);
2829
+ if (cached !== void 0) return cached;
2830
+ if (!isTerminalSuccessStatus(data.status)) {
2831
+ uriCache.set(data, null);
2832
+ return null;
2833
+ }
2834
+ const result = data.generation_url ?? readOutput(data.output) ?? data.video_url ?? data.data ?? null;
2835
+ uriCache.set(data, result);
2836
+ return result;
2837
+ }
2838
+ function resolveUri(uri) {
2839
+ return uri.startsWith("/") ? `${PRUNA_BASE_URL}${uri}` : uri;
2840
+ }
2841
+
2842
+ // src/providers/pruna/client/core/services/pruna-client.service.ts
2843
+ function buildFileExtension(mime) {
2844
+ return MIME_FILE_EXTENSION[mime] ?? "bin";
2845
+ }
2846
+ async function uploadImage(base64OrUrl, apiKey, onProgress) {
2847
+ if (isHttpsUrl(base64OrUrl)) return base64OrUrl;
2848
+ if (base64OrUrl.startsWith(HTTP_PREFIX)) {
2849
+ throw new Error("Only HTTPS URLs are supported for security.");
2850
+ }
2851
+ onProgress?.("uploading");
2852
+ const raw = stripBase64Prefix(base64OrUrl);
2853
+ let bytes;
2854
+ try {
2855
+ bytes = base64ToBytes(raw);
2856
+ } catch {
2857
+ throw new Error("Invalid image format. Provide base64 or a valid HTTPS URL.");
2858
+ }
2859
+ const mime = detectImageMime(bytes);
2860
+ const buffer = bytes.buffer.slice(
2861
+ bytes.byteOffset,
2862
+ bytes.byteOffset + bytes.byteLength
2863
+ );
2864
+ const blob = new Blob([buffer], { type: mime });
2865
+ const formData = new FormData();
2866
+ formData.append("content", blob, `upload.${buildFileExtension(mime)}`);
2867
+ const res = await fetch(PRUNA_FILES_URL, {
2868
+ method: "POST",
2869
+ headers: { apikey: apiKey },
2870
+ body: formData
2871
+ });
2872
+ if (!res.ok) {
2873
+ throw await parseApiError(res, "File upload error");
2874
+ }
2875
+ const data = await res.json();
2876
+ return data.urls?.get ?? buildFileUrlById(data.id ?? "");
2877
+ }
2878
+ async function submitPrediction(model, input, apiKey, signal, onProgress) {
2879
+ onProgress?.("predicting");
2880
+ const res = await fetch(PRUNA_PREDICTIONS_URL, {
2881
+ method: "POST",
2882
+ headers: {
2883
+ apikey: apiKey,
2884
+ Model: model,
2885
+ "Try-Sync": "true",
2886
+ "Content-Type": "application/json"
2887
+ },
2888
+ body: JSON.stringify({ input }),
2889
+ signal
2890
+ });
2891
+ if (!res.ok) {
2892
+ throw await parseApiError(res, "API error");
2893
+ }
2894
+ return res.json();
2895
+ }
2896
+ var GenerationFailedError = class extends Error {
2897
+ constructor(message) {
2898
+ super(message);
2899
+ this.name = "GenerationFailedError";
2900
+ }
2901
+ };
2902
+ async function attemptPoll(fullUrl, apiKey, signal) {
2903
+ let res;
2904
+ try {
2905
+ res = await fetch(fullUrl, { headers: { apikey: apiKey }, signal });
2906
+ } catch (err) {
2907
+ if (isAbortError(err)) throw new CancellationError();
2908
+ return { outcome: "transient-failure" };
2909
+ }
2910
+ if (!res.ok) {
2911
+ if (isAuthFailureStatus(res.status)) throw new AuthenticationError();
2912
+ return { outcome: "transient-failure" };
2913
+ }
2914
+ let data;
2915
+ try {
2916
+ data = await res.json();
2917
+ } catch {
2918
+ return { outcome: "transient-failure" };
2919
+ }
2920
+ if (isTerminalSuccessStatus(data.status)) {
2921
+ const uri = extractUri(data);
2922
+ if (uri) return { outcome: "succeeded", uri: resolveUri(uri) };
2923
+ return { outcome: "transient-failure" };
2924
+ }
2925
+ if (isTerminalFailureStatus(data.status)) {
2926
+ throw new GenerationFailedError(data.error ?? "Generation failed during processing.");
2927
+ }
2928
+ return { outcome: "continue" };
2929
+ }
2930
+ async function pollForResult(pollUrl, apiKey, maxAttempts = POLL_DEFAULTS.maxAttempts, intervalMs = POLL_DEFAULTS.intervalMs, signal, onProgress) {
2931
+ const fullUrl = isAbsoluteHttpUrl(pollUrl) ? pollUrl : buildPrunaApiUrl(pollUrl);
2932
+ const controller = new AbortController();
2933
+ const onExternalAbort = () => controller.abort();
2934
+ signal?.addEventListener("abort", onExternalAbort);
2935
+ try {
2936
+ for (let i = 0; i < maxAttempts; i++) {
2937
+ if (signal?.aborted) throw new CancellationError();
2938
+ const delay = calculateExponentialBackoff2({ attempt: i, baseIntervalMs: intervalMs });
2939
+ await new Promise((resolve) => setTimeout(resolve, delay));
2940
+ if (signal?.aborted) throw new CancellationError();
2941
+ onProgress?.("polling", i + 1);
2942
+ const result = await attemptPoll(fullUrl, apiKey, controller.signal);
2943
+ if (result.outcome === "succeeded" && result.uri) {
2944
+ return result.uri;
2945
+ }
2946
+ }
2947
+ throw new TimeoutError();
2948
+ } finally {
2949
+ signal?.removeEventListener("abort", onExternalAbort);
2950
+ controller.abort();
2951
+ }
2952
+ }
2953
+
2954
+ // src/providers/pruna/client/core/math/comparison-hash.ts
2955
+ function sortKeysDeep(value) {
2956
+ if (Array.isArray(value)) {
2957
+ return value.map(sortKeysDeep);
2958
+ }
2959
+ if (value !== null && typeof value === "object") {
2960
+ const record = value;
2961
+ return Object.keys(record).sort().reduce((acc, key) => {
2962
+ acc[key] = sortKeysDeep(record[key]);
2963
+ return acc;
2964
+ }, {});
2965
+ }
2966
+ return value;
2967
+ }
2968
+ function hashValue(value) {
2969
+ if (value === null || typeof value !== "object") {
2970
+ return String(value);
2971
+ }
2972
+ try {
2973
+ return JSON.stringify(sortKeysDeep(value));
2974
+ } catch {
2975
+ return String(value);
2976
+ }
2977
+ }
2978
+ function compositeHash(...values) {
2979
+ return values.map(hashValue).join("|");
2980
+ }
2981
+
2982
+ // src/providers/pruna/client/core/math/cache-key-derivation.ts
2983
+ function deriveGenerationCacheKey(apiKey, input) {
2984
+ return `${apiKey}:${compositeHash(input)}`;
2985
+ }
2986
+
2987
+ // src/providers/pruna/client/core/validation/api-key-validator.ts
2988
+ function assertValidApiKey(apiKey) {
2989
+ if (typeof apiKey !== "string") {
2990
+ throw new ValidationError2({
2991
+ field: "apiKey",
2992
+ reason: "must be a string",
2993
+ receivedValue: typeof apiKey
2994
+ });
2995
+ }
2996
+ if (apiKey.length < MIN_API_KEY_LENGTH) {
2997
+ throw new ValidationError2({
2998
+ field: "apiKey",
2999
+ reason: "must not be empty",
3000
+ receivedValue: apiKey
3001
+ });
3002
+ }
3003
+ if (apiKey.length > MAX_API_KEY_LENGTH) {
3004
+ throw new ValidationError2({
3005
+ field: "apiKey",
3006
+ reason: `exceeds maximum length of ${MAX_API_KEY_LENGTH}`,
3007
+ receivedValue: apiKey.length
3008
+ });
3009
+ }
3010
+ }
3011
+
3012
+ // src/providers/pruna/client/core/validation/image-source-validator.ts
3013
+ function classifyImageSource(source) {
3014
+ if (source.startsWith(HTTPS_PREFIX)) return "https-url";
3015
+ if (source.startsWith(HTTP_PREFIX)) return "http-url";
3016
+ if (source.includes(BASE64_MARKER) || /^[A-Za-z0-9+/=]+$/.test(source)) return "base64";
3017
+ return "unknown";
3018
+ }
3019
+ function assertSecureImageSource(image, fieldName = "image") {
3020
+ if (typeof image !== "string") {
3021
+ throw new ValidationError2({
3022
+ field: fieldName,
3023
+ reason: "must be a string",
3024
+ receivedValue: typeof image
3025
+ });
3026
+ }
3027
+ const kind = classifyImageSource(image);
3028
+ if (kind === "http-url") {
3029
+ throw new ValidationError2({
3030
+ field: fieldName,
3031
+ reason: "HTTP URLs are not supported; use HTTPS or a base64 data URI",
3032
+ receivedValue: image
3033
+ });
3034
+ }
3035
+ if (kind === "unknown") {
3036
+ throw new ValidationError2({
3037
+ field: fieldName,
3038
+ reason: "must be an HTTPS URL or a base64 data URI",
3039
+ receivedValue: typeof image
3040
+ });
3041
+ }
3042
+ }
3043
+
3044
+ // src/providers/pruna/client/core/validation/aspect-ratio-validator.ts
3045
+ var SUPPORTED_ASPECT_RATIOS = [
3046
+ "16:9",
3047
+ "9:16",
3048
+ "1:1",
3049
+ "4:3",
3050
+ "3:4",
3051
+ "3:2",
3052
+ "2:3"
3053
+ ];
3054
+ function isSupportedAspectRatio(value) {
3055
+ return typeof value === "string" && SUPPORTED_ASPECT_RATIOS.includes(value);
3056
+ }
3057
+ function assertValidAspectRatio(value, fieldName = "aspect_ratio") {
3058
+ if (value === void 0 || value === null) return;
3059
+ if (!isSupportedAspectRatio(value)) {
3060
+ throw new ValidationError2({
3061
+ field: fieldName,
3062
+ reason: `must be one of ${SUPPORTED_ASPECT_RATIOS.join(", ")}`,
3063
+ receivedValue: value
3064
+ });
3065
+ }
3066
+ }
3067
+
3068
+ // src/providers/pruna/client/core/validation/resolution-validator.ts
3069
+ var SUPPORTED_RESOLUTIONS = ["720p", "1080p"];
3070
+ function isSupportedResolution(value) {
3071
+ return typeof value === "string" && SUPPORTED_RESOLUTIONS.includes(value);
3072
+ }
3073
+ function assertValidResolution(value, fieldName = "resolution") {
3074
+ if (value === void 0 || value === null) return;
3075
+ if (!isSupportedResolution(value)) {
3076
+ throw new ValidationError2({
3077
+ field: fieldName,
3078
+ reason: `must be one of ${SUPPORTED_RESOLUTIONS.join(", ")}`,
3079
+ receivedValue: value
3080
+ });
3081
+ }
3082
+ }
3083
+
3084
+ // src/providers/pruna/client/core/validation/duration-validator.ts
3085
+ var MIN_VIDEO_DURATION_SECONDS = 1;
3086
+ var MAX_VIDEO_DURATION_SECONDS = 30;
3087
+ function isValidDuration(value) {
3088
+ return typeof value === "number" && Number.isFinite(value) && value >= MIN_VIDEO_DURATION_SECONDS && value <= MAX_VIDEO_DURATION_SECONDS && Number.isInteger(value);
3089
+ }
3090
+ function assertValidDuration(value, fieldName = "duration") {
3091
+ if (value === void 0 || value === null) return;
3092
+ if (!isValidDuration(value)) {
3093
+ throw new ValidationError2({
3094
+ field: fieldName,
3095
+ reason: `must be an integer between ${MIN_VIDEO_DURATION_SECONDS} and ${MAX_VIDEO_DURATION_SECONDS} seconds`,
3096
+ receivedValue: value
3097
+ });
3098
+ }
3099
+ }
3100
+
3101
+ // src/providers/pruna/client/core/validation/prompt-validator.ts
3102
+ var MIN_PROMPT_LENGTH = 1;
3103
+ var MAX_PROMPT_LENGTH = 4e3;
3104
+ function assertValidPrompt(value, fieldName = "prompt") {
3105
+ if (typeof value !== "string") {
3106
+ throw new ValidationError2({
3107
+ field: fieldName,
3108
+ reason: "must be a string",
3109
+ receivedValue: typeof value
3110
+ });
3111
+ }
3112
+ if (value.trim().length < MIN_PROMPT_LENGTH) {
3113
+ throw new ValidationError2({
3114
+ field: fieldName,
3115
+ reason: "must not be empty or whitespace-only",
3116
+ receivedValue: value
3117
+ });
3118
+ }
3119
+ if (value.length > MAX_PROMPT_LENGTH) {
3120
+ throw new ValidationError2({
3121
+ field: fieldName,
3122
+ reason: `exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`,
3123
+ receivedValue: value.length
3124
+ });
3125
+ }
3126
+ }
3127
+
3128
+ // src/providers/pruna/client/core/validation/seed-validator.ts
3129
+ var MIN_SEED = -2147483648;
3130
+ var MAX_SEED = 2147483647;
3131
+ function isValidSeed(value) {
3132
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= MIN_SEED && value <= MAX_SEED;
3133
+ }
3134
+ function assertValidSeed(value, fieldName = "seed") {
3135
+ if (value === void 0 || value === null) return;
3136
+ if (!isValidSeed(value)) {
3137
+ throw new ValidationError2({
3138
+ field: fieldName,
3139
+ reason: `must be an integer in [${MIN_SEED}, ${MAX_SEED}]`,
3140
+ receivedValue: value
3141
+ });
3142
+ }
3143
+ }
3144
+
3145
+ // src/providers/pruna/client/core/protocol/input-type-guard.ts
3146
+ function isTextToImageInput(input) {
3147
+ return input.model === "p-image";
3148
+ }
3149
+ function isImageToImageInput(input) {
3150
+ return input.model === "p-image-edit";
3151
+ }
3152
+ function isImageToVideoInput(input) {
3153
+ return input.model === "p-video";
3154
+ }
3155
+
3156
+ // src/providers/pruna/client/core/utils/request-deduplicator.ts
3157
+ var RequestDeduplicator2 = class {
3158
+ pendingRequests = /* @__PURE__ */ new Map();
3159
+ ttl;
3160
+ constructor(ttl = DEFAULT_DEDUPLICATOR_TTL_MS) {
3161
+ this.ttl = ttl;
3162
+ }
3163
+ execute(key, fn) {
3164
+ const existing = this.pendingRequests.get(key);
3165
+ if (existing) {
3166
+ if (Date.now() - existing.timestamp < this.ttl) {
3167
+ return existing.promise;
3168
+ }
3169
+ this.pendingRequests.delete(key);
3170
+ }
3171
+ const promise = fn().finally(() => {
3172
+ setTimeout(() => {
3173
+ this.pendingRequests.delete(key);
3174
+ }, DEDUPLICATOR_CLEANUP_GRACE_MS);
3175
+ });
3176
+ this.pendingRequests.set(key, { promise, timestamp: Date.now() });
3177
+ return promise;
3178
+ }
3179
+ clear() {
3180
+ this.pendingRequests.clear();
3181
+ }
3182
+ size() {
3183
+ this.purgeExpired();
3184
+ return this.pendingRequests.size;
3185
+ }
3186
+ purgeExpired() {
3187
+ const now = Date.now();
3188
+ for (const [key, value] of this.pendingRequests) {
3189
+ if (now - value.timestamp >= this.ttl) {
3190
+ this.pendingRequests.delete(key);
3191
+ }
3192
+ }
3193
+ }
3194
+ };
3195
+ var globalDeduplicator = new RequestDeduplicator2();
3196
+
3197
+ // src/providers/pruna/client/generation/builders/text-to-image.builder.ts
3198
+ var TextToImageInputBuilder = class {
3199
+ model = "p-image";
3200
+ build(input, _context) {
3201
+ const payload = {
3202
+ prompt: input.prompt,
3203
+ aspect_ratio: input.aspect_ratio ?? DEFAULT_ASPECT_RATIO
2546
3204
  };
2547
- return;
3205
+ if (input.seed !== void 0) {
3206
+ payload.seed = input.seed;
3207
+ }
3208
+ return Promise.resolve(payload);
2548
3209
  }
2549
- const pollUrl = prediction.status_url || prediction.generation_url;
3210
+ };
3211
+
3212
+ // src/providers/pruna/client/generation/builders/image-to-image.builder.ts
3213
+ var ImageToImageInputBuilder = class {
3214
+ model = "p-image-edit";
3215
+ build(input, _context) {
3216
+ const payload = {
3217
+ images: [stripBase64Prefix(input.image)],
3218
+ prompt: input.prompt,
3219
+ aspect_ratio: input.aspect_ratio ?? DEFAULT_ASPECT_RATIO
3220
+ };
3221
+ if (input.seed !== void 0) {
3222
+ payload.seed = input.seed;
3223
+ }
3224
+ return Promise.resolve(payload);
3225
+ }
3226
+ };
3227
+
3228
+ // src/providers/pruna/client/generation/builders/image-to-video.builder.ts
3229
+ var ImageToVideoInputBuilder = class {
3230
+ model = "p-video";
3231
+ async build(input, context) {
3232
+ if (context.signal?.aborted) {
3233
+ throw new CancellationError();
3234
+ }
3235
+ const fileUrl = await uploadImage(
3236
+ input.image,
3237
+ context.apiKey,
3238
+ context.onProgress
3239
+ );
3240
+ return {
3241
+ image: fileUrl,
3242
+ prompt: input.prompt,
3243
+ duration: input.duration ?? P_VIDEO_DEFAULTS.duration,
3244
+ resolution: input.resolution ?? P_VIDEO_DEFAULTS.resolution,
3245
+ fps: P_VIDEO_DEFAULTS.fps,
3246
+ draft: input.draft ?? P_VIDEO_DEFAULTS.draft,
3247
+ aspect_ratio: input.aspect_ratio ?? DEFAULT_ASPECT_RATIO,
3248
+ prompt_upsampling: P_VIDEO_DEFAULTS.promptUpsampling
3249
+ };
3250
+ }
3251
+ };
3252
+
3253
+ // src/providers/pruna/client/generation/builders/model-input-builder.ts
3254
+ var ModelInputBuilderRegistry = class {
3255
+ builders = /* @__PURE__ */ new Map();
3256
+ register(builder) {
3257
+ this.builders.set(builder.model, builder);
3258
+ return this;
3259
+ }
3260
+ resolve(model) {
3261
+ const builder = this.builders.get(model);
3262
+ if (!builder) {
3263
+ throw new Error(`No input builder registered for model: ${model}`);
3264
+ }
3265
+ return builder;
3266
+ }
3267
+ };
3268
+
3269
+ // src/providers/pruna/client/generation/builders/builder-factory.ts
3270
+ function createDefaultBuilderRegistry() {
3271
+ return new ModelInputBuilderRegistry().register(new TextToImageInputBuilder()).register(new ImageToImageInputBuilder()).register(new ImageToVideoInputBuilder());
3272
+ }
3273
+
3274
+ // src/providers/pruna/client/generation/services/generation.service.ts
3275
+ var builderRegistry = createDefaultBuilderRegistry();
3276
+ function validateGenerateInput(input) {
3277
+ assertValidPrompt(input.prompt, `${input.model}.prompt`);
3278
+ assertValidAspectRatio(input.aspect_ratio, `${input.model}.aspect_ratio`);
3279
+ if (isTextToImageInput(input)) {
3280
+ assertValidSeed(input.seed, `${input.model}.seed`);
3281
+ } else if (isImageToImageInput(input)) {
3282
+ assertSecureImageSource(input.image, `${input.model}.image`);
3283
+ assertValidSeed(input.seed, `${input.model}.seed`);
3284
+ } else if (isImageToVideoInput(input)) {
3285
+ assertSecureImageSource(input.image, `${input.model}.image`);
3286
+ assertValidDuration(input.duration, `${input.model}.duration`);
3287
+ assertValidResolution(input.resolution, `${input.model}.resolution`);
3288
+ }
3289
+ }
3290
+ async function generate(apiKey, input, options = {}) {
3291
+ assertValidApiKey(apiKey);
3292
+ validateGenerateInput(input);
3293
+ const { signal, onProgress, useCache = true } = options;
3294
+ if (useCache) {
3295
+ const cacheKey = deriveGenerationCacheKey(apiKey, input);
3296
+ return globalDeduplicator.execute(
3297
+ cacheKey,
3298
+ () => generateInternal(apiKey, input, { signal, onProgress })
3299
+ );
3300
+ }
3301
+ return generateInternal(apiKey, input, { signal, onProgress });
3302
+ }
3303
+ async function generateInternal(apiKey, input, options) {
3304
+ const { signal, onProgress } = options;
3305
+ const builder = builderRegistry.resolve(input.model);
3306
+ const modelInput = await builder.build(input, { apiKey, signal, onProgress });
3307
+ const response = await submitPrediction(input.model, modelInput, apiKey, signal, onProgress);
3308
+ const syncUri = extractUri(response);
3309
+ if (syncUri) {
3310
+ return { url: resolveUri(syncUri), model: input.model };
3311
+ }
3312
+ const pollUrl = response.get_url ?? response.status_url;
2550
3313
  if (!pollUrl) {
2551
- throw new Error("Pruna prediction response missing polling URL");
3314
+ throw new Error("Pruna API returned no result and no polling URL.");
2552
3315
  }
2553
- const resultUrl = await prunaCore.pollForResult(pollUrl, apiKey, 60, 2e3, signal, onProgress);
2554
- yield { stage: "polling", result: { url: resultUrl, model: modelId } };
3316
+ const url = await pollForResult(
3317
+ pollUrl,
3318
+ apiKey,
3319
+ POLL_DEFAULTS.maxAttempts,
3320
+ POLL_DEFAULTS.intervalMs,
3321
+ signal,
3322
+ onProgress
3323
+ );
3324
+ return { url, model: input.model };
3325
+ }
3326
+ async function generateImageThenVideo(apiKey, input, options = {}) {
3327
+ assertValidApiKey(apiKey);
3328
+ assertValidPrompt(input.prompt, "text-to-video.prompt");
3329
+ assertValidAspectRatio(input.aspect_ratio, "text-to-video.aspect_ratio");
3330
+ assertValidDuration(input.duration, "text-to-video.duration");
3331
+ assertValidResolution(input.resolution, "text-to-video.resolution");
3332
+ const { signal, onProgress } = options;
3333
+ const aspectRatio = input.aspect_ratio ?? DEFAULT_ASPECT_RATIO;
3334
+ onProgress?.("predicting", 1);
3335
+ const imageResult = await generate(
3336
+ apiKey,
3337
+ { model: "p-image", prompt: input.prompt, aspect_ratio: aspectRatio },
3338
+ { signal }
3339
+ );
3340
+ onProgress?.("predicting", 2);
3341
+ return generate(
3342
+ apiKey,
3343
+ {
3344
+ model: "p-video",
3345
+ prompt: input.prompt,
3346
+ image: imageResult.url,
3347
+ duration: input.duration ?? P_VIDEO_DEFAULTS.duration,
3348
+ resolution: input.resolution ?? P_VIDEO_DEFAULTS.resolution,
3349
+ aspect_ratio: aspectRatio,
3350
+ draft: input.draft ?? P_VIDEO_DEFAULTS.draft
3351
+ },
3352
+ { signal, onProgress }
3353
+ );
3354
+ }
3355
+
3356
+ // src/providers/pruna/pruna-generate.ts
3357
+ async function* generateWithPruna(request, apiKey, signal, onProgress) {
3358
+ const stages = [];
3359
+ let notify = null;
3360
+ let done = false;
3361
+ const wake = () => {
3362
+ const fn = notify;
3363
+ if (fn) {
3364
+ notify = null;
3365
+ fn();
3366
+ }
3367
+ };
3368
+ const handleProgress = (stage) => {
3369
+ stages.push({ stage });
3370
+ onProgress?.(stage);
3371
+ wake();
3372
+ };
3373
+ const promise = (async () => {
3374
+ try {
3375
+ const model = request.model ?? (request.image ? "p-image-edit" : "p-image");
3376
+ if (model === "p-video" && !request.image && request.prompt) {
3377
+ const videoRes = await generateImageThenVideo(
3378
+ apiKey,
3379
+ {
3380
+ prompt: request.prompt,
3381
+ duration: request.duration ?? 5,
3382
+ aspect_ratio: request.aspectRatio ?? request.aspect_ratio ?? "16:9"
3383
+ },
3384
+ { signal, onProgress: handleProgress }
3385
+ );
3386
+ stages.push({
3387
+ stage: "polling",
3388
+ result: { url: videoRes.url, model: "p-video" }
3389
+ });
3390
+ return;
3391
+ }
3392
+ const input = {
3393
+ model,
3394
+ prompt: request.prompt ?? "",
3395
+ aspect_ratio: request.aspectRatio ?? request.aspect_ratio ?? "1:1",
3396
+ ...request.image ? { image: request.image } : {},
3397
+ ...request.duration ? { duration: request.duration } : {},
3398
+ ...request.resolution ? { resolution: request.resolution } : {},
3399
+ ...request.seed !== void 0 ? { seed: request.seed } : {}
3400
+ };
3401
+ const res = await generate(apiKey, input, {
3402
+ signal,
3403
+ onProgress: handleProgress
3404
+ });
3405
+ stages.push({
3406
+ stage: "polling",
3407
+ result: { url: res.url, model: res.model }
3408
+ });
3409
+ } catch (err) {
3410
+ stages.push({
3411
+ stage: "processing",
3412
+ error: err instanceof Error ? err : new Error(String(err))
3413
+ });
3414
+ } finally {
3415
+ done = true;
3416
+ wake();
3417
+ }
3418
+ })();
3419
+ while (!done || stages.length > 0) {
3420
+ if (stages.length === 0 && !done) {
3421
+ await new Promise((resolve) => {
3422
+ notify = resolve;
3423
+ });
3424
+ notify = null;
3425
+ }
3426
+ while (stages.length > 0) {
3427
+ const nextStage = stages.shift();
3428
+ if (!nextStage) continue;
3429
+ yield nextStage;
3430
+ if (nextStage.error) {
3431
+ throw nextStage.error;
3432
+ }
3433
+ }
3434
+ }
3435
+ await promise;
2555
3436
  }
2556
3437
 
2557
3438
  // src/providers/pruna/pruna.provider.ts
@@ -4862,7 +5743,7 @@ function useText(options = {}, deps) {
4862
5743
  (request, onChunk) => engine instanceof ProviderFactory ? engine.chatStream(request, { onChunk }) : engine.stream(request, { onChunk }),
4863
5744
  [engine]
4864
5745
  );
4865
- const generate = useCallback(
5746
+ const generate2 = useCallback(
4866
5747
  (prompt, config) => runWithState(async (signal) => {
4867
5748
  const response = await run(buildRequest(prompt, config, signal));
4868
5749
  setResult(response.text);
@@ -4918,7 +5799,7 @@ function useText(options = {}, deps) {
4918
5799
  isLoading,
4919
5800
  error,
4920
5801
  result,
4921
- generate,
5802
+ generate: generate2,
4922
5803
  generateJSON,
4923
5804
  stream,
4924
5805
  cancel,
@@ -5230,6 +6111,273 @@ function useChat(options, deps) {
5230
6111
  };
5231
6112
  }
5232
6113
 
5233
- export { useAIContent, useAIGeneration, useBlogGenerator, useCacheStats, useChat, useContentWizard, useImageWizard, useSocialContentGenerator, useText, useVideoWizard };
6114
+ // src/providers/groq/groq-container.ts
6115
+ var container = null;
6116
+ function getProvider() {
6117
+ return container;
6118
+ }
6119
+
6120
+ // src/hooks/use-groq.hook.ts
6121
+ function useGroq(options = {}, deps) {
6122
+ const textGeneration = deps?.textGeneration ?? getProvider()?.textGeneration;
6123
+ if (!textGeneration) {
6124
+ throw new Error(
6125
+ "useGroq requires a TextGenerationService dependency. Provide it via the second argument or wire it through configureProvider()."
6126
+ );
6127
+ }
6128
+ const [isLoading, setIsLoading] = useState(false);
6129
+ const [error, setError] = useState(null);
6130
+ const [result, setResult] = useState(null);
6131
+ const optionsRef = useRef(options);
6132
+ optionsRef.current = options;
6133
+ const abortControllerRef = useRef(null);
6134
+ const getOrCreateAbortController = useCallback(() => {
6135
+ if (!abortControllerRef.current || abortControllerRef.current.signal.aborted) {
6136
+ abortControllerRef.current = new AbortController();
6137
+ }
6138
+ return abortControllerRef.current;
6139
+ }, []);
6140
+ const stableConfig = useMemo(
6141
+ () => ({
6142
+ model: options.model,
6143
+ generationConfig: options.generationConfig
6144
+ }),
6145
+ [
6146
+ options.model,
6147
+ options.generationConfig?.temperature,
6148
+ options.generationConfig?.maxTokens,
6149
+ options.generationConfig?.topP
6150
+ ]
6151
+ );
6152
+ useEffect(() => {
6153
+ abortControllerRef.current = new AbortController();
6154
+ return () => {
6155
+ abortControllerRef.current?.abort();
6156
+ abortControllerRef.current = null;
6157
+ };
6158
+ }, []);
6159
+ const runWithState = useCallback(
6160
+ async (task) => {
6161
+ const controller = getOrCreateAbortController();
6162
+ const { signal } = controller;
6163
+ if (signal.aborted) return null;
6164
+ setIsLoading(true);
6165
+ setError(null);
6166
+ setResult(null);
6167
+ const currentOptions = optionsRef.current;
6168
+ currentOptions.onStart?.();
6169
+ try {
6170
+ const value = await task(signal);
6171
+ return value;
6172
+ } catch (err) {
6173
+ if (signal.aborted) {
6174
+ return null;
6175
+ }
6176
+ const message = resolveUserFriendlyMessage(err);
6177
+ setError(message);
6178
+ currentOptions.onError?.(message);
6179
+ throw err;
6180
+ } finally {
6181
+ if (!signal.aborted) {
6182
+ setIsLoading(false);
6183
+ }
6184
+ }
6185
+ },
6186
+ [getOrCreateAbortController]
6187
+ );
6188
+ const generate2 = useCallback(
6189
+ (prompt, config) => runWithState(async () => {
6190
+ const response = await textGeneration.generateCompletion(prompt, {
6191
+ model: stableConfig.model,
6192
+ generationConfig: {
6193
+ ...stableConfig.generationConfig,
6194
+ ...config
6195
+ }
6196
+ });
6197
+ setResult(response);
6198
+ optionsRef.current.onSuccess?.(response);
6199
+ return response;
6200
+ }),
6201
+ [runWithState, textGeneration, stableConfig]
6202
+ );
6203
+ const generateJSON = useCallback(
6204
+ (prompt, config) => runWithState(async () => {
6205
+ const response = await textGeneration.generateStructured(prompt, {
6206
+ model: stableConfig.model,
6207
+ generationConfig: {
6208
+ ...stableConfig.generationConfig,
6209
+ ...config
6210
+ },
6211
+ ...config?.schema !== void 0 ? { schema: config.schema } : {}
6212
+ });
6213
+ const json = JSON.stringify(response, null, 2);
6214
+ setResult(json);
6215
+ optionsRef.current.onSuccess?.(json);
6216
+ return response;
6217
+ }),
6218
+ [runWithState, textGeneration, stableConfig]
6219
+ );
6220
+ const stream = useCallback(
6221
+ (prompt, onChunk, config) => runWithState(async () => {
6222
+ const callbacks = {
6223
+ onChunk,
6224
+ onComplete: (text) => {
6225
+ setResult(text);
6226
+ optionsRef.current.onSuccess?.(text);
6227
+ }
6228
+ };
6229
+ return textGeneration.streamCompletion(
6230
+ prompt,
6231
+ callbacks,
6232
+ {
6233
+ model: stableConfig.model,
6234
+ generationConfig: {
6235
+ ...stableConfig.generationConfig,
6236
+ ...config
6237
+ }
6238
+ }
6239
+ );
6240
+ }),
6241
+ [runWithState, textGeneration, stableConfig]
6242
+ );
6243
+ const cancel = useCallback(() => {
6244
+ if (abortControllerRef.current) {
6245
+ abortControllerRef.current.abort();
6246
+ abortControllerRef.current = new AbortController();
6247
+ }
6248
+ setIsLoading(false);
6249
+ }, []);
6250
+ const reset = useCallback(() => {
6251
+ cancel();
6252
+ setError(null);
6253
+ setResult(null);
6254
+ }, [cancel]);
6255
+ const clearError = useCallback(() => setError(null), []);
6256
+ return {
6257
+ isLoading,
6258
+ error,
6259
+ result,
6260
+ generate: generate2,
6261
+ generateJSON,
6262
+ stream,
6263
+ cancel,
6264
+ reset,
6265
+ clearError
6266
+ };
6267
+ }
6268
+ function isAbortError2(err) {
6269
+ if (!(err instanceof Error)) return false;
6270
+ if (err.name === "AbortError") return true;
6271
+ if (err instanceof DOMException && err.code === DOMException.ABORT_ERR) return true;
6272
+ return isCancellationError(err);
6273
+ }
6274
+ function toError(value) {
6275
+ return value instanceof Error ? value : new Error(String(value));
6276
+ }
6277
+ function useAsyncAction(executor, options = {}) {
6278
+ const [result, setResult] = useState(null);
6279
+ const [isLoading, setIsLoading] = useState(false);
6280
+ const [error, setError] = useState(null);
6281
+ const abortRef = useRef(null);
6282
+ const executorRef = useRef(executor);
6283
+ const optionsRef = useRef(options);
6284
+ const mountedRef = useRef(true);
6285
+ useEffect(() => {
6286
+ executorRef.current = executor;
6287
+ optionsRef.current = options;
6288
+ });
6289
+ useEffect(() => {
6290
+ mountedRef.current = true;
6291
+ return () => {
6292
+ mountedRef.current = false;
6293
+ abortRef.current?.abort();
6294
+ abortRef.current = null;
6295
+ };
6296
+ }, []);
6297
+ const run = useCallback(
6298
+ async (input) => {
6299
+ abortRef.current?.abort();
6300
+ const controller = new AbortController();
6301
+ abortRef.current = controller;
6302
+ setIsLoading(true);
6303
+ setError(null);
6304
+ setResult(null);
6305
+ try {
6306
+ const value = await executorRef.current(input, controller.signal);
6307
+ if (!mountedRef.current) return null;
6308
+ setResult(value);
6309
+ optionsRef.current.onSuccess?.(value);
6310
+ return value;
6311
+ } catch (err) {
6312
+ if (isAbortError2(err)) {
6313
+ if (mountedRef.current) {
6314
+ optionsRef.current.onCancelled?.();
6315
+ }
6316
+ return null;
6317
+ }
6318
+ if (!mountedRef.current) return null;
6319
+ const normalized = toError(err);
6320
+ setError(normalized);
6321
+ optionsRef.current.onError?.(normalized);
6322
+ return null;
6323
+ } finally {
6324
+ if (mountedRef.current) {
6325
+ setIsLoading(false);
6326
+ }
6327
+ }
6328
+ },
6329
+ []
6330
+ );
6331
+ const cancel = useCallback(() => {
6332
+ abortRef.current?.abort();
6333
+ abortRef.current = null;
6334
+ }, []);
6335
+ const reset = useCallback(() => {
6336
+ abortRef.current?.abort();
6337
+ abortRef.current = null;
6338
+ setResult(null);
6339
+ setError(null);
6340
+ setIsLoading(false);
6341
+ }, []);
6342
+ return useMemo(
6343
+ () => ({ result, isLoading, error, run, cancel, reset }),
6344
+ [result, isLoading, error, run, cancel, reset]
6345
+ );
6346
+ }
6347
+
6348
+ // src/hooks/usePrunaGeneration.ts
6349
+ function usePrunaGeneration(apiKey, options = {}) {
6350
+ return useAsyncAction(
6351
+ (input, signal) => generate(apiKey, input, { signal, onProgress: options.onProgress }),
6352
+ options
6353
+ );
6354
+ }
6355
+
6356
+ // src/hooks/usePrunaProxy.ts
6357
+ function createProxyExecutor(proxyUrl, executorOptions) {
6358
+ return async (input, signal) => {
6359
+ executorOptions.onProgress?.("uploading");
6360
+ const res = await fetch(proxyUrl, {
6361
+ method: "POST",
6362
+ headers: { "Content-Type": "application/json" },
6363
+ body: JSON.stringify(input),
6364
+ signal
6365
+ });
6366
+ if (!res.ok) {
6367
+ const payload = await res.json().catch(() => ({ message: res.statusText }));
6368
+ throw new Error(payload.message ?? `Proxy error: ${res.status}`);
6369
+ }
6370
+ return res.json();
6371
+ };
6372
+ }
6373
+ function usePrunaProxy(options) {
6374
+ const { proxyUrl, onProgress, onSuccess, onError } = options;
6375
+ return useAsyncAction(
6376
+ createProxyExecutor(proxyUrl, { onProgress }),
6377
+ { onSuccess, onError }
6378
+ );
6379
+ }
6380
+
6381
+ export { CancellationError, useAIContent, useAIGeneration, useAsyncAction, useBlogGenerator, useCacheStats, useChat, useContentWizard, useGroq, useImageWizard, usePrunaGeneration, usePrunaProxy, useSocialContentGenerator, useText, useVideoWizard };
5234
6382
  //# sourceMappingURL=index.js.map
5235
6383
  //# sourceMappingURL=index.js.map