noosphere 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,10 +20,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AudioCraftProvider: () => AudioCraftProvider,
24
+ HfLocalProvider: () => HfLocalProvider,
23
25
  Noosphere: () => Noosphere,
24
26
  NoosphereError: () => NoosphereError,
27
+ OllamaProvider: () => OllamaProvider,
28
+ OpenAICompatProvider: () => OpenAICompatProvider,
25
29
  PROVIDER_IDS: () => PROVIDER_IDS,
26
30
  PROVIDER_LOGOS: () => PROVIDER_LOGOS,
31
+ WhisperLocalProvider: () => WhisperLocalProvider,
32
+ detectOpenAICompatServers: () => detectOpenAICompatServers,
27
33
  getAllProviderLogos: () => getAllProviderLogos,
28
34
  getProviderLogo: () => getProviderLogo
29
35
  });
@@ -336,20 +342,22 @@ var Registry = class {
336
342
  const cached = this.modelCache.get(provider);
337
343
  return cached?.models.find((m) => m.id === modelId) ?? null;
338
344
  }
339
- async syncProvider(providerId) {
345
+ async syncProvider(providerId, modality) {
340
346
  const provider = this.providers.get(providerId);
341
347
  if (!provider) return 0;
342
- const models = await provider.listModels();
348
+ if (modality && !provider.modalities.includes(modality)) return 0;
349
+ const models = await provider.listModels(modality);
343
350
  this.modelCache.set(providerId, { models, syncedAt: Date.now() });
344
351
  return models.length;
345
352
  }
346
- async syncAll() {
353
+ async syncAll(modality) {
347
354
  const byProvider = {};
348
355
  const errors = [];
349
356
  let synced = 0;
350
357
  for (const provider of this.providers.values()) {
358
+ if (modality && !provider.modalities.includes(modality)) continue;
351
359
  try {
352
- const count = await this.syncProvider(provider.id);
360
+ const count = await this.syncProvider(provider.id, modality);
353
361
  byProvider[provider.id] = count;
354
362
  synced += count;
355
363
  } catch (err) {
@@ -418,7 +426,7 @@ var UsageTracker = class {
418
426
  filtered = filtered.filter((e) => e.modality === options.modality);
419
427
  }
420
428
  const byProvider = {};
421
- const byModality = { llm: 0, image: 0, video: 0, tts: 0 };
429
+ const byModality = { llm: 0, image: 0, video: 0, tts: 0, stt: 0, music: 0, embedding: 0 };
422
430
  let totalCost = 0;
423
431
  for (const event of filtered) {
424
432
  totalCost += event.cost;
@@ -1043,11 +1051,55 @@ var ComfyUIProvider = class {
1043
1051
  }
1044
1052
  }
1045
1053
  async listModels(modality) {
1054
+ const models = [];
1055
+ const logo = getProviderLogo("comfyui");
1046
1056
  try {
1047
- const res = await fetch(`${this.baseUrl}/object_info`);
1048
- if (!res.ok) return [];
1049
- const models = [];
1050
- const logo = getProviderLogo("comfyui");
1057
+ const controller = new AbortController();
1058
+ const timer = setTimeout(() => controller.abort(), 5e3);
1059
+ try {
1060
+ const res = await fetch(`${this.baseUrl}/object_info`, { signal: controller.signal });
1061
+ if (res.ok) {
1062
+ const objectInfo = await res.json();
1063
+ const ckptNode = objectInfo?.["CheckpointLoaderSimple"];
1064
+ const ckptNames = ckptNode?.input?.required?.ckpt_name?.[0] ?? [];
1065
+ for (const name of ckptNames) {
1066
+ if (modality && modality !== "image") continue;
1067
+ models.push({
1068
+ id: `comfyui-ckpt-${name}`,
1069
+ provider: "comfyui",
1070
+ name: `ComfyUI: ${name}`,
1071
+ modality: "image",
1072
+ local: true,
1073
+ cost: { price: 0, unit: "free" },
1074
+ logo,
1075
+ status: "installed",
1076
+ localInfo: { sizeBytes: 0, runtime: "comfyui" },
1077
+ capabilities: { maxWidth: 2048, maxHeight: 2048, supportsNegativePrompt: true }
1078
+ });
1079
+ }
1080
+ const loraNode = objectInfo?.["LoraLoader"];
1081
+ const loraNames = loraNode?.input?.required?.lora_name?.[0] ?? [];
1082
+ for (const name of loraNames) {
1083
+ if (modality && modality !== "image") continue;
1084
+ models.push({
1085
+ id: `comfyui-lora-${name}`,
1086
+ provider: "comfyui",
1087
+ name: `LoRA: ${name}`,
1088
+ modality: "image",
1089
+ local: true,
1090
+ cost: { price: 0, unit: "free" },
1091
+ logo,
1092
+ status: "installed",
1093
+ localInfo: { sizeBytes: 0, runtime: "comfyui" }
1094
+ });
1095
+ }
1096
+ }
1097
+ } finally {
1098
+ clearTimeout(timer);
1099
+ }
1100
+ } catch {
1101
+ }
1102
+ if (models.length === 0) {
1051
1103
  if (!modality || modality === "image") {
1052
1104
  models.push({
1053
1105
  id: "comfyui-txt2img",
@@ -1072,10 +1124,44 @@ var ComfyUIProvider = class {
1072
1124
  capabilities: { maxDuration: 10, supportsImageToVideo: true }
1073
1125
  });
1074
1126
  }
1075
- return models;
1076
- } catch {
1077
- return [];
1078
1127
  }
1128
+ if (!modality || modality === "image") {
1129
+ try {
1130
+ const controller = new AbortController();
1131
+ const timer = setTimeout(() => controller.abort(), 5e3);
1132
+ try {
1133
+ const res = await fetch(
1134
+ "https://civitai.com/api/v1/models?types=Checkpoint&sort=Highest%20Rated&limit=50&nsfw=false",
1135
+ { signal: controller.signal }
1136
+ );
1137
+ if (res.ok) {
1138
+ const data = await res.json();
1139
+ for (const item of data.items ?? []) {
1140
+ const version = item.modelVersions?.[0];
1141
+ models.push({
1142
+ id: `civitai-${item.id}`,
1143
+ provider: "comfyui",
1144
+ name: item.name ?? `CivitAI Model ${item.id}`,
1145
+ description: item.description ? item.description.replace(/<[^>]+>/g, "").trim().slice(0, 300) || void 0 : void 0,
1146
+ modality: "image",
1147
+ local: true,
1148
+ cost: { price: 0, unit: "free" },
1149
+ logo,
1150
+ status: "available",
1151
+ localInfo: {
1152
+ sizeBytes: version?.files?.[0]?.sizeKB ? version.files[0].sizeKB * 1024 : 0,
1153
+ runtime: "comfyui"
1154
+ }
1155
+ });
1156
+ }
1157
+ }
1158
+ } finally {
1159
+ clearTimeout(timer);
1160
+ }
1161
+ } catch {
1162
+ }
1163
+ }
1164
+ return models;
1079
1165
  }
1080
1166
  async image(options) {
1081
1167
  const start = Date.now();
@@ -1415,6 +1501,1043 @@ var HuggingFaceProvider = class {
1415
1501
  }
1416
1502
  };
1417
1503
 
1504
+ // src/providers/ollama.ts
1505
+ var OLLAMA_FAMILY_TO_PROVIDER = {
1506
+ "llama": "meta",
1507
+ "codellama": "meta",
1508
+ "gemma": "google",
1509
+ "gemma2": "google",
1510
+ "gemma3": "google",
1511
+ "qwen": "qwen",
1512
+ "qwen2": "qwen",
1513
+ "qwen2.5": "qwen",
1514
+ "qwen3": "qwen",
1515
+ "deepseek": "deepseek",
1516
+ "deepcoder": "deepseek",
1517
+ "deepscaler": "deepseek",
1518
+ "qwq": "qwen",
1519
+ "phi": "microsoft",
1520
+ "phi3": "microsoft",
1521
+ "phi4": "microsoft",
1522
+ "mistral": "mistral",
1523
+ "mixtral": "mistral",
1524
+ "codestral": "mistral",
1525
+ "ministral": "mistral",
1526
+ "nemotron": "nvidia",
1527
+ "command": "cohere",
1528
+ "command-r": "cohere",
1529
+ "gpt-oss": "openai",
1530
+ "starcoder": "huggingface",
1531
+ "falcon": "meta",
1532
+ "glm": "zai",
1533
+ "granite": "ibm",
1534
+ "olmo": "meta",
1535
+ "yi": "zai",
1536
+ "minimax": "minimax",
1537
+ "kimi": "meta",
1538
+ "dolphin": "ollama",
1539
+ "wizard": "ollama",
1540
+ "nomic": "ollama",
1541
+ "mxbai": "ollama",
1542
+ "bge": "ollama",
1543
+ "all-minilm": "ollama",
1544
+ "moondream": "ollama"
1545
+ };
1546
+ var VISION_MODELS = /* @__PURE__ */ new Set([
1547
+ "llava",
1548
+ "moondream",
1549
+ "minicpm-v",
1550
+ "llama3.2-vision",
1551
+ "qwen2.5vl",
1552
+ "gemma3",
1553
+ "llava-llama3",
1554
+ "llava-phi3",
1555
+ "bakllava"
1556
+ ]);
1557
+ function inferLogoProvider(modelName, _family) {
1558
+ const base = modelName.split(":")[0].toLowerCase().replace(/^[^/]+\//, "");
1559
+ const sortedPrefixes = Object.entries(OLLAMA_FAMILY_TO_PROVIDER).sort((a, b) => b[0].length - a[0].length);
1560
+ for (const [prefix, provider] of sortedPrefixes) {
1561
+ if (base === prefix || base.startsWith(prefix)) return provider;
1562
+ }
1563
+ return "ollama";
1564
+ }
1565
+ function supportsVision(modelName) {
1566
+ const base = modelName.split(":")[0].toLowerCase();
1567
+ for (const v of VISION_MODELS) {
1568
+ if (base === v || base.startsWith(v)) return true;
1569
+ }
1570
+ return false;
1571
+ }
1572
+ async function fetchJson(url, options) {
1573
+ const timeoutMs = options?.timeoutMs ?? 5e3;
1574
+ const controller = new AbortController();
1575
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1576
+ try {
1577
+ const res = await fetch(url, { ...options, signal: controller.signal });
1578
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1579
+ return await res.json();
1580
+ } finally {
1581
+ clearTimeout(timer);
1582
+ }
1583
+ }
1584
+ async function fetchOllamaDescriptions() {
1585
+ const controller = new AbortController();
1586
+ const timer = setTimeout(() => controller.abort(), 5e3);
1587
+ try {
1588
+ const res = await fetch("https://ollama.com/library", { signal: controller.signal });
1589
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1590
+ const html = await res.text();
1591
+ const descriptions = /* @__PURE__ */ new Map();
1592
+ const cardRegex = /href="\/library\/([^"]+)"[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/g;
1593
+ let match;
1594
+ while ((match = cardRegex.exec(html)) !== null) {
1595
+ const modelName = match[1].trim();
1596
+ const desc = match[2].replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
1597
+ if (modelName && desc) {
1598
+ descriptions.set(modelName, desc);
1599
+ }
1600
+ }
1601
+ return descriptions;
1602
+ } finally {
1603
+ clearTimeout(timer);
1604
+ }
1605
+ }
1606
+ var OllamaProvider = class {
1607
+ id = "ollama";
1608
+ name = "Ollama (Local)";
1609
+ modalities = ["llm"];
1610
+ isLocal = true;
1611
+ baseUrl;
1612
+ constructor(config) {
1613
+ const host = config?.host ?? "http://localhost";
1614
+ const port = config?.port ?? 11434;
1615
+ const cleanHost = host.replace(/\/+$/, "");
1616
+ const hasPort = /:\d+$/.test(cleanHost);
1617
+ this.baseUrl = hasPort ? cleanHost : `${cleanHost}:${port}`;
1618
+ }
1619
+ async ping() {
1620
+ try {
1621
+ await fetchJson(`${this.baseUrl}/api/version`, { timeoutMs: 2e3 });
1622
+ return true;
1623
+ } catch {
1624
+ return false;
1625
+ }
1626
+ }
1627
+ async listModels(_modality) {
1628
+ if (_modality && _modality !== "llm") return [];
1629
+ const [localData, catalogData, runningData, descriptions] = await Promise.all([
1630
+ fetchJson(`${this.baseUrl}/api/tags`, { timeoutMs: 5e3 }).catch(() => null),
1631
+ fetchJson("https://ollama.com/api/tags", { timeoutMs: 5e3 }).catch(() => null),
1632
+ fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 }).catch(() => null),
1633
+ fetchOllamaDescriptions().catch(() => /* @__PURE__ */ new Map())
1634
+ ]);
1635
+ const runningNames = /* @__PURE__ */ new Set();
1636
+ if (runningData?.models) {
1637
+ for (const m of runningData.models) {
1638
+ runningNames.add(m.name);
1639
+ runningNames.add(m.model);
1640
+ }
1641
+ }
1642
+ const models = /* @__PURE__ */ new Map();
1643
+ if (localData?.models) {
1644
+ for (const m of localData.models) {
1645
+ const isRunning = runningNames.has(m.name) || runningNames.has(m.model);
1646
+ models.set(m.name, this.toModelInfo(m, isRunning ? "running" : "installed", true, descriptions));
1647
+ }
1648
+ }
1649
+ if (catalogData?.models) {
1650
+ for (const m of catalogData.models) {
1651
+ const name = m.name;
1652
+ if (!models.has(name)) {
1653
+ models.set(name, this.toModelInfo(m, "available", false, descriptions));
1654
+ }
1655
+ }
1656
+ }
1657
+ return Array.from(models.values());
1658
+ }
1659
+ toModelInfo(m, status, isLocal, descriptions) {
1660
+ const name = m.name ?? m.model ?? "unknown";
1661
+ const family = m.details?.family;
1662
+ const logoProvider = inferLogoProvider(name, family);
1663
+ const baseName = name.split(":")[0];
1664
+ const description = descriptions?.get(baseName);
1665
+ return {
1666
+ id: name,
1667
+ provider: "ollama",
1668
+ name,
1669
+ ...description ? { description } : {},
1670
+ modality: "llm",
1671
+ local: true,
1672
+ cost: { price: 0, unit: "free" },
1673
+ logo: getProviderLogo(logoProvider),
1674
+ status,
1675
+ localInfo: {
1676
+ sizeBytes: m.size ?? 0,
1677
+ family: family ?? m.details?.family,
1678
+ parameterSize: m.details?.parameter_size,
1679
+ quantization: m.details?.quantization_level,
1680
+ format: m.details?.format,
1681
+ digest: m.digest,
1682
+ modifiedAt: m.modified_at,
1683
+ running: status === "running",
1684
+ runtime: "ollama"
1685
+ },
1686
+ capabilities: {
1687
+ contextWindow: 128e3,
1688
+ supportsVision: supportsVision(name),
1689
+ supportsStreaming: true
1690
+ }
1691
+ };
1692
+ }
1693
+ async chat(options) {
1694
+ const start = Date.now();
1695
+ const messages = options.messages.map((m) => ({
1696
+ role: m.role,
1697
+ content: m.content
1698
+ }));
1699
+ const body = {
1700
+ model: options.model ?? "llama3.2",
1701
+ messages,
1702
+ stream: false
1703
+ };
1704
+ if (options.temperature !== void 0 || options.maxTokens !== void 0) {
1705
+ body.options = {};
1706
+ if (options.temperature !== void 0) body.options.temperature = options.temperature;
1707
+ if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
1708
+ }
1709
+ const res = await fetch(`${this.baseUrl}/api/chat`, {
1710
+ method: "POST",
1711
+ headers: { "Content-Type": "application/json" },
1712
+ body: JSON.stringify(body)
1713
+ });
1714
+ if (!res.ok) {
1715
+ throw new Error(`Ollama chat failed: ${res.status} ${await res.text()}`);
1716
+ }
1717
+ const data = await res.json();
1718
+ return {
1719
+ content: data.message?.content ?? "",
1720
+ provider: "ollama",
1721
+ model: options.model ?? "llama3.2",
1722
+ modality: "llm",
1723
+ latencyMs: Date.now() - start,
1724
+ usage: {
1725
+ cost: 0,
1726
+ input: data.prompt_eval_count ?? 0,
1727
+ output: data.eval_count ?? 0,
1728
+ unit: "tokens"
1729
+ }
1730
+ };
1731
+ }
1732
+ stream(options) {
1733
+ const self = this;
1734
+ const start = Date.now();
1735
+ let aborted = false;
1736
+ let resolveResult = null;
1737
+ let rejectResult = null;
1738
+ const resultPromise = new Promise((resolve, reject) => {
1739
+ resolveResult = resolve;
1740
+ rejectResult = reject;
1741
+ });
1742
+ const messages = options.messages.map((m) => ({
1743
+ role: m.role,
1744
+ content: m.content
1745
+ }));
1746
+ const body = {
1747
+ model: options.model ?? "llama3.2",
1748
+ messages,
1749
+ stream: true
1750
+ };
1751
+ if (options.temperature !== void 0 || options.maxTokens !== void 0) {
1752
+ body.options = {};
1753
+ if (options.temperature !== void 0) body.options.temperature = options.temperature;
1754
+ if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
1755
+ }
1756
+ const asyncIterator = {
1757
+ async *[Symbol.asyncIterator]() {
1758
+ try {
1759
+ const res = await fetch(`${self.baseUrl}/api/chat`, {
1760
+ method: "POST",
1761
+ headers: { "Content-Type": "application/json" },
1762
+ body: JSON.stringify(body)
1763
+ });
1764
+ if (!res.ok) {
1765
+ throw new Error(`Ollama stream failed: ${res.status} ${await res.text()}`);
1766
+ }
1767
+ const reader = res.body.getReader();
1768
+ const decoder = new TextDecoder();
1769
+ let fullContent = "";
1770
+ let finalData = null;
1771
+ let buffer = "";
1772
+ while (!aborted) {
1773
+ const { done, value } = await reader.read();
1774
+ if (done) break;
1775
+ buffer += decoder.decode(value, { stream: true });
1776
+ const lines = buffer.split("\n");
1777
+ buffer = lines.pop() ?? "";
1778
+ for (const line of lines) {
1779
+ if (!line.trim()) continue;
1780
+ try {
1781
+ const chunk = JSON.parse(line);
1782
+ if (chunk.message?.content) {
1783
+ fullContent += chunk.message.content;
1784
+ yield { type: "text_delta", delta: chunk.message.content };
1785
+ }
1786
+ if (chunk.done) {
1787
+ finalData = chunk;
1788
+ }
1789
+ } catch {
1790
+ }
1791
+ }
1792
+ }
1793
+ const result = {
1794
+ content: fullContent,
1795
+ provider: "ollama",
1796
+ model: options.model ?? "llama3.2",
1797
+ modality: "llm",
1798
+ latencyMs: Date.now() - start,
1799
+ usage: {
1800
+ cost: 0,
1801
+ input: finalData?.prompt_eval_count ?? 0,
1802
+ output: finalData?.eval_count ?? 0,
1803
+ unit: "tokens"
1804
+ }
1805
+ };
1806
+ resolveResult?.(result);
1807
+ yield { type: "done", result };
1808
+ } catch (err) {
1809
+ const error = err instanceof Error ? err : new Error(String(err));
1810
+ rejectResult?.(error);
1811
+ yield { type: "error", error };
1812
+ }
1813
+ }
1814
+ };
1815
+ return {
1816
+ [Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
1817
+ result: () => resultPromise,
1818
+ abort: () => {
1819
+ aborted = true;
1820
+ }
1821
+ };
1822
+ }
1823
+ // --- Extra model management methods ---
1824
+ async *pullModel(name) {
1825
+ const res = await fetch(`${this.baseUrl}/api/pull`, {
1826
+ method: "POST",
1827
+ headers: { "Content-Type": "application/json" },
1828
+ body: JSON.stringify({ name, stream: true })
1829
+ });
1830
+ if (!res.ok) {
1831
+ throw new Error(`Ollama pull failed: ${res.status} ${await res.text()}`);
1832
+ }
1833
+ const reader = res.body.getReader();
1834
+ const decoder = new TextDecoder();
1835
+ let buffer = "";
1836
+ while (true) {
1837
+ const { done, value } = await reader.read();
1838
+ if (done) break;
1839
+ buffer += decoder.decode(value, { stream: true });
1840
+ const lines = buffer.split("\n");
1841
+ buffer = lines.pop() ?? "";
1842
+ for (const line of lines) {
1843
+ if (!line.trim()) continue;
1844
+ try {
1845
+ yield JSON.parse(line);
1846
+ } catch {
1847
+ }
1848
+ }
1849
+ }
1850
+ }
1851
+ async deleteModel(name) {
1852
+ const res = await fetch(`${this.baseUrl}/api/delete`, {
1853
+ method: "DELETE",
1854
+ headers: { "Content-Type": "application/json" },
1855
+ body: JSON.stringify({ name })
1856
+ });
1857
+ if (!res.ok) {
1858
+ throw new Error(`Ollama delete failed: ${res.status} ${await res.text()}`);
1859
+ }
1860
+ }
1861
+ async showModel(name) {
1862
+ const res = await fetch(`${this.baseUrl}/api/show`, {
1863
+ method: "POST",
1864
+ headers: { "Content-Type": "application/json" },
1865
+ body: JSON.stringify({ name })
1866
+ });
1867
+ if (!res.ok) {
1868
+ throw new Error(`Ollama show failed: ${res.status} ${await res.text()}`);
1869
+ }
1870
+ return await res.json();
1871
+ }
1872
+ async getRunningModels() {
1873
+ const data = await fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 });
1874
+ return data?.models ?? [];
1875
+ }
1876
+ };
1877
+
1878
+ // src/utils/parse-readme.ts
1879
+ async function fetchReadmeDescription(modelId, timeoutMs = 5e3) {
1880
+ try {
1881
+ const controller = new AbortController();
1882
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1883
+ try {
1884
+ const res = await fetch(
1885
+ `https://huggingface.co/${modelId}/raw/main/README.md`,
1886
+ { signal: controller.signal }
1887
+ );
1888
+ if (!res.ok) return void 0;
1889
+ const text = await res.text();
1890
+ return parseReadmeDescription(text);
1891
+ } finally {
1892
+ clearTimeout(timer);
1893
+ }
1894
+ } catch {
1895
+ return void 0;
1896
+ }
1897
+ }
1898
+ function parseReadmeDescription(readme) {
1899
+ const withoutFrontmatter = readme.replace(/^---[\s\S]*?---\s*/, "");
1900
+ const lines = withoutFrontmatter.split("\n");
1901
+ let paragraph = "";
1902
+ for (const line of lines) {
1903
+ const trimmed = line.trim();
1904
+ if (!trimmed) {
1905
+ if (paragraph) break;
1906
+ continue;
1907
+ }
1908
+ if (trimmed.startsWith("#")) {
1909
+ if (paragraph) break;
1910
+ continue;
1911
+ }
1912
+ if (/^\[?!\[/.test(trimmed) || /^</.test(trimmed)) continue;
1913
+ if (/^\[.*\]\(.*\)$/.test(trimmed)) continue;
1914
+ paragraph += (paragraph ? " " : "") + trimmed;
1915
+ }
1916
+ if (!paragraph) return void 0;
1917
+ paragraph = paragraph.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/<[^>]+>/g, "").trim();
1918
+ if (!paragraph) return void 0;
1919
+ if (paragraph.length > 300) paragraph = paragraph.slice(0, 297) + "...";
1920
+ return paragraph;
1921
+ }
1922
+
1923
+ // src/providers/hf-local.ts
1924
+ var import_promises = require("fs/promises");
1925
+ var import_node_path = require("path");
1926
+ var import_node_os = require("os");
1927
+ var FETCH_TIMEOUT_MS3 = 5e3;
1928
+ var HF_HUB_API2 = "https://huggingface.co/api/models";
1929
+ var HF_ORG_TO_LOGO_PROVIDER = {
1930
+ "meta-llama": "meta",
1931
+ "facebook": "meta",
1932
+ "google": "google",
1933
+ "microsoft": "microsoft",
1934
+ "nvidia": "nvidia",
1935
+ "mistralai": "mistral",
1936
+ "Qwen": "qwen",
1937
+ "deepseek-ai": "deepseek",
1938
+ "openai": "openai",
1939
+ "CohereForAI": "cohere",
1940
+ "rhasspy": "piper",
1941
+ "stabilityai": "huggingface",
1942
+ "black-forest-labs": "huggingface",
1943
+ "tiiuae": "huggingface",
1944
+ "allenai": "huggingface",
1945
+ "Salesforce": "huggingface"
1946
+ };
1947
+ var PIPELINE_TAG_TO_MODALITY = {
1948
+ "text-to-image": "image",
1949
+ "text-to-video": "video",
1950
+ "text-to-audio": "music",
1951
+ "text-to-speech": "tts",
1952
+ "automatic-speech-recognition": "stt"
1953
+ };
1954
+ var CATALOG_QUERIES = [
1955
+ { pipeline_tag: "text-to-image", limit: 50 },
1956
+ { pipeline_tag: "text-to-video", limit: 30 },
1957
+ { pipeline_tag: "text-to-audio", limit: 30 },
1958
+ { pipeline_tag: "text-to-speech", limit: 30 },
1959
+ { pipeline_tag: "automatic-speech-recognition", limit: 30 },
1960
+ { pipeline_tag: "text-to-image", limit: 100, library: "diffusers" }
1961
+ ];
1962
+ async function fetchJsonTimeout(url, timeoutMs = FETCH_TIMEOUT_MS3) {
1963
+ const controller = new AbortController();
1964
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1965
+ try {
1966
+ const res = await fetch(url, { signal: controller.signal });
1967
+ if (!res.ok) return null;
1968
+ return await res.json();
1969
+ } catch {
1970
+ return null;
1971
+ } finally {
1972
+ clearTimeout(timer);
1973
+ }
1974
+ }
1975
+ var HfLocalProvider = class {
1976
+ id = "hf-local";
1977
+ name = "HuggingFace Local Models";
1978
+ modalities = ["image", "video", "tts", "stt", "music"];
1979
+ isLocal = true;
1980
+ cachedModels = null;
1981
+ async ping() {
1982
+ return true;
1983
+ }
1984
+ async listModels(modality) {
1985
+ if (!this.cachedModels) {
1986
+ const [catalog, installed] = await Promise.all([
1987
+ this.fetchCatalog(),
1988
+ this.scanLocalCache()
1989
+ ]);
1990
+ const modelMap = /* @__PURE__ */ new Map();
1991
+ for (const m of catalog) modelMap.set(m.id, m);
1992
+ for (const m of installed) modelMap.set(m.id, m);
1993
+ this.cachedModels = Array.from(modelMap.values());
1994
+ }
1995
+ if (modality) return this.cachedModels.filter((m) => m.modality === modality);
1996
+ return this.cachedModels;
1997
+ }
1998
+ async fetchCatalog() {
1999
+ const seen = /* @__PURE__ */ new Set();
2000
+ const entries = [];
2001
+ const results = await Promise.allSettled(
2002
+ CATALOG_QUERIES.map(async (q) => {
2003
+ const params = new URLSearchParams({
2004
+ pipeline_tag: q.pipeline_tag,
2005
+ sort: "downloads",
2006
+ limit: String(q.limit)
2007
+ });
2008
+ if (q.library) params.set("library", q.library);
2009
+ return fetchJsonTimeout(`${HF_HUB_API2}?${params}`);
2010
+ })
2011
+ );
2012
+ for (const result of results) {
2013
+ if (result.status !== "fulfilled" || !Array.isArray(result.value)) continue;
2014
+ for (const entry of result.value) {
2015
+ const id = entry.id ?? entry.modelId;
2016
+ if (!id || seen.has(id)) continue;
2017
+ seen.add(id);
2018
+ entries.push({
2019
+ id,
2020
+ pipelineTag: entry.pipeline_tag ?? "",
2021
+ libraryName: entry.library_name
2022
+ });
2023
+ }
2024
+ }
2025
+ const descriptionMap = /* @__PURE__ */ new Map();
2026
+ for (let i = 0; i < entries.length; i += 10) {
2027
+ const batch = entries.slice(i, i + 10);
2028
+ const descs = await Promise.allSettled(
2029
+ batch.map(async (e) => {
2030
+ const desc = await fetchReadmeDescription(e.id);
2031
+ return { id: e.id, desc };
2032
+ })
2033
+ );
2034
+ for (const d of descs) {
2035
+ if (d.status === "fulfilled" && d.value.desc) {
2036
+ descriptionMap.set(d.value.id, d.value.desc);
2037
+ }
2038
+ }
2039
+ }
2040
+ const models = [];
2041
+ for (const e of entries) {
2042
+ const modality = PIPELINE_TAG_TO_MODALITY[e.pipelineTag] ?? "image";
2043
+ const org = e.id.includes("/") ? e.id.split("/")[0] : void 0;
2044
+ const logoProvider = org ? HF_ORG_TO_LOGO_PROVIDER[org] ?? "huggingface" : "huggingface";
2045
+ models.push({
2046
+ id: e.id,
2047
+ provider: "hf-local",
2048
+ name: e.id.split("/").pop() ?? e.id,
2049
+ modality,
2050
+ local: true,
2051
+ cost: { price: 0, unit: "free" },
2052
+ logo: getProviderLogo(logoProvider),
2053
+ description: descriptionMap.get(e.id),
2054
+ status: "available",
2055
+ localInfo: {
2056
+ sizeBytes: 0,
2057
+ runtime: "huggingface",
2058
+ family: e.libraryName
2059
+ },
2060
+ capabilities: {}
2061
+ });
2062
+ }
2063
+ return models;
2064
+ }
2065
+ async scanLocalCache() {
2066
+ const models = [];
2067
+ const cacheDir = (0, import_node_path.join)((0, import_node_os.homedir)(), ".cache", "huggingface", "hub");
2068
+ try {
2069
+ const entries = await (0, import_promises.readdir)(cacheDir, { withFileTypes: true });
2070
+ for (const entry of entries) {
2071
+ if (!entry.isDirectory() || !entry.name.startsWith("models--")) continue;
2072
+ const parts = entry.name.replace("models--", "").split("--");
2073
+ const modelId = parts.join("/");
2074
+ const modelDir = (0, import_node_path.join)(cacheDir, entry.name);
2075
+ let snapshotHash;
2076
+ try {
2077
+ snapshotHash = (await (0, import_promises.readFile)((0, import_node_path.join)(modelDir, "refs", "main"), "utf-8")).trim();
2078
+ } catch {
2079
+ continue;
2080
+ }
2081
+ let pipelineTag = "";
2082
+ const snapshotDir = (0, import_node_path.join)(modelDir, "snapshots", snapshotHash);
2083
+ try {
2084
+ const modelIndex = JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(snapshotDir, "model_index.json"), "utf-8"));
2085
+ if (modelIndex._class_name?.includes("Stable") || modelIndex._class_name?.includes("Flux")) {
2086
+ pipelineTag = "text-to-image";
2087
+ } else if (modelIndex._class_name?.includes("Video") || modelIndex._class_name?.includes("Animate")) {
2088
+ pipelineTag = "text-to-video";
2089
+ }
2090
+ } catch {
2091
+ try {
2092
+ const config = JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(snapshotDir, "config.json"), "utf-8"));
2093
+ if (config.task_specific_params?.["text-to-image"]) pipelineTag = "text-to-image";
2094
+ else if (config.model_type?.includes("whisper")) pipelineTag = "automatic-speech-recognition";
2095
+ } catch {
2096
+ }
2097
+ }
2098
+ const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
2099
+ const org = modelId.includes("/") ? modelId.split("/")[0] : void 0;
2100
+ const logoProvider = org ? HF_ORG_TO_LOGO_PROVIDER[org] ?? "huggingface" : "huggingface";
2101
+ models.push({
2102
+ id: modelId,
2103
+ provider: "hf-local",
2104
+ name: modelId.split("/").pop() ?? modelId,
2105
+ modality,
2106
+ local: true,
2107
+ cost: { price: 0, unit: "free" },
2108
+ logo: getProviderLogo(logoProvider),
2109
+ status: "installed",
2110
+ localInfo: {
2111
+ sizeBytes: 0,
2112
+ runtime: "huggingface",
2113
+ diskPath: snapshotDir
2114
+ }
2115
+ });
2116
+ }
2117
+ } catch {
2118
+ }
2119
+ return models;
2120
+ }
2121
+ };
2122
+
2123
+ // src/providers/whisper-local.ts
2124
+ var import_node_child_process = require("child_process");
2125
+ var import_promises2 = require("fs/promises");
2126
+ var import_node_path2 = require("path");
2127
+ var import_node_os2 = require("os");
2128
+ var WHISPER_MODELS = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "turbo"];
2129
+ var WHISPER_HF_REPOS = {
2130
+ "tiny": "openai/whisper-tiny",
2131
+ "base": "openai/whisper-base",
2132
+ "small": "openai/whisper-small",
2133
+ "medium": "openai/whisper-medium",
2134
+ "large": "openai/whisper-large",
2135
+ "large-v2": "openai/whisper-large-v2",
2136
+ "large-v3": "openai/whisper-large-v3",
2137
+ "turbo": "openai/whisper-large-v3-turbo"
2138
+ };
2139
+ async function fetchWhisperDescriptions() {
2140
+ const descriptions = /* @__PURE__ */ new Map();
2141
+ const fetches = Object.entries(WHISPER_HF_REPOS).map(async ([size, repo]) => {
2142
+ const desc = await fetchReadmeDescription(repo, 8e3);
2143
+ if (desc) descriptions.set(size, desc);
2144
+ });
2145
+ await Promise.allSettled(fetches);
2146
+ return descriptions;
2147
+ }
2148
+ function runPython(code, timeoutMs = 5e3) {
2149
+ return new Promise((resolve, reject) => {
2150
+ const proc = (0, import_node_child_process.execFile)("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
2151
+ if (err) reject(err);
2152
+ else resolve(stdout.trim());
2153
+ });
2154
+ });
2155
+ }
2156
+ async function fileExists(path) {
2157
+ try {
2158
+ await (0, import_promises2.access)(path);
2159
+ return true;
2160
+ } catch {
2161
+ return false;
2162
+ }
2163
+ }
2164
+ var WhisperLocalProvider = class {
2165
+ id = "whisper-local";
2166
+ name = "Whisper (Local)";
2167
+ modalities = ["stt"];
2168
+ isLocal = true;
2169
+ runtime = null;
2170
+ async ping() {
2171
+ return await this.detectRuntime() !== null;
2172
+ }
2173
+ async detectRuntime() {
2174
+ if (this.runtime) return this.runtime;
2175
+ try {
2176
+ await runPython('import faster_whisper; print("ok")');
2177
+ this.runtime = "faster-whisper";
2178
+ return this.runtime;
2179
+ } catch {
2180
+ }
2181
+ try {
2182
+ await runPython("import whisper; print(whisper.__version__)");
2183
+ this.runtime = "whisper";
2184
+ return this.runtime;
2185
+ } catch {
2186
+ }
2187
+ return null;
2188
+ }
2189
+ async listModels(_modality) {
2190
+ if (_modality && _modality !== "stt") return [];
2191
+ const runtime = await this.detectRuntime();
2192
+ if (!runtime) return [];
2193
+ const descMap = await fetchWhisperDescriptions().catch(() => /* @__PURE__ */ new Map());
2194
+ const logo = getProviderLogo("openai");
2195
+ const models = [];
2196
+ for (const name of WHISPER_MODELS) {
2197
+ const installed = await this.isModelCached(name, runtime);
2198
+ models.push({
2199
+ id: `whisper-${name}`,
2200
+ provider: "whisper-local",
2201
+ name: `Whisper ${name}`,
2202
+ description: descMap.get(name),
2203
+ modality: "stt",
2204
+ local: true,
2205
+ cost: { price: 0, unit: "free" },
2206
+ logo,
2207
+ status: installed ? "installed" : "available",
2208
+ localInfo: {
2209
+ sizeBytes: 0,
2210
+ runtime
2211
+ }
2212
+ });
2213
+ }
2214
+ return models;
2215
+ }
2216
+ async isModelCached(name, runtime) {
2217
+ if (runtime === "whisper") {
2218
+ return fileExists((0, import_node_path2.join)((0, import_node_os2.homedir)(), ".cache", "whisper", `${name}.pt`));
2219
+ }
2220
+ const hfDir = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".cache", "huggingface", "hub", `models--Systran--faster-whisper-${name}`);
2221
+ return fileExists(hfDir);
2222
+ }
2223
+ async transcribe(options) {
2224
+ const runtime = await this.detectRuntime();
2225
+ if (!runtime) throw new Error("Whisper is not installed");
2226
+ const model = options.model?.replace("whisper-", "") ?? "base";
2227
+ const lang = options.language ? `--language ${options.language}` : "";
2228
+ const task = options.task ?? "transcribe";
2229
+ if (runtime === "faster-whisper") {
2230
+ const code = `
2231
+ import json, sys
2232
+ from faster_whisper import WhisperModel
2233
+ model = WhisperModel("${model}")
2234
+ segments, info = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
2235
+ segs = [{"start": s.start, "end": s.end, "text": s.text} for s in segments]
2236
+ print(json.dumps({"text": " ".join(s["text"] for s in segs), "language": info.language, "duration": info.duration, "segments": segs}))
2237
+ `;
2238
+ const output = await runPython(code, 12e4);
2239
+ return JSON.parse(output);
2240
+ } else {
2241
+ const code = `
2242
+ import json, whisper
2243
+ model = whisper.load_model("${model}")
2244
+ result = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
2245
+ segs = [{"start": s["start"], "end": s["end"], "text": s["text"]} for s in result.get("segments", [])]
2246
+ print(json.dumps({"text": result["text"], "language": result.get("language", ""), "duration": 0, "segments": segs}))
2247
+ `;
2248
+ const output = await runPython(code, 12e4);
2249
+ return JSON.parse(output);
2250
+ }
2251
+ }
2252
+ };
2253
+
2254
+ // src/providers/audiocraft.ts
2255
+ var import_node_child_process2 = require("child_process");
2256
+ var import_promises3 = require("fs/promises");
2257
+ var import_node_path3 = require("path");
2258
+ var import_node_os3 = require("os");
2259
+ var AUDIOCRAFT_MODELS = [
2260
+ { id: "musicgen-small", name: "MusicGen Small" },
2261
+ { id: "musicgen-medium", name: "MusicGen Medium" },
2262
+ { id: "musicgen-large", name: "MusicGen Large" },
2263
+ { id: "musicgen-melody", name: "MusicGen Melody" },
2264
+ { id: "audiogen-medium", name: "AudioGen Medium" }
2265
+ ];
2266
+ var AUDIOCRAFT_HF_REPOS = {
2267
+ "musicgen-small": "facebook/musicgen-small",
2268
+ "musicgen-medium": "facebook/musicgen-medium",
2269
+ "musicgen-large": "facebook/musicgen-large",
2270
+ "musicgen-melody": "facebook/musicgen-melody",
2271
+ "audiogen-medium": "facebook/audiogen-medium"
2272
+ };
2273
+ async function fetchAudioCraftDescriptions() {
2274
+ const descriptions = /* @__PURE__ */ new Map();
2275
+ const fetches = Object.entries(AUDIOCRAFT_HF_REPOS).map(async ([id, repo]) => {
2276
+ const desc = await fetchReadmeDescription(repo, 8e3);
2277
+ if (desc) descriptions.set(id, desc);
2278
+ });
2279
+ await Promise.allSettled(fetches);
2280
+ return descriptions;
2281
+ }
2282
+ function runPython2(code, timeoutMs = 5e3) {
2283
+ return new Promise((resolve, reject) => {
2284
+ (0, import_node_child_process2.execFile)("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
2285
+ if (err) reject(err);
2286
+ else resolve(stdout.trim());
2287
+ });
2288
+ });
2289
+ }
2290
+ async function fileExists2(path) {
2291
+ try {
2292
+ await (0, import_promises3.access)(path);
2293
+ return true;
2294
+ } catch {
2295
+ return false;
2296
+ }
2297
+ }
2298
+ var AudioCraftProvider = class {
2299
+ id = "audiocraft";
2300
+ name = "AudioCraft (Local)";
2301
+ modalities = ["music"];
2302
+ isLocal = true;
2303
+ detected = null;
2304
+ async ping() {
2305
+ if (this.detected !== null) return this.detected;
2306
+ try {
2307
+ await runPython2('import audiocraft; print("ok")');
2308
+ this.detected = true;
2309
+ } catch {
2310
+ this.detected = false;
2311
+ }
2312
+ return this.detected;
2313
+ }
2314
+ async listModels(_modality) {
2315
+ if (_modality && _modality !== "music") return [];
2316
+ if (!await this.ping()) return [];
2317
+ const descMap = await fetchAudioCraftDescriptions().catch(() => /* @__PURE__ */ new Map());
2318
+ const logo = getProviderLogo("meta");
2319
+ const models = [];
2320
+ for (const m of AUDIOCRAFT_MODELS) {
2321
+ const hfDir = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".cache", "huggingface", "hub", `models--facebook--${m.id}`);
2322
+ const installed = await fileExists2(hfDir);
2323
+ models.push({
2324
+ id: m.id,
2325
+ provider: "audiocraft",
2326
+ name: m.name,
2327
+ description: descMap.get(m.id),
2328
+ modality: "music",
2329
+ local: true,
2330
+ cost: { price: 0, unit: "free" },
2331
+ logo,
2332
+ status: installed ? "installed" : "available",
2333
+ localInfo: {
2334
+ sizeBytes: 0,
2335
+ runtime: "audiocraft"
2336
+ }
2337
+ });
2338
+ }
2339
+ return models;
2340
+ }
2341
+ };
2342
+
2343
+ // src/providers/openai-compat.ts
2344
+ var FETCH_TIMEOUT_MS4 = 5e3;
2345
+ var KNOWN_LOCAL_SERVERS = [
2346
+ { port: 8080, name: "llama.cpp / LocalAI", id: "llamacpp" },
2347
+ { port: 1234, name: "LM Studio", id: "lmstudio" },
2348
+ { port: 8e3, name: "vLLM", id: "vllm" },
2349
+ { port: 5e3, name: "TabbyAPI", id: "tabbyapi" },
2350
+ { port: 5001, name: "KoboldCpp", id: "koboldcpp" },
2351
+ { port: 1337, name: "Jan", id: "jan" }
2352
+ ];
2353
+ async function fetchJsonTimeout2(url, headers, timeoutMs = FETCH_TIMEOUT_MS4) {
2354
+ const controller = new AbortController();
2355
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2356
+ try {
2357
+ const res = await fetch(url, { signal: controller.signal, headers });
2358
+ if (!res.ok) return null;
2359
+ return await res.json();
2360
+ } catch {
2361
+ return null;
2362
+ } finally {
2363
+ clearTimeout(timer);
2364
+ }
2365
+ }
2366
+ var OpenAICompatProvider = class {
2367
+ id;
2368
+ name;
2369
+ modalities = ["llm"];
2370
+ isLocal = true;
2371
+ baseUrl;
2372
+ headers;
2373
+ constructor(config) {
2374
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
2375
+ this.id = config.id ?? `openai-compat-${new URL(config.baseUrl).port}`;
2376
+ this.name = config.name ?? `OpenAI-Compatible (${this.baseUrl})`;
2377
+ this.headers = { "Content-Type": "application/json" };
2378
+ if (config.apiKey) {
2379
+ this.headers["Authorization"] = `Bearer ${config.apiKey}`;
2380
+ }
2381
+ }
2382
+ async ping() {
2383
+ const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers, 2e3);
2384
+ return data !== null;
2385
+ }
2386
+ async listModels(_modality) {
2387
+ if (_modality && _modality !== "llm") return [];
2388
+ const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers);
2389
+ if (!data?.data || !Array.isArray(data.data)) return [];
2390
+ const logo = getProviderLogo("openai");
2391
+ return data.data.map((m) => ({
2392
+ id: m.id,
2393
+ provider: this.id,
2394
+ name: m.id,
2395
+ modality: "llm",
2396
+ local: true,
2397
+ cost: { price: 0, unit: "free" },
2398
+ logo,
2399
+ status: "running",
2400
+ localInfo: {
2401
+ sizeBytes: 0,
2402
+ runtime: this.id
2403
+ },
2404
+ capabilities: {
2405
+ supportsStreaming: true
2406
+ }
2407
+ }));
2408
+ }
2409
+ async chat(options) {
2410
+ const start = Date.now();
2411
+ const model = options.model ?? "default";
2412
+ const body = {
2413
+ model,
2414
+ messages: options.messages,
2415
+ stream: false
2416
+ };
2417
+ if (options.temperature !== void 0) body.temperature = options.temperature;
2418
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
2419
+ if (options.jsonMode) body.response_format = { type: "json_object" };
2420
+ const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
2421
+ method: "POST",
2422
+ headers: this.headers,
2423
+ body: JSON.stringify(body)
2424
+ });
2425
+ if (!res.ok) throw new Error(`OpenAI-compat chat failed: ${res.status} ${await res.text()}`);
2426
+ const data = await res.json();
2427
+ const choice = data.choices?.[0];
2428
+ return {
2429
+ content: choice?.message?.content ?? "",
2430
+ provider: this.id,
2431
+ model,
2432
+ modality: "llm",
2433
+ latencyMs: Date.now() - start,
2434
+ usage: {
2435
+ cost: 0,
2436
+ input: data.usage?.prompt_tokens ?? 0,
2437
+ output: data.usage?.completion_tokens ?? 0,
2438
+ unit: "tokens"
2439
+ }
2440
+ };
2441
+ }
2442
+ stream(options) {
2443
+ const self = this;
2444
+ const start = Date.now();
2445
+ let aborted = false;
2446
+ let resolveResult = null;
2447
+ let rejectResult = null;
2448
+ const resultPromise = new Promise((resolve, reject) => {
2449
+ resolveResult = resolve;
2450
+ rejectResult = reject;
2451
+ });
2452
+ const model = options.model ?? "default";
2453
+ const body = {
2454
+ model,
2455
+ messages: options.messages,
2456
+ stream: true
2457
+ };
2458
+ if (options.temperature !== void 0) body.temperature = options.temperature;
2459
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
2460
+ const asyncIterator = {
2461
+ async *[Symbol.asyncIterator]() {
2462
+ try {
2463
+ const res = await fetch(`${self.baseUrl}/v1/chat/completions`, {
2464
+ method: "POST",
2465
+ headers: self.headers,
2466
+ body: JSON.stringify(body)
2467
+ });
2468
+ if (!res.ok) throw new Error(`Stream failed: ${res.status} ${await res.text()}`);
2469
+ const reader = res.body.getReader();
2470
+ const decoder = new TextDecoder();
2471
+ let fullContent = "";
2472
+ let buffer = "";
2473
+ while (!aborted) {
2474
+ const { done, value } = await reader.read();
2475
+ if (done) break;
2476
+ buffer += decoder.decode(value, { stream: true });
2477
+ const lines = buffer.split("\n");
2478
+ buffer = lines.pop() ?? "";
2479
+ for (const line of lines) {
2480
+ if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
2481
+ try {
2482
+ const chunk = JSON.parse(line.slice(6));
2483
+ const delta = chunk.choices?.[0]?.delta?.content;
2484
+ if (delta) {
2485
+ fullContent += delta;
2486
+ yield { type: "text_delta", delta };
2487
+ }
2488
+ } catch {
2489
+ }
2490
+ }
2491
+ }
2492
+ const result = {
2493
+ content: fullContent,
2494
+ provider: self.id,
2495
+ model,
2496
+ modality: "llm",
2497
+ latencyMs: Date.now() - start,
2498
+ usage: { cost: 0, unit: "tokens" }
2499
+ };
2500
+ resolveResult?.(result);
2501
+ yield { type: "done", result };
2502
+ } catch (err) {
2503
+ const error = err instanceof Error ? err : new Error(String(err));
2504
+ rejectResult?.(error);
2505
+ yield { type: "error", error };
2506
+ }
2507
+ }
2508
+ };
2509
+ return {
2510
+ [Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
2511
+ result: () => resultPromise,
2512
+ abort: () => {
2513
+ aborted = true;
2514
+ }
2515
+ };
2516
+ }
2517
+ };
2518
+ async function detectOpenAICompatServers() {
2519
+ const providers = [];
2520
+ const results = await Promise.allSettled(
2521
+ KNOWN_LOCAL_SERVERS.map(async (server) => {
2522
+ const baseUrl = `http://localhost:${server.port}`;
2523
+ const provider = new OpenAICompatProvider({
2524
+ baseUrl,
2525
+ name: server.name,
2526
+ id: server.id
2527
+ });
2528
+ const ok = await provider.ping();
2529
+ if (ok) return provider;
2530
+ return null;
2531
+ })
2532
+ );
2533
+ for (const result of results) {
2534
+ if (result.status === "fulfilled" && result.value) {
2535
+ providers.push(result.value);
2536
+ }
2537
+ }
2538
+ return providers;
2539
+ }
2540
+
1418
2541
  // src/noosphere.ts
