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/README.md +287 -2
- package/dist/index.cjs +1201 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -3
- package/dist/index.d.ts +150 -3
- package/dist/index.js +1195 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -312,20 +312,22 @@ var Registry = class {
|
|
|
312
312
|
const cached = this.modelCache.get(provider);
|
|
313
313
|
return cached?.models.find((m) => m.id === modelId) ?? null;
|
|
314
314
|
}
|
|
315
|
-
async syncProvider(providerId) {
|
|
315
|
+
async syncProvider(providerId, modality) {
|
|
316
316
|
const provider = this.providers.get(providerId);
|
|
317
317
|
if (!provider) return 0;
|
|
318
|
-
|
|
318
|
+
if (modality && !provider.modalities.includes(modality)) return 0;
|
|
319
|
+
const models = await provider.listModels(modality);
|
|
319
320
|
this.modelCache.set(providerId, { models, syncedAt: Date.now() });
|
|
320
321
|
return models.length;
|
|
321
322
|
}
|
|
322
|
-
async syncAll() {
|
|
323
|
+
async syncAll(modality) {
|
|
323
324
|
const byProvider = {};
|
|
324
325
|
const errors = [];
|
|
325
326
|
let synced = 0;
|
|
326
327
|
for (const provider of this.providers.values()) {
|
|
328
|
+
if (modality && !provider.modalities.includes(modality)) continue;
|
|
327
329
|
try {
|
|
328
|
-
const count = await this.syncProvider(provider.id);
|
|
330
|
+
const count = await this.syncProvider(provider.id, modality);
|
|
329
331
|
byProvider[provider.id] = count;
|
|
330
332
|
synced += count;
|
|
331
333
|
} catch (err) {
|
|
@@ -394,7 +396,7 @@ var UsageTracker = class {
|
|
|
394
396
|
filtered = filtered.filter((e) => e.modality === options.modality);
|
|
395
397
|
}
|
|
396
398
|
const byProvider = {};
|
|
397
|
-
const byModality = { llm: 0, image: 0, video: 0, tts: 0 };
|
|
399
|
+
const byModality = { llm: 0, image: 0, video: 0, tts: 0, stt: 0, music: 0, embedding: 0 };
|
|
398
400
|
let totalCost = 0;
|
|
399
401
|
for (const event of filtered) {
|
|
400
402
|
totalCost += event.cost;
|
|
@@ -1019,11 +1021,55 @@ var ComfyUIProvider = class {
|
|
|
1019
1021
|
}
|
|
1020
1022
|
}
|
|
1021
1023
|
async listModels(modality) {
|
|
1024
|
+
const models = [];
|
|
1025
|
+
const logo = getProviderLogo("comfyui");
|
|
1022
1026
|
try {
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
+
const controller = new AbortController();
|
|
1028
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1029
|
+
try {
|
|
1030
|
+
const res = await fetch(`${this.baseUrl}/object_info`, { signal: controller.signal });
|
|
1031
|
+
if (res.ok) {
|
|
1032
|
+
const objectInfo = await res.json();
|
|
1033
|
+
const ckptNode = objectInfo?.["CheckpointLoaderSimple"];
|
|
1034
|
+
const ckptNames = ckptNode?.input?.required?.ckpt_name?.[0] ?? [];
|
|
1035
|
+
for (const name of ckptNames) {
|
|
1036
|
+
if (modality && modality !== "image") continue;
|
|
1037
|
+
models.push({
|
|
1038
|
+
id: `comfyui-ckpt-${name}`,
|
|
1039
|
+
provider: "comfyui",
|
|
1040
|
+
name: `ComfyUI: ${name}`,
|
|
1041
|
+
modality: "image",
|
|
1042
|
+
local: true,
|
|
1043
|
+
cost: { price: 0, unit: "free" },
|
|
1044
|
+
logo,
|
|
1045
|
+
status: "installed",
|
|
1046
|
+
localInfo: { sizeBytes: 0, runtime: "comfyui" },
|
|
1047
|
+
capabilities: { maxWidth: 2048, maxHeight: 2048, supportsNegativePrompt: true }
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
const loraNode = objectInfo?.["LoraLoader"];
|
|
1051
|
+
const loraNames = loraNode?.input?.required?.lora_name?.[0] ?? [];
|
|
1052
|
+
for (const name of loraNames) {
|
|
1053
|
+
if (modality && modality !== "image") continue;
|
|
1054
|
+
models.push({
|
|
1055
|
+
id: `comfyui-lora-${name}`,
|
|
1056
|
+
provider: "comfyui",
|
|
1057
|
+
name: `LoRA: ${name}`,
|
|
1058
|
+
modality: "image",
|
|
1059
|
+
local: true,
|
|
1060
|
+
cost: { price: 0, unit: "free" },
|
|
1061
|
+
logo,
|
|
1062
|
+
status: "installed",
|
|
1063
|
+
localInfo: { sizeBytes: 0, runtime: "comfyui" }
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
} finally {
|
|
1068
|
+
clearTimeout(timer);
|
|
1069
|
+
}
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
if (models.length === 0) {
|
|
1027
1073
|
if (!modality || modality === "image") {
|
|
1028
1074
|
models.push({
|
|
1029
1075
|
id: "comfyui-txt2img",
|
|
@@ -1048,10 +1094,44 @@ var ComfyUIProvider = class {
|
|
|
1048
1094
|
capabilities: { maxDuration: 10, supportsImageToVideo: true }
|
|
1049
1095
|
});
|
|
1050
1096
|
}
|
|
1051
|
-
return models;
|
|
1052
|
-
} catch {
|
|
1053
|
-
return [];
|
|
1054
1097
|
}
|
|
1098
|
+
if (!modality || modality === "image") {
|
|
1099
|
+
try {
|
|
1100
|
+
const controller = new AbortController();
|
|
1101
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1102
|
+
try {
|
|
1103
|
+
const res = await fetch(
|
|
1104
|
+
"https://civitai.com/api/v1/models?types=Checkpoint&sort=Highest%20Rated&limit=50&nsfw=false",
|
|
1105
|
+
{ signal: controller.signal }
|
|
1106
|
+
);
|
|
1107
|
+
if (res.ok) {
|
|
1108
|
+
const data = await res.json();
|
|
1109
|
+
for (const item of data.items ?? []) {
|
|
1110
|
+
const version = item.modelVersions?.[0];
|
|
1111
|
+
models.push({
|
|
1112
|
+
id: `civitai-${item.id}`,
|
|
1113
|
+
provider: "comfyui",
|
|
1114
|
+
name: item.name ?? `CivitAI Model ${item.id}`,
|
|
1115
|
+
description: item.description ? item.description.replace(/<[^>]+>/g, "").trim().slice(0, 300) || void 0 : void 0,
|
|
1116
|
+
modality: "image",
|
|
1117
|
+
local: true,
|
|
1118
|
+
cost: { price: 0, unit: "free" },
|
|
1119
|
+
logo,
|
|
1120
|
+
status: "available",
|
|
1121
|
+
localInfo: {
|
|
1122
|
+
sizeBytes: version?.files?.[0]?.sizeKB ? version.files[0].sizeKB * 1024 : 0,
|
|
1123
|
+
runtime: "comfyui"
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
} finally {
|
|
1129
|
+
clearTimeout(timer);
|
|
1130
|
+
}
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return models;
|
|
1055
1135
|
}
|
|
1056
1136
|
async image(options) {
|
|
1057
1137
|
const start = Date.now();
|
|
@@ -1391,6 +1471,1043 @@ var HuggingFaceProvider = class {
|
|
|
1391
1471
|
}
|
|
1392
1472
|
};
|
|
1393
1473
|
|
|
1474
|
+
// src/providers/ollama.ts
|
|
1475
|
+
var OLLAMA_FAMILY_TO_PROVIDER = {
|
|
1476
|
+
"llama": "meta",
|
|
1477
|
+
"codellama": "meta",
|
|
1478
|
+
"gemma": "google",
|
|
1479
|
+
"gemma2": "google",
|
|
1480
|
+
"gemma3": "google",
|
|
1481
|
+
"qwen": "qwen",
|
|
1482
|
+
"qwen2": "qwen",
|
|
1483
|
+
"qwen2.5": "qwen",
|
|
1484
|
+
"qwen3": "qwen",
|
|
1485
|
+
"deepseek": "deepseek",
|
|
1486
|
+
"deepcoder": "deepseek",
|
|
1487
|
+
"deepscaler": "deepseek",
|
|
1488
|
+
"qwq": "qwen",
|
|
1489
|
+
"phi": "microsoft",
|
|
1490
|
+
"phi3": "microsoft",
|
|
1491
|
+
"phi4": "microsoft",
|
|
1492
|
+
"mistral": "mistral",
|
|
1493
|
+
"mixtral": "mistral",
|
|
1494
|
+
"codestral": "mistral",
|
|
1495
|
+
"ministral": "mistral",
|
|
1496
|
+
"nemotron": "nvidia",
|
|
1497
|
+
"command": "cohere",
|
|
1498
|
+
"command-r": "cohere",
|
|
1499
|
+
"gpt-oss": "openai",
|
|
1500
|
+
"starcoder": "huggingface",
|
|
1501
|
+
"falcon": "meta",
|
|
1502
|
+
"glm": "zai",
|
|
1503
|
+
"granite": "ibm",
|
|
1504
|
+
"olmo": "meta",
|
|
1505
|
+
"yi": "zai",
|
|
1506
|
+
"minimax": "minimax",
|
|
1507
|
+
"kimi": "meta",
|
|
1508
|
+
"dolphin": "ollama",
|
|
1509
|
+
"wizard": "ollama",
|
|
1510
|
+
"nomic": "ollama",
|
|
1511
|
+
"mxbai": "ollama",
|
|
1512
|
+
"bge": "ollama",
|
|
1513
|
+
"all-minilm": "ollama",
|
|
1514
|
+
"moondream": "ollama"
|
|
1515
|
+
};
|
|
1516
|
+
var VISION_MODELS = /* @__PURE__ */ new Set([
|
|
1517
|
+
"llava",
|
|
1518
|
+
"moondream",
|
|
1519
|
+
"minicpm-v",
|
|
1520
|
+
"llama3.2-vision",
|
|
1521
|
+
"qwen2.5vl",
|
|
1522
|
+
"gemma3",
|
|
1523
|
+
"llava-llama3",
|
|
1524
|
+
"llava-phi3",
|
|
1525
|
+
"bakllava"
|
|
1526
|
+
]);
|
|
1527
|
+
function inferLogoProvider(modelName, _family) {
|
|
1528
|
+
const base = modelName.split(":")[0].toLowerCase().replace(/^[^/]+\//, "");
|
|
1529
|
+
const sortedPrefixes = Object.entries(OLLAMA_FAMILY_TO_PROVIDER).sort((a, b) => b[0].length - a[0].length);
|
|
1530
|
+
for (const [prefix, provider] of sortedPrefixes) {
|
|
1531
|
+
if (base === prefix || base.startsWith(prefix)) return provider;
|
|
1532
|
+
}
|
|
1533
|
+
return "ollama";
|
|
1534
|
+
}
|
|
1535
|
+
function supportsVision(modelName) {
|
|
1536
|
+
const base = modelName.split(":")[0].toLowerCase();
|
|
1537
|
+
for (const v of VISION_MODELS) {
|
|
1538
|
+
if (base === v || base.startsWith(v)) return true;
|
|
1539
|
+
}
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
async function fetchJson(url, options) {
|
|
1543
|
+
const timeoutMs = options?.timeoutMs ?? 5e3;
|
|
1544
|
+
const controller = new AbortController();
|
|
1545
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1546
|
+
try {
|
|
1547
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
1548
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1549
|
+
return await res.json();
|
|
1550
|
+
} finally {
|
|
1551
|
+
clearTimeout(timer);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function fetchOllamaDescriptions() {
|
|
1555
|
+
const controller = new AbortController();
|
|
1556
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1557
|
+
try {
|
|
1558
|
+
const res = await fetch("https://ollama.com/library", { signal: controller.signal });
|
|
1559
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1560
|
+
const html = await res.text();
|
|
1561
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
1562
|
+
const cardRegex = /href="\/library\/([^"]+)"[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/g;
|
|
1563
|
+
let match;
|
|
1564
|
+
while ((match = cardRegex.exec(html)) !== null) {
|
|
1565
|
+
const modelName = match[1].trim();
|
|
1566
|
+
const desc = match[2].replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
1567
|
+
if (modelName && desc) {
|
|
1568
|
+
descriptions.set(modelName, desc);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return descriptions;
|
|
1572
|
+
} finally {
|
|
1573
|
+
clearTimeout(timer);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
var OllamaProvider = class {
|
|
1577
|
+
id = "ollama";
|
|
1578
|
+
name = "Ollama (Local)";
|
|
1579
|
+
modalities = ["llm"];
|
|
1580
|
+
isLocal = true;
|
|
1581
|
+
baseUrl;
|
|
1582
|
+
constructor(config) {
|
|
1583
|
+
const host = config?.host ?? "http://localhost";
|
|
1584
|
+
const port = config?.port ?? 11434;
|
|
1585
|
+
const cleanHost = host.replace(/\/+$/, "");
|
|
1586
|
+
const hasPort = /:\d+$/.test(cleanHost);
|
|
1587
|
+
this.baseUrl = hasPort ? cleanHost : `${cleanHost}:${port}`;
|
|
1588
|
+
}
|
|
1589
|
+
async ping() {
|
|
1590
|
+
try {
|
|
1591
|
+
await fetchJson(`${this.baseUrl}/api/version`, { timeoutMs: 2e3 });
|
|
1592
|
+
return true;
|
|
1593
|
+
} catch {
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async listModels(_modality) {
|
|
1598
|
+
if (_modality && _modality !== "llm") return [];
|
|
1599
|
+
const [localData, catalogData, runningData, descriptions] = await Promise.all([
|
|
1600
|
+
fetchJson(`${this.baseUrl}/api/tags`, { timeoutMs: 5e3 }).catch(() => null),
|
|
1601
|
+
fetchJson("https://ollama.com/api/tags", { timeoutMs: 5e3 }).catch(() => null),
|
|
1602
|
+
fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 }).catch(() => null),
|
|
1603
|
+
fetchOllamaDescriptions().catch(() => /* @__PURE__ */ new Map())
|
|
1604
|
+
]);
|
|
1605
|
+
const runningNames = /* @__PURE__ */ new Set();
|
|
1606
|
+
if (runningData?.models) {
|
|
1607
|
+
for (const m of runningData.models) {
|
|
1608
|
+
runningNames.add(m.name);
|
|
1609
|
+
runningNames.add(m.model);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
const models = /* @__PURE__ */ new Map();
|
|
1613
|
+
if (localData?.models) {
|
|
1614
|
+
for (const m of localData.models) {
|
|
1615
|
+
const isRunning = runningNames.has(m.name) || runningNames.has(m.model);
|
|
1616
|
+
models.set(m.name, this.toModelInfo(m, isRunning ? "running" : "installed", true, descriptions));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (catalogData?.models) {
|
|
1620
|
+
for (const m of catalogData.models) {
|
|
1621
|
+
const name = m.name;
|
|
1622
|
+
if (!models.has(name)) {
|
|
1623
|
+
models.set(name, this.toModelInfo(m, "available", false, descriptions));
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return Array.from(models.values());
|
|
1628
|
+
}
|
|
1629
|
+
toModelInfo(m, status, isLocal, descriptions) {
|
|
1630
|
+
const name = m.name ?? m.model ?? "unknown";
|
|
1631
|
+
const family = m.details?.family;
|
|
1632
|
+
const logoProvider = inferLogoProvider(name, family);
|
|
1633
|
+
const baseName = name.split(":")[0];
|
|
1634
|
+
const description = descriptions?.get(baseName);
|
|
1635
|
+
return {
|
|
1636
|
+
id: name,
|
|
1637
|
+
provider: "ollama",
|
|
1638
|
+
name,
|
|
1639
|
+
...description ? { description } : {},
|
|
1640
|
+
modality: "llm",
|
|
1641
|
+
local: true,
|
|
1642
|
+
cost: { price: 0, unit: "free" },
|
|
1643
|
+
logo: getProviderLogo(logoProvider),
|
|
1644
|
+
status,
|
|
1645
|
+
localInfo: {
|
|
1646
|
+
sizeBytes: m.size ?? 0,
|
|
1647
|
+
family: family ?? m.details?.family,
|
|
1648
|
+
parameterSize: m.details?.parameter_size,
|
|
1649
|
+
quantization: m.details?.quantization_level,
|
|
1650
|
+
format: m.details?.format,
|
|
1651
|
+
digest: m.digest,
|
|
1652
|
+
modifiedAt: m.modified_at,
|
|
1653
|
+
running: status === "running",
|
|
1654
|
+
runtime: "ollama"
|
|
1655
|
+
},
|
|
1656
|
+
capabilities: {
|
|
1657
|
+
contextWindow: 128e3,
|
|
1658
|
+
supportsVision: supportsVision(name),
|
|
1659
|
+
supportsStreaming: true
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async chat(options) {
|
|
1664
|
+
const start = Date.now();
|
|
1665
|
+
const messages = options.messages.map((m) => ({
|
|
1666
|
+
role: m.role,
|
|
1667
|
+
content: m.content
|
|
1668
|
+
}));
|
|
1669
|
+
const body = {
|
|
1670
|
+
model: options.model ?? "llama3.2",
|
|
1671
|
+
messages,
|
|
1672
|
+
stream: false
|
|
1673
|
+
};
|
|
1674
|
+
if (options.temperature !== void 0 || options.maxTokens !== void 0) {
|
|
1675
|
+
body.options = {};
|
|
1676
|
+
if (options.temperature !== void 0) body.options.temperature = options.temperature;
|
|
1677
|
+
if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
|
|
1678
|
+
}
|
|
1679
|
+
const res = await fetch(`${this.baseUrl}/api/chat`, {
|
|
1680
|
+
method: "POST",
|
|
1681
|
+
headers: { "Content-Type": "application/json" },
|
|
1682
|
+
body: JSON.stringify(body)
|
|
1683
|
+
});
|
|
1684
|
+
if (!res.ok) {
|
|
1685
|
+
throw new Error(`Ollama chat failed: ${res.status} ${await res.text()}`);
|
|
1686
|
+
}
|
|
1687
|
+
const data = await res.json();
|
|
1688
|
+
return {
|
|
1689
|
+
content: data.message?.content ?? "",
|
|
1690
|
+
provider: "ollama",
|
|
1691
|
+
model: options.model ?? "llama3.2",
|
|
1692
|
+
modality: "llm",
|
|
1693
|
+
latencyMs: Date.now() - start,
|
|
1694
|
+
usage: {
|
|
1695
|
+
cost: 0,
|
|
1696
|
+
input: data.prompt_eval_count ?? 0,
|
|
1697
|
+
output: data.eval_count ?? 0,
|
|
1698
|
+
unit: "tokens"
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
stream(options) {
|
|
1703
|
+
const self = this;
|
|
1704
|
+
const start = Date.now();
|
|
1705
|
+
let aborted = false;
|
|
1706
|
+
let resolveResult = null;
|
|
1707
|
+
let rejectResult = null;
|
|
1708
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
1709
|
+
resolveResult = resolve;
|
|
1710
|
+
rejectResult = reject;
|
|
1711
|
+
});
|
|
1712
|
+
const messages = options.messages.map((m) => ({
|
|
1713
|
+
role: m.role,
|
|
1714
|
+
content: m.content
|
|
1715
|
+
}));
|
|
1716
|
+
const body = {
|
|
1717
|
+
model: options.model ?? "llama3.2",
|
|
1718
|
+
messages,
|
|
1719
|
+
stream: true
|
|
1720
|
+
};
|
|
1721
|
+
if (options.temperature !== void 0 || options.maxTokens !== void 0) {
|
|
1722
|
+
body.options = {};
|
|
1723
|
+
if (options.temperature !== void 0) body.options.temperature = options.temperature;
|
|
1724
|
+
if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
|
|
1725
|
+
}
|
|
1726
|
+
const asyncIterator = {
|
|
1727
|
+
async *[Symbol.asyncIterator]() {
|
|
1728
|
+
try {
|
|
1729
|
+
const res = await fetch(`${self.baseUrl}/api/chat`, {
|
|
1730
|
+
method: "POST",
|
|
1731
|
+
headers: { "Content-Type": "application/json" },
|
|
1732
|
+
body: JSON.stringify(body)
|
|
1733
|
+
});
|
|
1734
|
+
if (!res.ok) {
|
|
1735
|
+
throw new Error(`Ollama stream failed: ${res.status} ${await res.text()}`);
|
|
1736
|
+
}
|
|
1737
|
+
const reader = res.body.getReader();
|
|
1738
|
+
const decoder = new TextDecoder();
|
|
1739
|
+
let fullContent = "";
|
|
1740
|
+
let finalData = null;
|
|
1741
|
+
let buffer = "";
|
|
1742
|
+
while (!aborted) {
|
|
1743
|
+
const { done, value } = await reader.read();
|
|
1744
|
+
if (done) break;
|
|
1745
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1746
|
+
const lines = buffer.split("\n");
|
|
1747
|
+
buffer = lines.pop() ?? "";
|
|
1748
|
+
for (const line of lines) {
|
|
1749
|
+
if (!line.trim()) continue;
|
|
1750
|
+
try {
|
|
1751
|
+
const chunk = JSON.parse(line);
|
|
1752
|
+
if (chunk.message?.content) {
|
|
1753
|
+
fullContent += chunk.message.content;
|
|
1754
|
+
yield { type: "text_delta", delta: chunk.message.content };
|
|
1755
|
+
}
|
|
1756
|
+
if (chunk.done) {
|
|
1757
|
+
finalData = chunk;
|
|
1758
|
+
}
|
|
1759
|
+
} catch {
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
const result = {
|
|
1764
|
+
content: fullContent,
|
|
1765
|
+
provider: "ollama",
|
|
1766
|
+
model: options.model ?? "llama3.2",
|
|
1767
|
+
modality: "llm",
|
|
1768
|
+
latencyMs: Date.now() - start,
|
|
1769
|
+
usage: {
|
|
1770
|
+
cost: 0,
|
|
1771
|
+
input: finalData?.prompt_eval_count ?? 0,
|
|
1772
|
+
output: finalData?.eval_count ?? 0,
|
|
1773
|
+
unit: "tokens"
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
resolveResult?.(result);
|
|
1777
|
+
yield { type: "done", result };
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1780
|
+
rejectResult?.(error);
|
|
1781
|
+
yield { type: "error", error };
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
return {
|
|
1786
|
+
[Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
|
|
1787
|
+
result: () => resultPromise,
|
|
1788
|
+
abort: () => {
|
|
1789
|
+
aborted = true;
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
// --- Extra model management methods ---
|
|
1794
|
+
async *pullModel(name) {
|
|
1795
|
+
const res = await fetch(`${this.baseUrl}/api/pull`, {
|
|
1796
|
+
method: "POST",
|
|
1797
|
+
headers: { "Content-Type": "application/json" },
|
|
1798
|
+
body: JSON.stringify({ name, stream: true })
|
|
1799
|
+
});
|
|
1800
|
+
if (!res.ok) {
|
|
1801
|
+
throw new Error(`Ollama pull failed: ${res.status} ${await res.text()}`);
|
|
1802
|
+
}
|
|
1803
|
+
const reader = res.body.getReader();
|
|
1804
|
+
const decoder = new TextDecoder();
|
|
1805
|
+
let buffer = "";
|
|
1806
|
+
while (true) {
|
|
1807
|
+
const { done, value } = await reader.read();
|
|
1808
|
+
if (done) break;
|
|
1809
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1810
|
+
const lines = buffer.split("\n");
|
|
1811
|
+
buffer = lines.pop() ?? "";
|
|
1812
|
+
for (const line of lines) {
|
|
1813
|
+
if (!line.trim()) continue;
|
|
1814
|
+
try {
|
|
1815
|
+
yield JSON.parse(line);
|
|
1816
|
+
} catch {
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
async deleteModel(name) {
|
|
1822
|
+
const res = await fetch(`${this.baseUrl}/api/delete`, {
|
|
1823
|
+
method: "DELETE",
|
|
1824
|
+
headers: { "Content-Type": "application/json" },
|
|
1825
|
+
body: JSON.stringify({ name })
|
|
1826
|
+
});
|
|
1827
|
+
if (!res.ok) {
|
|
1828
|
+
throw new Error(`Ollama delete failed: ${res.status} ${await res.text()}`);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
async showModel(name) {
|
|
1832
|
+
const res = await fetch(`${this.baseUrl}/api/show`, {
|
|
1833
|
+
method: "POST",
|
|
1834
|
+
headers: { "Content-Type": "application/json" },
|
|
1835
|
+
body: JSON.stringify({ name })
|
|
1836
|
+
});
|
|
1837
|
+
if (!res.ok) {
|
|
1838
|
+
throw new Error(`Ollama show failed: ${res.status} ${await res.text()}`);
|
|
1839
|
+
}
|
|
1840
|
+
return await res.json();
|
|
1841
|
+
}
|
|
1842
|
+
async getRunningModels() {
|
|
1843
|
+
const data = await fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 });
|
|
1844
|
+
return data?.models ?? [];
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
// src/utils/parse-readme.ts
|
|
1849
|
+
async function fetchReadmeDescription(modelId, timeoutMs = 5e3) {
|
|
1850
|
+
try {
|
|
1851
|
+
const controller = new AbortController();
|
|
1852
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1853
|
+
try {
|
|
1854
|
+
const res = await fetch(
|
|
1855
|
+
`https://huggingface.co/${modelId}/raw/main/README.md`,
|
|
1856
|
+
{ signal: controller.signal }
|
|
1857
|
+
);
|
|
1858
|
+
if (!res.ok) return void 0;
|
|
1859
|
+
const text = await res.text();
|
|
1860
|
+
return parseReadmeDescription(text);
|
|
1861
|
+
} finally {
|
|
1862
|
+
clearTimeout(timer);
|
|
1863
|
+
}
|
|
1864
|
+
} catch {
|
|
1865
|
+
return void 0;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
function parseReadmeDescription(readme) {
|
|
1869
|
+
const withoutFrontmatter = readme.replace(/^---[\s\S]*?---\s*/, "");
|
|
1870
|
+
const lines = withoutFrontmatter.split("\n");
|
|
1871
|
+
let paragraph = "";
|
|
1872
|
+
for (const line of lines) {
|
|
1873
|
+
const trimmed = line.trim();
|
|
1874
|
+
if (!trimmed) {
|
|
1875
|
+
if (paragraph) break;
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (trimmed.startsWith("#")) {
|
|
1879
|
+
if (paragraph) break;
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
if (/^\[?!\[/.test(trimmed) || /^</.test(trimmed)) continue;
|
|
1883
|
+
if (/^\[.*\]\(.*\)$/.test(trimmed)) continue;
|
|
1884
|
+
paragraph += (paragraph ? " " : "") + trimmed;
|
|
1885
|
+
}
|
|
1886
|
+
if (!paragraph) return void 0;
|
|
1887
|
+
paragraph = paragraph.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/<[^>]+>/g, "").trim();
|
|
1888
|
+
if (!paragraph) return void 0;
|
|
1889
|
+
if (paragraph.length > 300) paragraph = paragraph.slice(0, 297) + "...";
|
|
1890
|
+
return paragraph;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/providers/hf-local.ts
|
|
1894
|
+
import { readdir, readFile } from "fs/promises";
|
|
1895
|
+
import { join } from "path";
|
|
1896
|
+
import { homedir } from "os";
|
|
1897
|
+
var FETCH_TIMEOUT_MS3 = 5e3;
|
|
1898
|
+
var HF_HUB_API2 = "https://huggingface.co/api/models";
|
|
1899
|
+
var HF_ORG_TO_LOGO_PROVIDER = {
|
|
1900
|
+
"meta-llama": "meta",
|
|
1901
|
+
"facebook": "meta",
|
|
1902
|
+
"google": "google",
|
|
1903
|
+
"microsoft": "microsoft",
|
|
1904
|
+
"nvidia": "nvidia",
|
|
1905
|
+
"mistralai": "mistral",
|
|
1906
|
+
"Qwen": "qwen",
|
|
1907
|
+
"deepseek-ai": "deepseek",
|
|
1908
|
+
"openai": "openai",
|
|
1909
|
+
"CohereForAI": "cohere",
|
|
1910
|
+
"rhasspy": "piper",
|
|
1911
|
+
"stabilityai": "huggingface",
|
|
1912
|
+
"black-forest-labs": "huggingface",
|
|
1913
|
+
"tiiuae": "huggingface",
|
|
1914
|
+
"allenai": "huggingface",
|
|
1915
|
+
"Salesforce": "huggingface"
|
|
1916
|
+
};
|
|
1917
|
+
var PIPELINE_TAG_TO_MODALITY = {
|
|
1918
|
+
"text-to-image": "image",
|
|
1919
|
+
"text-to-video": "video",
|
|
1920
|
+
"text-to-audio": "music",
|
|
1921
|
+
"text-to-speech": "tts",
|
|
1922
|
+
"automatic-speech-recognition": "stt"
|
|
1923
|
+
};
|
|
1924
|
+
var CATALOG_QUERIES = [
|
|
1925
|
+
{ pipeline_tag: "text-to-image", limit: 50 },
|
|
1926
|
+
{ pipeline_tag: "text-to-video", limit: 30 },
|
|
1927
|
+
{ pipeline_tag: "text-to-audio", limit: 30 },
|
|
1928
|
+
{ pipeline_tag: "text-to-speech", limit: 30 },
|
|
1929
|
+
{ pipeline_tag: "automatic-speech-recognition", limit: 30 },
|
|
1930
|
+
{ pipeline_tag: "text-to-image", limit: 100, library: "diffusers" }
|
|
1931
|
+
];
|
|
1932
|
+
async function fetchJsonTimeout(url, timeoutMs = FETCH_TIMEOUT_MS3) {
|
|
1933
|
+
const controller = new AbortController();
|
|
1934
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1935
|
+
try {
|
|
1936
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
1937
|
+
if (!res.ok) return null;
|
|
1938
|
+
return await res.json();
|
|
1939
|
+
} catch {
|
|
1940
|
+
return null;
|
|
1941
|
+
} finally {
|
|
1942
|
+
clearTimeout(timer);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
var HfLocalProvider = class {
|
|
1946
|
+
id = "hf-local";
|
|
1947
|
+
name = "HuggingFace Local Models";
|
|
1948
|
+
modalities = ["image", "video", "tts", "stt", "music"];
|
|
1949
|
+
isLocal = true;
|
|
1950
|
+
cachedModels = null;
|
|
1951
|
+
async ping() {
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
async listModels(modality) {
|
|
1955
|
+
if (!this.cachedModels) {
|
|
1956
|
+
const [catalog, installed] = await Promise.all([
|
|
1957
|
+
this.fetchCatalog(),
|
|
1958
|
+
this.scanLocalCache()
|
|
1959
|
+
]);
|
|
1960
|
+
const modelMap = /* @__PURE__ */ new Map();
|
|
1961
|
+
for (const m of catalog) modelMap.set(m.id, m);
|
|
1962
|
+
for (const m of installed) modelMap.set(m.id, m);
|
|
1963
|
+
this.cachedModels = Array.from(modelMap.values());
|
|
1964
|
+
}
|
|
1965
|
+
if (modality) return this.cachedModels.filter((m) => m.modality === modality);
|
|
1966
|
+
return this.cachedModels;
|
|
1967
|
+
}
|
|
1968
|
+
async fetchCatalog() {
|
|
1969
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1970
|
+
const entries = [];
|
|
1971
|
+
const results = await Promise.allSettled(
|
|
1972
|
+
CATALOG_QUERIES.map(async (q) => {
|
|
1973
|
+
const params = new URLSearchParams({
|
|
1974
|
+
pipeline_tag: q.pipeline_tag,
|
|
1975
|
+
sort: "downloads",
|
|
1976
|
+
limit: String(q.limit)
|
|
1977
|
+
});
|
|
1978
|
+
if (q.library) params.set("library", q.library);
|
|
1979
|
+
return fetchJsonTimeout(`${HF_HUB_API2}?${params}`);
|
|
1980
|
+
})
|
|
1981
|
+
);
|
|
1982
|
+
for (const result of results) {
|
|
1983
|
+
if (result.status !== "fulfilled" || !Array.isArray(result.value)) continue;
|
|
1984
|
+
for (const entry of result.value) {
|
|
1985
|
+
const id = entry.id ?? entry.modelId;
|
|
1986
|
+
if (!id || seen.has(id)) continue;
|
|
1987
|
+
seen.add(id);
|
|
1988
|
+
entries.push({
|
|
1989
|
+
id,
|
|
1990
|
+
pipelineTag: entry.pipeline_tag ?? "",
|
|
1991
|
+
libraryName: entry.library_name
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
const descriptionMap = /* @__PURE__ */ new Map();
|
|
1996
|
+
for (let i = 0; i < entries.length; i += 10) {
|
|
1997
|
+
const batch = entries.slice(i, i + 10);
|
|
1998
|
+
const descs = await Promise.allSettled(
|
|
1999
|
+
batch.map(async (e) => {
|
|
2000
|
+
const desc = await fetchReadmeDescription(e.id);
|
|
2001
|
+
return { id: e.id, desc };
|
|
2002
|
+
})
|
|
2003
|
+
);
|
|
2004
|
+
for (const d of descs) {
|
|
2005
|
+
if (d.status === "fulfilled" && d.value.desc) {
|
|
2006
|
+
descriptionMap.set(d.value.id, d.value.desc);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const models = [];
|
|
2011
|
+
for (const e of entries) {
|
|
2012
|
+
const modality = PIPELINE_TAG_TO_MODALITY[e.pipelineTag] ?? "image";
|
|
2013
|
+
const org = e.id.includes("/") ? e.id.split("/")[0] : void 0;
|
|
2014
|
+
const logoProvider = org ? HF_ORG_TO_LOGO_PROVIDER[org] ?? "huggingface" : "huggingface";
|
|
2015
|
+
models.push({
|
|
2016
|
+
id: e.id,
|
|
2017
|
+
provider: "hf-local",
|
|
2018
|
+
name: e.id.split("/").pop() ?? e.id,
|
|
2019
|
+
modality,
|
|
2020
|
+
local: true,
|
|
2021
|
+
cost: { price: 0, unit: "free" },
|
|
2022
|
+
logo: getProviderLogo(logoProvider),
|
|
2023
|
+
description: descriptionMap.get(e.id),
|
|
2024
|
+
status: "available",
|
|
2025
|
+
localInfo: {
|
|
2026
|
+
sizeBytes: 0,
|
|
2027
|
+
runtime: "huggingface",
|
|
2028
|
+
family: e.libraryName
|
|
2029
|
+
},
|
|
2030
|
+
capabilities: {}
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
return models;
|
|
2034
|
+
}
|
|
2035
|
+
async scanLocalCache() {
|
|
2036
|
+
const models = [];
|
|
2037
|
+
const cacheDir = join(homedir(), ".cache", "huggingface", "hub");
|
|
2038
|
+
try {
|
|
2039
|
+
const entries = await readdir(cacheDir, { withFileTypes: true });
|
|
2040
|
+
for (const entry of entries) {
|
|
2041
|
+
if (!entry.isDirectory() || !entry.name.startsWith("models--")) continue;
|
|
2042
|
+
const parts = entry.name.replace("models--", "").split("--");
|
|
2043
|
+
const modelId = parts.join("/");
|
|
2044
|
+
const modelDir = join(cacheDir, entry.name);
|
|
2045
|
+
let snapshotHash;
|
|
2046
|
+
try {
|
|
2047
|
+
snapshotHash = (await readFile(join(modelDir, "refs", "main"), "utf-8")).trim();
|
|
2048
|
+
} catch {
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
let pipelineTag = "";
|
|
2052
|
+
const snapshotDir = join(modelDir, "snapshots", snapshotHash);
|
|
2053
|
+
try {
|
|
2054
|
+
const modelIndex = JSON.parse(await readFile(join(snapshotDir, "model_index.json"), "utf-8"));
|
|
2055
|
+
if (modelIndex._class_name?.includes("Stable") || modelIndex._class_name?.includes("Flux")) {
|
|
2056
|
+
pipelineTag = "text-to-image";
|
|
2057
|
+
} else if (modelIndex._class_name?.includes("Video") || modelIndex._class_name?.includes("Animate")) {
|
|
2058
|
+
pipelineTag = "text-to-video";
|
|
2059
|
+
}
|
|
2060
|
+
} catch {
|
|
2061
|
+
try {
|
|
2062
|
+
const config = JSON.parse(await readFile(join(snapshotDir, "config.json"), "utf-8"));
|
|
2063
|
+
if (config.task_specific_params?.["text-to-image"]) pipelineTag = "text-to-image";
|
|
2064
|
+
else if (config.model_type?.includes("whisper")) pipelineTag = "automatic-speech-recognition";
|
|
2065
|
+
} catch {
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
|
|
2069
|
+
const org = modelId.includes("/") ? modelId.split("/")[0] : void 0;
|
|
2070
|
+
const logoProvider = org ? HF_ORG_TO_LOGO_PROVIDER[org] ?? "huggingface" : "huggingface";
|
|
2071
|
+
models.push({
|
|
2072
|
+
id: modelId,
|
|
2073
|
+
provider: "hf-local",
|
|
2074
|
+
name: modelId.split("/").pop() ?? modelId,
|
|
2075
|
+
modality,
|
|
2076
|
+
local: true,
|
|
2077
|
+
cost: { price: 0, unit: "free" },
|
|
2078
|
+
logo: getProviderLogo(logoProvider),
|
|
2079
|
+
status: "installed",
|
|
2080
|
+
localInfo: {
|
|
2081
|
+
sizeBytes: 0,
|
|
2082
|
+
runtime: "huggingface",
|
|
2083
|
+
diskPath: snapshotDir
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
return models;
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
// src/providers/whisper-local.ts
|
|
2094
|
+
import { execFile } from "child_process";
|
|
2095
|
+
import { access } from "fs/promises";
|
|
2096
|
+
import { join as join2 } from "path";
|
|
2097
|
+
import { homedir as homedir2 } from "os";
|
|
2098
|
+
var WHISPER_MODELS = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "turbo"];
|
|
2099
|
+
var WHISPER_HF_REPOS = {
|
|
2100
|
+
"tiny": "openai/whisper-tiny",
|
|
2101
|
+
"base": "openai/whisper-base",
|
|
2102
|
+
"small": "openai/whisper-small",
|
|
2103
|
+
"medium": "openai/whisper-medium",
|
|
2104
|
+
"large": "openai/whisper-large",
|
|
2105
|
+
"large-v2": "openai/whisper-large-v2",
|
|
2106
|
+
"large-v3": "openai/whisper-large-v3",
|
|
2107
|
+
"turbo": "openai/whisper-large-v3-turbo"
|
|
2108
|
+
};
|
|
2109
|
+
async function fetchWhisperDescriptions() {
|
|
2110
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
2111
|
+
const fetches = Object.entries(WHISPER_HF_REPOS).map(async ([size, repo]) => {
|
|
2112
|
+
const desc = await fetchReadmeDescription(repo, 8e3);
|
|
2113
|
+
if (desc) descriptions.set(size, desc);
|
|
2114
|
+
});
|
|
2115
|
+
await Promise.allSettled(fetches);
|
|
2116
|
+
return descriptions;
|
|
2117
|
+
}
|
|
2118
|
+
function runPython(code, timeoutMs = 5e3) {
|
|
2119
|
+
return new Promise((resolve, reject) => {
|
|
2120
|
+
const proc = execFile("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
|
|
2121
|
+
if (err) reject(err);
|
|
2122
|
+
else resolve(stdout.trim());
|
|
2123
|
+
});
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
async function fileExists(path) {
|
|
2127
|
+
try {
|
|
2128
|
+
await access(path);
|
|
2129
|
+
return true;
|
|
2130
|
+
} catch {
|
|
2131
|
+
return false;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
var WhisperLocalProvider = class {
|
|
2135
|
+
id = "whisper-local";
|
|
2136
|
+
name = "Whisper (Local)";
|
|
2137
|
+
modalities = ["stt"];
|
|
2138
|
+
isLocal = true;
|
|
2139
|
+
runtime = null;
|
|
2140
|
+
async ping() {
|
|
2141
|
+
return await this.detectRuntime() !== null;
|
|
2142
|
+
}
|
|
2143
|
+
async detectRuntime() {
|
|
2144
|
+
if (this.runtime) return this.runtime;
|
|
2145
|
+
try {
|
|
2146
|
+
await runPython('import faster_whisper; print("ok")');
|
|
2147
|
+
this.runtime = "faster-whisper";
|
|
2148
|
+
return this.runtime;
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
try {
|
|
2152
|
+
await runPython("import whisper; print(whisper.__version__)");
|
|
2153
|
+
this.runtime = "whisper";
|
|
2154
|
+
return this.runtime;
|
|
2155
|
+
} catch {
|
|
2156
|
+
}
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
async listModels(_modality) {
|
|
2160
|
+
if (_modality && _modality !== "stt") return [];
|
|
2161
|
+
const runtime = await this.detectRuntime();
|
|
2162
|
+
if (!runtime) return [];
|
|
2163
|
+
const descMap = await fetchWhisperDescriptions().catch(() => /* @__PURE__ */ new Map());
|
|
2164
|
+
const logo = getProviderLogo("openai");
|
|
2165
|
+
const models = [];
|
|
2166
|
+
for (const name of WHISPER_MODELS) {
|
|
2167
|
+
const installed = await this.isModelCached(name, runtime);
|
|
2168
|
+
models.push({
|
|
2169
|
+
id: `whisper-${name}`,
|
|
2170
|
+
provider: "whisper-local",
|
|
2171
|
+
name: `Whisper ${name}`,
|
|
2172
|
+
description: descMap.get(name),
|
|
2173
|
+
modality: "stt",
|
|
2174
|
+
local: true,
|
|
2175
|
+
cost: { price: 0, unit: "free" },
|
|
2176
|
+
logo,
|
|
2177
|
+
status: installed ? "installed" : "available",
|
|
2178
|
+
localInfo: {
|
|
2179
|
+
sizeBytes: 0,
|
|
2180
|
+
runtime
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
return models;
|
|
2185
|
+
}
|
|
2186
|
+
async isModelCached(name, runtime) {
|
|
2187
|
+
if (runtime === "whisper") {
|
|
2188
|
+
return fileExists(join2(homedir2(), ".cache", "whisper", `${name}.pt`));
|
|
2189
|
+
}
|
|
2190
|
+
const hfDir = join2(homedir2(), ".cache", "huggingface", "hub", `models--Systran--faster-whisper-${name}`);
|
|
2191
|
+
return fileExists(hfDir);
|
|
2192
|
+
}
|
|
2193
|
+
async transcribe(options) {
|
|
2194
|
+
const runtime = await this.detectRuntime();
|
|
2195
|
+
if (!runtime) throw new Error("Whisper is not installed");
|
|
2196
|
+
const model = options.model?.replace("whisper-", "") ?? "base";
|
|
2197
|
+
const lang = options.language ? `--language ${options.language}` : "";
|
|
2198
|
+
const task = options.task ?? "transcribe";
|
|
2199
|
+
if (runtime === "faster-whisper") {
|
|
2200
|
+
const code = `
|
|
2201
|
+
import json, sys
|
|
2202
|
+
from faster_whisper import WhisperModel
|
|
2203
|
+
model = WhisperModel("${model}")
|
|
2204
|
+
segments, info = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
|
|
2205
|
+
segs = [{"start": s.start, "end": s.end, "text": s.text} for s in segments]
|
|
2206
|
+
print(json.dumps({"text": " ".join(s["text"] for s in segs), "language": info.language, "duration": info.duration, "segments": segs}))
|
|
2207
|
+
`;
|
|
2208
|
+
const output = await runPython(code, 12e4);
|
|
2209
|
+
return JSON.parse(output);
|
|
2210
|
+
} else {
|
|
2211
|
+
const code = `
|
|
2212
|
+
import json, whisper
|
|
2213
|
+
model = whisper.load_model("${model}")
|
|
2214
|
+
result = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
|
|
2215
|
+
segs = [{"start": s["start"], "end": s["end"], "text": s["text"]} for s in result.get("segments", [])]
|
|
2216
|
+
print(json.dumps({"text": result["text"], "language": result.get("language", ""), "duration": 0, "segments": segs}))
|
|
2217
|
+
`;
|
|
2218
|
+
const output = await runPython(code, 12e4);
|
|
2219
|
+
return JSON.parse(output);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
|
|
2224
|
+
// src/providers/audiocraft.ts
|
|
2225
|
+
import { execFile as execFile2 } from "child_process";
|
|
2226
|
+
import { access as access2 } from "fs/promises";
|
|
2227
|
+
import { join as join3 } from "path";
|
|
2228
|
+
import { homedir as homedir3 } from "os";
|
|
2229
|
+
var AUDIOCRAFT_MODELS = [
|
|
2230
|
+
{ id: "musicgen-small", name: "MusicGen Small" },
|
|
2231
|
+
{ id: "musicgen-medium", name: "MusicGen Medium" },
|
|
2232
|
+
{ id: "musicgen-large", name: "MusicGen Large" },
|
|
2233
|
+
{ id: "musicgen-melody", name: "MusicGen Melody" },
|
|
2234
|
+
{ id: "audiogen-medium", name: "AudioGen Medium" }
|
|
2235
|
+
];
|
|
2236
|
+
var AUDIOCRAFT_HF_REPOS = {
|
|
2237
|
+
"musicgen-small": "facebook/musicgen-small",
|
|
2238
|
+
"musicgen-medium": "facebook/musicgen-medium",
|
|
2239
|
+
"musicgen-large": "facebook/musicgen-large",
|
|
2240
|
+
"musicgen-melody": "facebook/musicgen-melody",
|
|
2241
|
+
"audiogen-medium": "facebook/audiogen-medium"
|
|
2242
|
+
};
|
|
2243
|
+
async function fetchAudioCraftDescriptions() {
|
|
2244
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
2245
|
+
const fetches = Object.entries(AUDIOCRAFT_HF_REPOS).map(async ([id, repo]) => {
|
|
2246
|
+
const desc = await fetchReadmeDescription(repo, 8e3);
|
|
2247
|
+
if (desc) descriptions.set(id, desc);
|
|
2248
|
+
});
|
|
2249
|
+
await Promise.allSettled(fetches);
|
|
2250
|
+
return descriptions;
|
|
2251
|
+
}
|
|
2252
|
+
function runPython2(code, timeoutMs = 5e3) {
|
|
2253
|
+
return new Promise((resolve, reject) => {
|
|
2254
|
+
execFile2("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
|
|
2255
|
+
if (err) reject(err);
|
|
2256
|
+
else resolve(stdout.trim());
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
async function fileExists2(path) {
|
|
2261
|
+
try {
|
|
2262
|
+
await access2(path);
|
|
2263
|
+
return true;
|
|
2264
|
+
} catch {
|
|
2265
|
+
return false;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
var AudioCraftProvider = class {
|
|
2269
|
+
id = "audiocraft";
|
|
2270
|
+
name = "AudioCraft (Local)";
|
|
2271
|
+
modalities = ["music"];
|
|
2272
|
+
isLocal = true;
|
|
2273
|
+
detected = null;
|
|
2274
|
+
async ping() {
|
|
2275
|
+
if (this.detected !== null) return this.detected;
|
|
2276
|
+
try {
|
|
2277
|
+
await runPython2('import audiocraft; print("ok")');
|
|
2278
|
+
this.detected = true;
|
|
2279
|
+
} catch {
|
|
2280
|
+
this.detected = false;
|
|
2281
|
+
}
|
|
2282
|
+
return this.detected;
|
|
2283
|
+
}
|
|
2284
|
+
async listModels(_modality) {
|
|
2285
|
+
if (_modality && _modality !== "music") return [];
|
|
2286
|
+
if (!await this.ping()) return [];
|
|
2287
|
+
const descMap = await fetchAudioCraftDescriptions().catch(() => /* @__PURE__ */ new Map());
|
|
2288
|
+
const logo = getProviderLogo("meta");
|
|
2289
|
+
const models = [];
|
|
2290
|
+
for (const m of AUDIOCRAFT_MODELS) {
|
|
2291
|
+
const hfDir = join3(homedir3(), ".cache", "huggingface", "hub", `models--facebook--${m.id}`);
|
|
2292
|
+
const installed = await fileExists2(hfDir);
|
|
2293
|
+
models.push({
|
|
2294
|
+
id: m.id,
|
|
2295
|
+
provider: "audiocraft",
|
|
2296
|
+
name: m.name,
|
|
2297
|
+
description: descMap.get(m.id),
|
|
2298
|
+
modality: "music",
|
|
2299
|
+
local: true,
|
|
2300
|
+
cost: { price: 0, unit: "free" },
|
|
2301
|
+
logo,
|
|
2302
|
+
status: installed ? "installed" : "available",
|
|
2303
|
+
localInfo: {
|
|
2304
|
+
sizeBytes: 0,
|
|
2305
|
+
runtime: "audiocraft"
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
return models;
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
|
|
2313
|
+
// src/providers/openai-compat.ts
|
|
2314
|
+
var FETCH_TIMEOUT_MS4 = 5e3;
|
|
2315
|
+
var KNOWN_LOCAL_SERVERS = [
|
|
2316
|
+
{ port: 8080, name: "llama.cpp / LocalAI", id: "llamacpp" },
|
|
2317
|
+
{ port: 1234, name: "LM Studio", id: "lmstudio" },
|
|
2318
|
+
{ port: 8e3, name: "vLLM", id: "vllm" },
|
|
2319
|
+
{ port: 5e3, name: "TabbyAPI", id: "tabbyapi" },
|
|
2320
|
+
{ port: 5001, name: "KoboldCpp", id: "koboldcpp" },
|
|
2321
|
+
{ port: 1337, name: "Jan", id: "jan" }
|
|
2322
|
+
];
|
|
2323
|
+
async function fetchJsonTimeout2(url, headers, timeoutMs = FETCH_TIMEOUT_MS4) {
|
|
2324
|
+
const controller = new AbortController();
|
|
2325
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2326
|
+
try {
|
|
2327
|
+
const res = await fetch(url, { signal: controller.signal, headers });
|
|
2328
|
+
if (!res.ok) return null;
|
|
2329
|
+
return await res.json();
|
|
2330
|
+
} catch {
|
|
2331
|
+
return null;
|
|
2332
|
+
} finally {
|
|
2333
|
+
clearTimeout(timer);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
var OpenAICompatProvider = class {
|
|
2337
|
+
id;
|
|
2338
|
+
name;
|
|
2339
|
+
modalities = ["llm"];
|
|
2340
|
+
isLocal = true;
|
|
2341
|
+
baseUrl;
|
|
2342
|
+
headers;
|
|
2343
|
+
constructor(config) {
|
|
2344
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2345
|
+
this.id = config.id ?? `openai-compat-${new URL(config.baseUrl).port}`;
|
|
2346
|
+
this.name = config.name ?? `OpenAI-Compatible (${this.baseUrl})`;
|
|
2347
|
+
this.headers = { "Content-Type": "application/json" };
|
|
2348
|
+
if (config.apiKey) {
|
|
2349
|
+
this.headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
async ping() {
|
|
2353
|
+
const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers, 2e3);
|
|
2354
|
+
return data !== null;
|
|
2355
|
+
}
|
|
2356
|
+
async listModels(_modality) {
|
|
2357
|
+
if (_modality && _modality !== "llm") return [];
|
|
2358
|
+
const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers);
|
|
2359
|
+
if (!data?.data || !Array.isArray(data.data)) return [];
|
|
2360
|
+
const logo = getProviderLogo("openai");
|
|
2361
|
+
return data.data.map((m) => ({
|
|
2362
|
+
id: m.id,
|
|
2363
|
+
provider: this.id,
|
|
2364
|
+
name: m.id,
|
|
2365
|
+
modality: "llm",
|
|
2366
|
+
local: true,
|
|
2367
|
+
cost: { price: 0, unit: "free" },
|
|
2368
|
+
logo,
|
|
2369
|
+
status: "running",
|
|
2370
|
+
localInfo: {
|
|
2371
|
+
sizeBytes: 0,
|
|
2372
|
+
runtime: this.id
|
|
2373
|
+
},
|
|
2374
|
+
capabilities: {
|
|
2375
|
+
supportsStreaming: true
|
|
2376
|
+
}
|
|
2377
|
+
}));
|
|
2378
|
+
}
|
|
2379
|
+
async chat(options) {
|
|
2380
|
+
const start = Date.now();
|
|
2381
|
+
const model = options.model ?? "default";
|
|
2382
|
+
const body = {
|
|
2383
|
+
model,
|
|
2384
|
+
messages: options.messages,
|
|
2385
|
+
stream: false
|
|
2386
|
+
};
|
|
2387
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
2388
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
2389
|
+
if (options.jsonMode) body.response_format = { type: "json_object" };
|
|
2390
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
2391
|
+
method: "POST",
|
|
2392
|
+
headers: this.headers,
|
|
2393
|
+
body: JSON.stringify(body)
|
|
2394
|
+
});
|
|
2395
|
+
if (!res.ok) throw new Error(`OpenAI-compat chat failed: ${res.status} ${await res.text()}`);
|
|
2396
|
+
const data = await res.json();
|
|
2397
|
+
const choice = data.choices?.[0];
|
|
2398
|
+
return {
|
|
2399
|
+
content: choice?.message?.content ?? "",
|
|
2400
|
+
provider: this.id,
|
|
2401
|
+
model,
|
|
2402
|
+
modality: "llm",
|
|
2403
|
+
latencyMs: Date.now() - start,
|
|
2404
|
+
usage: {
|
|
2405
|
+
cost: 0,
|
|
2406
|
+
input: data.usage?.prompt_tokens ?? 0,
|
|
2407
|
+
output: data.usage?.completion_tokens ?? 0,
|
|
2408
|
+
unit: "tokens"
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
stream(options) {
|
|
2413
|
+
const self = this;
|
|
2414
|
+
const start = Date.now();
|
|
2415
|
+
let aborted = false;
|
|
2416
|
+
let resolveResult = null;
|
|
2417
|
+
let rejectResult = null;
|
|
2418
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
2419
|
+
resolveResult = resolve;
|
|
2420
|
+
rejectResult = reject;
|
|
2421
|
+
});
|
|
2422
|
+
const model = options.model ?? "default";
|
|
2423
|
+
const body = {
|
|
2424
|
+
model,
|
|
2425
|
+
messages: options.messages,
|
|
2426
|
+
stream: true
|
|
2427
|
+
};
|
|
2428
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
2429
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
2430
|
+
const asyncIterator = {
|
|
2431
|
+
async *[Symbol.asyncIterator]() {
|
|
2432
|
+
try {
|
|
2433
|
+
const res = await fetch(`${self.baseUrl}/v1/chat/completions`, {
|
|
2434
|
+
method: "POST",
|
|
2435
|
+
headers: self.headers,
|
|
2436
|
+
body: JSON.stringify(body)
|
|
2437
|
+
});
|
|
2438
|
+
if (!res.ok) throw new Error(`Stream failed: ${res.status} ${await res.text()}`);
|
|
2439
|
+
const reader = res.body.getReader();
|
|
2440
|
+
const decoder = new TextDecoder();
|
|
2441
|
+
let fullContent = "";
|
|
2442
|
+
let buffer = "";
|
|
2443
|
+
while (!aborted) {
|
|
2444
|
+
const { done, value } = await reader.read();
|
|
2445
|
+
if (done) break;
|
|
2446
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2447
|
+
const lines = buffer.split("\n");
|
|
2448
|
+
buffer = lines.pop() ?? "";
|
|
2449
|
+
for (const line of lines) {
|
|
2450
|
+
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
|
|
2451
|
+
try {
|
|
2452
|
+
const chunk = JSON.parse(line.slice(6));
|
|
2453
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
2454
|
+
if (delta) {
|
|
2455
|
+
fullContent += delta;
|
|
2456
|
+
yield { type: "text_delta", delta };
|
|
2457
|
+
}
|
|
2458
|
+
} catch {
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
const result = {
|
|
2463
|
+
content: fullContent,
|
|
2464
|
+
provider: self.id,
|
|
2465
|
+
model,
|
|
2466
|
+
modality: "llm",
|
|
2467
|
+
latencyMs: Date.now() - start,
|
|
2468
|
+
usage: { cost: 0, unit: "tokens" }
|
|
2469
|
+
};
|
|
2470
|
+
resolveResult?.(result);
|
|
2471
|
+
yield { type: "done", result };
|
|
2472
|
+
} catch (err) {
|
|
2473
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2474
|
+
rejectResult?.(error);
|
|
2475
|
+
yield { type: "error", error };
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
return {
|
|
2480
|
+
[Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
|
|
2481
|
+
result: () => resultPromise,
|
|
2482
|
+
abort: () => {
|
|
2483
|
+
aborted = true;
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
async function detectOpenAICompatServers() {
|
|
2489
|
+
const providers = [];
|
|
2490
|
+
const results = await Promise.allSettled(
|
|
2491
|
+
KNOWN_LOCAL_SERVERS.map(async (server) => {
|
|
2492
|
+
const baseUrl = `http://localhost:${server.port}`;
|
|
2493
|
+
const provider = new OpenAICompatProvider({
|
|
2494
|
+
baseUrl,
|
|
2495
|
+
name: server.name,
|
|
2496
|
+
id: server.id
|
|
2497
|
+
});
|
|
2498
|
+
const ok = await provider.ping();
|
|
2499
|
+
if (ok) return provider;
|
|
2500
|
+
return null;
|
|
2501
|
+
})
|
|
2502
|
+
);
|
|
2503
|
+
for (const result of results) {
|
|
2504
|
+
if (result.status === "fulfilled" && result.value) {
|
|
2505
|
+
providers.push(result.value);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
return providers;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
1394
2511
|
// src/noosphere.ts
|
|
1395
2512
|
var Noosphere = class {
|
|
1396
2513
|
config;
|
|
@@ -1573,14 +2690,38 @@ var Noosphere = class {
|
|
|
1573
2690
|
if (!this.initialized) await this.init();
|
|
1574
2691
|
return this.registry.getModel(provider, modelId);
|
|
1575
2692
|
}
|
|
1576
|
-
async syncModels() {
|
|
2693
|
+
async syncModels(modality) {
|
|
1577
2694
|
if (!this.initialized) await this.init();
|
|
1578
|
-
return this.registry.syncAll();
|
|
2695
|
+
return this.registry.syncAll(modality);
|
|
1579
2696
|
}
|
|
1580
2697
|
// --- Tracking Methods ---
|
|
1581
2698
|
getUsage(options) {
|
|
1582
2699
|
return this.tracker.getSummary(options);
|
|
1583
2700
|
}
|
|
2701
|
+
// --- Local Model Management ---
|
|
2702
|
+
async installModel(name) {
|
|
2703
|
+
if (!this.initialized) await this.init();
|
|
2704
|
+
const provider = this.registry.getProvider("ollama");
|
|
2705
|
+
if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
|
|
2706
|
+
return provider.pullModel(name);
|
|
2707
|
+
}
|
|
2708
|
+
async uninstallModel(name) {
|
|
2709
|
+
if (!this.initialized) await this.init();
|
|
2710
|
+
const provider = this.registry.getProvider("ollama");
|
|
2711
|
+
if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
|
|
2712
|
+
await provider.deleteModel(name);
|
|
2713
|
+
}
|
|
2714
|
+
async getHardware() {
|
|
2715
|
+
if (!this.initialized) await this.init();
|
|
2716
|
+
const provider = this.registry.getProvider("ollama");
|
|
2717
|
+
if (!provider) return { ollama: false, runningModels: [] };
|
|
2718
|
+
try {
|
|
2719
|
+
const runningModels = await provider.getRunningModels();
|
|
2720
|
+
return { ollama: true, runningModels };
|
|
2721
|
+
} catch {
|
|
2722
|
+
return { ollama: false, runningModels: [] };
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
1584
2725
|
// --- Lifecycle ---
|
|
1585
2726
|
async dispose() {
|
|
1586
2727
|
for (const provider of this.registry.getAllProviders()) {
|
|
@@ -1631,10 +2772,21 @@ var Noosphere = class {
|
|
|
1631
2772
|
return false;
|
|
1632
2773
|
}
|
|
1633
2774
|
};
|
|
2775
|
+
const ollamaCfg = local["ollama"];
|
|
1634
2776
|
const comfyuiCfg = local["comfyui"];
|
|
1635
2777
|
const piperCfg = local["piper"];
|
|
1636
2778
|
const kokoroCfg = local["kokoro"];
|
|
1637
2779
|
await Promise.allSettled([
|
|
2780
|
+
// Ollama — auto-detect even without explicit config
|
|
2781
|
+
(async () => {
|
|
2782
|
+
const host = ollamaCfg?.host ?? "http://localhost";
|
|
2783
|
+
const port = ollamaCfg?.port ?? 11434;
|
|
2784
|
+
const provider = new OllamaProvider({ host, port });
|
|
2785
|
+
const ok = await provider.ping();
|
|
2786
|
+
if (ok) {
|
|
2787
|
+
this.registry.addProvider(provider);
|
|
2788
|
+
}
|
|
2789
|
+
})(),
|
|
1638
2790
|
// ComfyUI
|
|
1639
2791
|
(async () => {
|
|
1640
2792
|
if (comfyuiCfg?.enabled) {
|
|
@@ -1661,6 +2813,29 @@ var Noosphere = class {
|
|
|
1661
2813
|
this.registry.addProvider(new LocalTTSProvider({ id: "kokoro", name: "Kokoro TTS", host: kokoroCfg.host, port: kokoroCfg.port }));
|
|
1662
2814
|
}
|
|
1663
2815
|
}
|
|
2816
|
+
})(),
|
|
2817
|
+
// HuggingFace local model catalog
|
|
2818
|
+
(async () => {
|
|
2819
|
+
this.registry.addProvider(new HfLocalProvider());
|
|
2820
|
+
})(),
|
|
2821
|
+
// Whisper local STT
|
|
2822
|
+
(async () => {
|
|
2823
|
+
const whisper = new WhisperLocalProvider();
|
|
2824
|
+
const ok = await whisper.ping();
|
|
2825
|
+
if (ok) this.registry.addProvider(whisper);
|
|
2826
|
+
})(),
|
|
2827
|
+
// AudioCraft local music generation
|
|
2828
|
+
(async () => {
|
|
2829
|
+
const audiocraft = new AudioCraftProvider();
|
|
2830
|
+
const ok = await audiocraft.ping();
|
|
2831
|
+
if (ok) this.registry.addProvider(audiocraft);
|
|
2832
|
+
})(),
|
|
2833
|
+
// Auto-detect OpenAI-compatible servers
|
|
2834
|
+
(async () => {
|
|
2835
|
+
const servers = await detectOpenAICompatServers();
|
|
2836
|
+
for (const server of servers) {
|
|
2837
|
+
this.registry.addProvider(server);
|
|
2838
|
+
}
|
|
1664
2839
|
})()
|
|
1665
2840
|
]);
|
|
1666
2841
|
}
|
|
@@ -1752,10 +2927,16 @@ var Noosphere = class {
|
|
|
1752
2927
|
}
|
|
1753
2928
|
};
|
|
1754
2929
|
export {
|
|
2930
|
+
AudioCraftProvider,
|
|
2931
|
+
HfLocalProvider,
|
|
1755
2932
|
Noosphere,
|
|
1756
2933
|
NoosphereError,
|
|
2934
|
+
OllamaProvider,
|
|
2935
|
+
OpenAICompatProvider,
|
|
1757
2936
|
PROVIDER_IDS,
|
|
1758
2937
|
PROVIDER_LOGOS,
|
|
2938
|
+
WhisperLocalProvider,
|
|
2939
|
+
detectOpenAICompatServers,
|
|
1759
2940
|
getAllProviderLogos,
|
|
1760
2941
|
getProviderLogo
|
|
1761
2942
|
};
|