@thorprovider/medusa-extended 1.11.3 → 1.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -454,7 +454,7 @@ var DropshipperClient = class {
454
454
  *
455
455
  * Unlike `chatAssistant`, this surfaces the model's progress: the backend
456
456
  * resolves the intent first and then streams the answer token by token
457
- * (catalog searches) or returns the full answer in one shot (greetings/other).
457
+ * (catalog searches) or returns the full answer in one shot (`default`).
458
458
  * The response is normalized into a single `ReadableStream<ChatStreamEvent>`
459
459
  * so callers can show a live "thinking" label and render text incrementally.
460
460
  *
@@ -474,7 +474,10 @@ var DropshipperClient = class {
474
474
  };
475
475
  let response;
476
476
  try {
477
- response = await fetchBackendWithResilience(url, init);
477
+ response = await fetchBackendWithResilience(url, init, {
478
+ timeoutMs: 6e4,
479
+ maxRetries: 1
480
+ });
478
481
  } catch (networkError) {
479
482
  throw new ProviderAPIError3(
480
483
  `[dropshipper] Network error on POST /admin/thor/dropshipper/ai/chat: ${networkError.message}`,
@@ -484,20 +487,36 @@ var DropshipperClient = class {
484
487
  }
485
488
  if (!response.ok) {
486
489
  let message = `HTTP ${response.status}`;
490
+ let errorCode;
491
+ let requestId;
492
+ let details;
487
493
  try {
488
494
  const json = await response.json();
489
495
  message = json?.message ?? message;
496
+ errorCode = json?.error_code ?? json?.type;
497
+ requestId = json?.request_id;
498
+ details = json?.details;
490
499
  } catch {
491
500
  }
501
+ this.logger.error(
502
+ `[dropshipper] POST /admin/thor/dropshipper/ai/chat failed status=${response.status} message=${message}` + (errorCode ? ` error_code=${errorCode}` : "") + (requestId ? ` request_id=${requestId}` : "") + (details ? ` details=${details}` : "")
503
+ );
492
504
  throw new ProviderAPIError3(
493
505
  `[dropshipper] POST /admin/thor/dropshipper/ai/chat failed: ${message}`,
494
506
  `DROPSHIPPER_API_${response.status}`,
495
- response.status
507
+ response.status,
508
+ {
509
+ message,
510
+ status: response.status,
511
+ errorCode,
512
+ requestId,
513
+ details
514
+ }
496
515
  );
497
516
  }
498
517
  const contentType = response.headers.get("content-type") ?? "";
499
518
  if (contentType.includes("text/event-stream") && response.body) {
500
- return mapUiMessageStreamToEvents(response.body);
519
+ return mapUiMessageStreamToEvents(response.body, this.logger);
501
520
  }
502
521
  const data = await response.json();
503
522
  return new ReadableStream({
@@ -513,6 +532,61 @@ var DropshipperClient = class {
513
532
  }
514
533
  });
515
534
  }
535
+ /**
536
+ * Cheap liveness probe for the AI assistant.
537
+ *
538
+ * The backend performs no LLM or retrieval work, so this is safe to poll
539
+ * (e.g. from a dashboard availability indicator). Returns 200 with
540
+ * `available: true` when the assistant is configured; the backend answers
541
+ * HTTP 503 with `available: false` when `AI_GATEWAY_API_KEY` is missing.
542
+ *
543
+ * Deliberately does NOT go through `fetchBackendWithResilience`: that layer
544
+ * funnels every call through the SHARED `backendCircuitBreaker`, and the
545
+ * definitive 503 "not configured" answer would count as failures and could
546
+ * open the circuit, taking down all dropshipper API traffic. A health probe
547
+ * must stay isolated, with no retries and a short timeout.
548
+ *
549
+ * `GET /admin/thor/dropshipper/ai/health`
550
+ */
551
+ async chatAssistantHealth() {
552
+ const url = `${this.baseUrl}/admin/thor/dropshipper/ai/health`;
553
+ const controller = new AbortController();
554
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
555
+ let response;
556
+ try {
557
+ response = await fetch(url, {
558
+ method: "GET",
559
+ headers: { Authorization: `Bearer ${this.token}` },
560
+ signal: controller.signal,
561
+ cache: "no-store"
562
+ });
563
+ } catch (error) {
564
+ clearTimeout(timeoutId);
565
+ throw new ProviderAPIError3(
566
+ `[dropshipper] Network error on GET /admin/thor/dropshipper/ai/health: ${error.message}`,
567
+ "NETWORK_ERROR",
568
+ 0
569
+ );
570
+ }
571
+ clearTimeout(timeoutId);
572
+ if (response.status === 503) {
573
+ try {
574
+ const body = await response.json();
575
+ if (body && typeof body.available === "boolean") {
576
+ return body;
577
+ }
578
+ } catch {
579
+ }
580
+ }
581
+ if (!response.ok) {
582
+ throw new ProviderAPIError3(
583
+ `[dropshipper] GET /admin/thor/dropshipper/ai/health failed: HTTP ${response.status}`,
584
+ `DROPSHIPPER_API_${response.status}`,
585
+ response.status
586
+ );
587
+ }
588
+ return response.json();
589
+ }
516
590
  // -------------------------------------------------------------------------
517
591
  // Notifications
518
592
  // -------------------------------------------------------------------------
@@ -1671,10 +1745,11 @@ var DropshipperClient = class {
1671
1745
  );
1672
1746
  }
1673
1747
  };