1419
2542
  var Noosphere = class {
1420
2543
  config;
@@ -1597,14 +2720,38 @@ var Noosphere = class {
1597
2720
  if (!this.initialized) await this.init();
1598
2721
  return this.registry.getModel(provider, modelId);
1599
2722
  }
1600
- async syncModels() {
2723
+ async syncModels(modality) {
1601
2724
  if (!this.initialized) await this.init();
1602
- return this.registry.syncAll();
2725
+ return this.registry.syncAll(modality);
1603
2726
  }
1604
2727
  // --- Tracking Methods ---
1605
2728
  getUsage(options) {
1606
2729
  return this.tracker.getSummary(options);
1607
2730
  }
2731
+ // --- Local Model Management ---
2732
+ async installModel(name) {
2733
+ if (!this.initialized) await this.init();
2734
+ const provider = this.registry.getProvider("ollama");
2735
+ if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
2736
+ return provider.pullModel(name);
2737
+ }
2738
+ async uninstallModel(name) {
2739
+ if (!this.initialized) await this.init();
2740
+ const provider = this.registry.getProvider("ollama");
2741
+ if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
2742
+ await provider.deleteModel(name);
2743
+ }
2744
+ async getHardware() {
2745
+ if (!this.initialized) await this.init();
2746
+ const provider = this.registry.getProvider("ollama");
2747
+ if (!provider) return { ollama: false, runningModels: [] };
2748
+ try {
2749
+ const runningModels = await provider.getRunningModels();
2750
+ return { ollama: true, runningModels };
2751
+ } catch {
2752
+ return { ollama: false, runningModels: [] };
2753
+ }
2754
+ }
1608
2755
  // --- Lifecycle ---
1609
2756
  async dispose() {
1610
2757
  for (const provider of this.registry.getAllProviders()) {
@@ -1655,10 +2802,21 @@ var Noosphere = class {
1655
2802
  return false;
1656
2803
  }
1657
2804
  };
2805
+ const ollamaCfg = local["ollama"];
1658
2806
  const comfyuiCfg = local["comfyui"];
1659
2807
  const piperCfg = local["piper"];
1660
2808
  const kokoroCfg = local["kokoro"];
1661
2809
  await Promise.allSettled([
2810
+ // Ollama — auto-detect even without explicit config
2811
+ (async () => {
2812
+ const host = ollamaCfg?.host ?? "http://localhost";
2813
+ const port = ollamaCfg?.port ?? 11434;
2814
+ const provider = new OllamaProvider({ host, port });
2815
+ const ok = await provider.ping();
2816
+ if (ok) {
2817
+ this.registry.addProvider(provider);
2818
+ }
2819
+ })(),
1662
2820
  // ComfyUI
1663
2821
  (async () => {
1664
2822
  if (comfyuiCfg?.enabled) {
@@ -1685,6 +2843,29 @@ var Noosphere = class {
1685
2843
  this.registry.addProvider(new LocalTTSProvider({ id: "kokoro", name: "Kokoro TTS", host: kokoroCfg.host, port: kokoroCfg.port }));
1686
2844
  }
1687
2845
  }
2846
+ })(),
2847
+ // HuggingFace local model catalog
2848
+ (async () => {
2849
+ this.registry.addProvider(new HfLocalProvider());
2850
+ })(),
2851
+ // Whisper local STT
2852
+ (async () => {
2853
+ const whisper = new WhisperLocalProvider();
2854
+ const ok = await whisper.ping();
2855
+ if (ok) this.registry.addProvider(whisper);
2856
+ })(),
2857
+ // AudioCraft local music generation
2858
+ (async () => {
2859
+ const audiocraft = new AudioCraftProvider();
2860
+ const ok = await audiocraft.ping();
2861
+ if (ok) this.registry.addProvider(audiocraft);
2862
+ })(),
2863
+ // Auto-detect OpenAI-compatible servers
2864
+ (async () => {
2865
+ const servers = await detectOpenAICompatServers();
2866
+ for (const server of servers) {
2867
+ this.registry.addProvider(server);
2868
+ }
1688
2869
  })()
1689
2870
  ]);
1690
2871
  }
@@ -1777,10 +2958,16 @@ var Noosphere = class {
1777
2958
  };
1778
2959
  // Annotate the CommonJS export names for ESM import in node:
1779
2960
  0 && (module.exports = {
2961
+ AudioCraftProvider,
2962
+ HfLocalProvider,
1780
2963
  Noosphere,
1781
2964
  NoosphereError,
2965
+ OllamaProvider,
2966
+ OpenAICompatProvider,
1782
2967
  PROVIDER_IDS,
1783
2968
  PROVIDER_LOGOS,
2969
+ WhisperLocalProvider,
2970
+ detectOpenAICompatServers,
1784
2971
  getAllProviderLogos,
1785
2972
  getProviderLogo
1786
2973
  });