1674
- async function* parseSse(stream) {
1748
+ async function* parseSse(stream, logger) {
1675
1749
  const reader = stream.getReader();
1676
1750
  const decoder = new TextDecoder();
1677
1751
  let buffer = "";
1752
+ let frameIndex = 0;
1678
1753
  try {
1679
1754
  while (true) {
1680
1755
  const { done, value } = await reader.read();
@@ -1687,9 +1762,13 @@ async function* parseSse(stream) {
1687
1762
  if (!trimmed.startsWith("data:")) continue;
1688
1763
  const payload = trimmed.slice(5).trim();
1689
1764
  if (!payload || payload === "[DONE]") continue;
1765
+ frameIndex += 1;
1690
1766
  try {
1691
1767
  yield JSON.parse(payload);
1692
- } catch {
1768
+ } catch (err) {
1769
+ logger?.warn(
1770
+ `[dropshipper] sse-malformed-frame frame=${frameIndex} payload=${payload.slice(0, 200)}`
1771
+ );
1693
1772
  }
1694
1773
  }
1695
1774
  }
@@ -1697,9 +1776,13 @@ async function* parseSse(stream) {
1697
1776
  if (tail.startsWith("data:")) {
1698
1777
  const payload = tail.slice(5).trim();
1699
1778
  if (payload && payload !== "[DONE]") {
1779
+ frameIndex += 1;
1700
1780
  try {
1701
1781
  yield JSON.parse(payload);
1702
- } catch {
1782
+ } catch (err) {
1783
+ logger?.warn(
1784
+ `[dropshipper] sse-malformed-frame frame=${frameIndex} payload=${payload.slice(0, 200)}`
1785
+ );
1703
1786
  }
1704
1787
  }
1705
1788
  }
@@ -1707,10 +1790,11 @@ async function* parseSse(stream) {
1707
1790
  reader.releaseLock();
1708
1791
  }
1709
1792
  }
1710
- function mapUiMessageStreamToEvents(stream) {
1793
+ function mapUiMessageStreamToEvents(stream, logger) {
1711
1794
  return new ReadableStream({
1712
1795
  async start(controller) {
1713
- for await (const raw of parseSse(stream)) {
1796
+ let eventCount = 0;
1797
+ for await (const raw of parseSse(stream, logger)) {
1714
1798
  const chunk = raw;
1715
1799
  switch (chunk.type) {
1716
1800
  case "start":
@@ -1718,27 +1802,39 @@ function mapUiMessageStreamToEvents(stream) {
1718
1802
  type: "start",
1719
1803
  intent: chunk.messageMetadata?.intent
1720
1804
  });
1805
+ eventCount += 1;
1721
1806
  break;
1722
1807
  case "text-delta":
1723
1808
  if (chunk.delta !== void 0) {
1724
1809
  controller.enqueue({ type: "text-delta", delta: chunk.delta });
1810
+ eventCount += 1;
1725
1811
  }
1726
1812
  break;
1727
1813
  case "finish":
1728
1814
  controller.enqueue({
1729
1815
  type: "finish",
1730
- intent: chunk.messageMetadata?.intent ?? "other",
1816
+ intent: chunk.messageMetadata?.intent ?? "default",
1731
1817
  sources: chunk.messageMetadata?.sources ?? []
1732
1818
  });
1819
+ eventCount += 1;
1733
1820
  break;
1734
1821
  case "error":
1822
+ logger?.warn(
1823
+ `[dropshipper] chat-stream-error errorText=${chunk.errorText}`
1824
+ );
1735
1825
  controller.enqueue({
1736
1826
  type: "error",
1737
1827
  errorText: chunk.errorText ?? "Error inesperado del asistente."
1738
1828
  });
1829
+ eventCount += 1;
1739
1830
  break;
1831
+ default:
1832
+ logger?.debug(
1833
+ `[dropshipper] chat-stream-unknown-frame type=${chunk.type}`
1834
+ );
1740
1835
  }
1741
1836
  }
1837
+ logger?.debug(`[dropshipper] chat-stream-completed events=${eventCount}`);
1742
1838
  controller.close();
1743
1839
  },
1744
1840
  cancel() {