noosphere 0.4.1 → 0.7.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 +227 -19
- package/dist/index.cjs +1083 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -4
- package/dist/index.d.ts +149 -4
- package/dist/index.js +1077 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
- package/assets/logos/png/ai21.png +0 -0
- package/assets/logos/png/amazon.png +0 -0
- package/assets/logos/png/anthropic.png +0 -0
- package/assets/logos/png/baidu.png +0 -0
- package/assets/logos/png/bytedance.png +0 -0
- package/assets/logos/png/cerebras.png +0 -0
- package/assets/logos/png/cohere.png +0 -0
- package/assets/logos/png/comfyui.png +0 -0
- package/assets/logos/png/deepseek.png +0 -0
- package/assets/logos/png/fal.png +0 -0
- package/assets/logos/png/fireworks-ai.png +0 -0
- package/assets/logos/png/google.png +0 -0
- package/assets/logos/png/groq.png +0 -0
- package/assets/logos/png/huggingface.png +0 -0
- package/assets/logos/png/ibm.png +0 -0
- package/assets/logos/png/inflection.png +0 -0
- package/assets/logos/png/kokoro.png +0 -0
- package/assets/logos/png/meta.png +0 -0
- package/assets/logos/png/microsoft.png +0 -0
- package/assets/logos/png/minimax.png +0 -0
- package/assets/logos/png/mistral.png +0 -0
- package/assets/logos/png/nebius.png +0 -0
- package/assets/logos/png/novita.png +0 -0
- package/assets/logos/png/nvidia.png +0 -0
- package/assets/logos/png/ollama.png +0 -0
- package/assets/logos/png/openai.png +0 -0
- package/assets/logos/png/openrouter.png +0 -0
- package/assets/logos/png/perplexity.png +0 -0
- package/assets/logos/png/pi-ai.png +0 -0
- package/assets/logos/png/piper.png +0 -0
- package/assets/logos/png/qwen.png +0 -0
- package/assets/logos/png/replicate.png +0 -0
- package/assets/logos/png/sambanova.png +0 -0
- package/assets/logos/png/tencent.png +0 -0
- package/assets/logos/png/together.png +0 -0
- package/assets/logos/png/upstage.png +0 -0
- package/assets/logos/png/xai.png +0 -0
- package/assets/logos/png/xiaomi.png +0 -0
- package/assets/logos/png/zai.png +0 -0
- package/assets/logos/svg/amazon.svg +0 -1
- package/assets/logos/svg/anthropic.svg +0 -1
- package/assets/logos/svg/baidu.svg +0 -1
- package/assets/logos/svg/cerebras.svg +0 -1
- package/assets/logos/svg/cohere.svg +0 -1
- package/assets/logos/svg/deepseek.svg +0 -1
- package/assets/logos/svg/fireworks-ai.svg +0 -9
- package/assets/logos/svg/google.svg +0 -1
- package/assets/logos/svg/groq.svg +0 -1
- package/assets/logos/svg/huggingface.svg +0 -67
- package/assets/logos/svg/meta.svg +0 -1
- package/assets/logos/svg/microsoft.svg +0 -1
- package/assets/logos/svg/mistral.svg +0 -1
- package/assets/logos/svg/nebius.svg +0 -4
- package/assets/logos/svg/novita.svg +0 -11
- package/assets/logos/svg/nvidia.svg +0 -1
- package/assets/logos/svg/ollama.svg +0 -7
- package/assets/logos/svg/openai.svg +0 -1
- package/assets/logos/svg/openrouter.svg +0 -21
- package/assets/logos/svg/perplexity.svg +0 -24
- package/assets/logos/svg/qwen.svg +0 -1
- package/assets/logos/svg/replicate.svg +0 -12
- package/assets/logos/svg/together.svg +0 -1
- package/assets/logos/svg/xai.svg +0 -1
package/dist/index.js
CHANGED
|
@@ -109,20 +109,7 @@ function resolveConfig(input) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// src/logos.ts
|
|
112
|
-
|
|
113
|
-
import { dirname, join, resolve } from "path";
|
|
114
|
-
import { existsSync } from "fs";
|
|
115
|
-
var _assetsDir = null;
|
|
116
|
-
function assetsDir() {
|
|
117
|
-
if (_assetsDir) return _assetsDir;
|
|
118
|
-
try {
|
|
119
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
120
|
-
_assetsDir = resolve(dirname(__filename), "..", "assets", "logos");
|
|
121
|
-
} catch {
|
|
122
|
-
_assetsDir = resolve(__dirname, "..", "assets", "logos");
|
|
123
|
-
}
|
|
124
|
-
return _assetsDir;
|
|
125
|
-
}
|
|
112
|
+
var CDN_BASE = "https://blockchainstarter.nyc3.digitaloceanspaces.com/noosphere/logos";
|
|
126
113
|
var PROVIDER_IDS = [
|
|
127
114
|
// Cloud LLM
|
|
128
115
|
"openai",
|
|
@@ -170,6 +157,40 @@ var PROVIDER_IDS = [
|
|
|
170
157
|
"nebius",
|
|
171
158
|
"novita"
|
|
172
159
|
];
|
|
160
|
+
var HAS_SVG = /* @__PURE__ */ new Set([
|
|
161
|
+
"openai",
|
|
162
|
+
"anthropic",
|
|
163
|
+
"google",
|
|
164
|
+
"groq",
|
|
165
|
+
"mistral",
|
|
166
|
+
"xai",
|
|
167
|
+
"openrouter",
|
|
168
|
+
"cerebras",
|
|
169
|
+
"huggingface",
|
|
170
|
+
"ollama",
|
|
171
|
+
"meta",
|
|
172
|
+
"deepseek",
|
|
173
|
+
"microsoft",
|
|
174
|
+
"nvidia",
|
|
175
|
+
"qwen",
|
|
176
|
+
"cohere",
|
|
177
|
+
"perplexity",
|
|
178
|
+
"amazon",
|
|
179
|
+
"baidu",
|
|
180
|
+
"together",
|
|
181
|
+
"fireworks-ai",
|
|
182
|
+
"replicate",
|
|
183
|
+
"nebius",
|
|
184
|
+
"novita",
|
|
185
|
+
"comfyui",
|
|
186
|
+
"fal",
|
|
187
|
+
"kokoro",
|
|
188
|
+
"piper",
|
|
189
|
+
"sambanova",
|
|
190
|
+
"pi-ai",
|
|
191
|
+
"zai"
|
|
192
|
+
// NO SVG: bytedance, tencent, xiaomi, ibm, ai21, inflection, upstage, minimax
|
|
193
|
+
]);
|
|
173
194
|
var _cache = /* @__PURE__ */ new Map();
|
|
174
195
|
function getProviderLogo(providerId) {
|
|
175
196
|
if (!providerId) return void 0;
|
|
@@ -192,13 +213,12 @@ function getProviderLogo(providerId) {
|
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
if (!matchedId) return void 0;
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!logo.svg && !logo.png) return void 0;
|
|
216
|
+
const logo = {
|
|
217
|
+
png: `${CDN_BASE}/png/${matchedId}.png`
|
|
218
|
+
};
|
|
219
|
+
if (HAS_SVG.has(matchedId)) {
|
|
220
|
+
logo.svg = `${CDN_BASE}/svg/${matchedId}.svg`;
|
|
221
|
+
}
|
|
202
222
|
_cache.set(providerId, logo);
|
|
203
223
|
return logo;
|
|
204
224
|
}
|
|
@@ -374,7 +394,7 @@ var UsageTracker = class {
|
|
|
374
394
|
filtered = filtered.filter((e) => e.modality === options.modality);
|
|
375
395
|
}
|
|
376
396
|
const byProvider = {};
|
|
377
|
-
const byModality = { llm: 0, image: 0, video: 0, tts: 0 };
|
|
397
|
+
const byModality = { llm: 0, image: 0, video: 0, tts: 0, stt: 0, music: 0, embedding: 0 };
|
|
378
398
|
let totalCost = 0;
|
|
379
399
|
for (const event of filtered) {
|
|
380
400
|
totalCost += event.cost;
|
|
@@ -601,8 +621,8 @@ var PiAiProvider = class {
|
|
|
601
621
|
let aborted = false;
|
|
602
622
|
let resolveResult = null;
|
|
603
623
|
let rejectResult = null;
|
|
604
|
-
const resultPromise = new Promise((
|
|
605
|
-
resolveResult =
|
|
624
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
625
|
+
resolveResult = resolve;
|
|
606
626
|
rejectResult = reject;
|
|
607
627
|
});
|
|
608
628
|
const ensureModel = async () => {
|
|
@@ -999,11 +1019,55 @@ var ComfyUIProvider = class {
|
|
|
999
1019
|
}
|
|
1000
1020
|
}
|
|
1001
1021
|
async listModels(modality) {
|
|
1022
|
+
const models = [];
|
|
1023
|
+
const logo = getProviderLogo("comfyui");
|
|
1002
1024
|
try {
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1025
|
+
const controller = new AbortController();
|
|
1026
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1027
|
+
try {
|
|
1028
|
+
const res = await fetch(`${this.baseUrl}/object_info`, { signal: controller.signal });
|
|
1029
|
+
if (res.ok) {
|
|
1030
|
+
const objectInfo = await res.json();
|
|
1031
|
+
const ckptNode = objectInfo?.["CheckpointLoaderSimple"];
|
|
1032
|
+
const ckptNames = ckptNode?.input?.required?.ckpt_name?.[0] ?? [];
|
|
1033
|
+
for (const name of ckptNames) {
|
|
1034
|
+
if (modality && modality !== "image") continue;
|
|
1035
|
+
models.push({
|
|
1036
|
+
id: `comfyui-ckpt-${name}`,
|
|
1037
|
+
provider: "comfyui",
|
|
1038
|
+
name: `ComfyUI: ${name}`,
|
|
1039
|
+
modality: "image",
|
|
1040
|
+
local: true,
|
|
1041
|
+
cost: { price: 0, unit: "free" },
|
|
1042
|
+
logo,
|
|
1043
|
+
status: "installed",
|
|
1044
|
+
localInfo: { sizeBytes: 0, runtime: "comfyui" },
|
|
1045
|
+
capabilities: { maxWidth: 2048, maxHeight: 2048, supportsNegativePrompt: true }
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
const loraNode = objectInfo?.["LoraLoader"];
|
|
1049
|
+
const loraNames = loraNode?.input?.required?.lora_name?.[0] ?? [];
|
|
1050
|
+
for (const name of loraNames) {
|
|
1051
|
+
if (modality && modality !== "image") continue;
|
|
1052
|
+
models.push({
|
|
1053
|
+
id: `comfyui-lora-${name}`,
|
|
1054
|
+
provider: "comfyui",
|
|
1055
|
+
name: `LoRA: ${name}`,
|
|
1056
|
+
modality: "image",
|
|
1057
|
+
local: true,
|
|
1058
|
+
cost: { price: 0, unit: "free" },
|
|
1059
|
+
logo,
|
|
1060
|
+
status: "installed",
|
|
1061
|
+
localInfo: { sizeBytes: 0, runtime: "comfyui" }
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
} finally {
|
|
1066
|
+
clearTimeout(timer);
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
if (models.length === 0) {
|
|
1007
1071
|
if (!modality || modality === "image") {
|
|
1008
1072
|
models.push({
|
|
1009
1073
|
id: "comfyui-txt2img",
|
|
@@ -1028,10 +1092,43 @@ var ComfyUIProvider = class {
|
|
|
1028
1092
|
capabilities: { maxDuration: 10, supportsImageToVideo: true }
|
|
1029
1093
|
});
|
|
1030
1094
|
}
|
|
1031
|
-
return models;
|
|
1032
|
-
} catch {
|
|
1033
|
-
return [];
|
|
1034
1095
|
}
|
|
1096
|
+
if (!modality || modality === "image") {
|
|
1097
|
+
try {
|
|
1098
|
+
const controller = new AbortController();
|
|
1099
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1100
|
+
try {
|
|
1101
|
+
const res = await fetch(
|
|
1102
|
+
"https://civitai.com/api/v1/models?types=Checkpoint&sort=Highest%20Rated&limit=50&nsfw=false",
|
|
1103
|
+
{ signal: controller.signal }
|
|
1104
|
+
);
|
|
1105
|
+
if (res.ok) {
|
|
1106
|
+
const data = await res.json();
|
|
1107
|
+
for (const item of data.items ?? []) {
|
|
1108
|
+
const version = item.modelVersions?.[0];
|
|
1109
|
+
models.push({
|
|
1110
|
+
id: `civitai-${item.id}`,
|
|
1111
|
+
provider: "comfyui",
|
|
1112
|
+
name: item.name ?? `CivitAI Model ${item.id}`,
|
|
1113
|
+
modality: "image",
|
|
1114
|
+
local: true,
|
|
1115
|
+
cost: { price: 0, unit: "free" },
|
|
1116
|
+
logo,
|
|
1117
|
+
status: "available",
|
|
1118
|
+
localInfo: {
|
|
1119
|
+
sizeBytes: version?.files?.[0]?.sizeKB ? version.files[0].sizeKB * 1024 : 0,
|
|
1120
|
+
runtime: "comfyui"
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
} finally {
|
|
1126
|
+
clearTimeout(timer);
|
|
1127
|
+
}
|
|
1128
|
+
} catch {
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return models;
|
|
1035
1132
|
}
|
|
1036
1133
|
async image(options) {
|
|
1037
1134
|
const start = Date.now();
|
|
@@ -1371,6 +1468,890 @@ var HuggingFaceProvider = class {
|
|
|
1371
1468
|
}
|
|
1372
1469
|
};
|
|
1373
1470
|
|
|
1471
|
+
// src/providers/ollama.ts
|
|
1472
|
+
var OLLAMA_FAMILY_TO_PROVIDER = {
|
|
1473
|
+
"llama": "meta",
|
|
1474
|
+
"codellama": "meta",
|
|
1475
|
+
"gemma": "google",
|
|
1476
|
+
"gemma2": "google",
|
|
1477
|
+
"gemma3": "google",
|
|
1478
|
+
"qwen": "qwen",
|
|
1479
|
+
"qwen2": "qwen",
|
|
1480
|
+
"qwen2.5": "qwen",
|
|
1481
|
+
"qwen3": "qwen",
|
|
1482
|
+
"deepseek": "deepseek",
|
|
1483
|
+
"deepcoder": "deepseek",
|
|
1484
|
+
"deepscaler": "deepseek",
|
|
1485
|
+
"qwq": "qwen",
|
|
1486
|
+
"phi": "microsoft",
|
|
1487
|
+
"phi3": "microsoft",
|
|
1488
|
+
"phi4": "microsoft",
|
|
1489
|
+
"mistral": "mistral",
|
|
1490
|
+
"mixtral": "mistral",
|
|
1491
|
+
"codestral": "mistral",
|
|
1492
|
+
"ministral": "mistral",
|
|
1493
|
+
"nemotron": "nvidia",
|
|
1494
|
+
"command": "cohere",
|
|
1495
|
+
"command-r": "cohere",
|
|
1496
|
+
"gpt-oss": "openai",
|
|
1497
|
+
"starcoder": "huggingface",
|
|
1498
|
+
"falcon": "meta",
|
|
1499
|
+
"glm": "zai",
|
|
1500
|
+
"granite": "ibm",
|
|
1501
|
+
"olmo": "meta",
|
|
1502
|
+
"yi": "zai",
|
|
1503
|
+
"minimax": "minimax",
|
|
1504
|
+
"kimi": "meta",
|
|
1505
|
+
"dolphin": "ollama",
|
|
1506
|
+
"wizard": "ollama",
|
|
1507
|
+
"nomic": "ollama",
|
|
1508
|
+
"mxbai": "ollama",
|
|
1509
|
+
"bge": "ollama",
|
|
1510
|
+
"all-minilm": "ollama",
|
|
1511
|
+
"moondream": "ollama"
|
|
1512
|
+
};
|
|
1513
|
+
var VISION_MODELS = /* @__PURE__ */ new Set([
|
|
1514
|
+
"llava",
|
|
1515
|
+
"moondream",
|
|
1516
|
+
"minicpm-v",
|
|
1517
|
+
"llama3.2-vision",
|
|
1518
|
+
"qwen2.5vl",
|
|
1519
|
+
"gemma3",
|
|
1520
|
+
"llava-llama3",
|
|
1521
|
+
"llava-phi3",
|
|
1522
|
+
"bakllava"
|
|
1523
|
+
]);
|
|
1524
|
+
function inferLogoProvider(modelName, _family) {
|
|
1525
|
+
const base = modelName.split(":")[0].toLowerCase().replace(/^[^/]+\//, "");
|
|
1526
|
+
const sortedPrefixes = Object.entries(OLLAMA_FAMILY_TO_PROVIDER).sort((a, b) => b[0].length - a[0].length);
|
|
1527
|
+
for (const [prefix, provider] of sortedPrefixes) {
|
|
1528
|
+
if (base === prefix || base.startsWith(prefix)) return provider;
|
|
1529
|
+
}
|
|
1530
|
+
return "ollama";
|
|
1531
|
+
}
|
|
1532
|
+
function supportsVision(modelName) {
|
|
1533
|
+
const base = modelName.split(":")[0].toLowerCase();
|
|
1534
|
+
for (const v of VISION_MODELS) {
|
|
1535
|
+
if (base === v || base.startsWith(v)) return true;
|
|
1536
|
+
}
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
async function fetchJson(url, options) {
|
|
1540
|
+
const timeoutMs = options?.timeoutMs ?? 5e3;
|
|
1541
|
+
const controller = new AbortController();
|
|
1542
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1543
|
+
try {
|
|
1544
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
1545
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1546
|
+
return await res.json();
|
|
1547
|
+
} finally {
|
|
1548
|
+
clearTimeout(timer);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
var OllamaProvider = class {
|
|
1552
|
+
id = "ollama";
|
|
1553
|
+
name = "Ollama (Local)";
|
|
1554
|
+
modalities = ["llm"];
|
|
1555
|
+
isLocal = true;
|
|
1556
|
+
baseUrl;
|
|
1557
|
+
constructor(config) {
|
|
1558
|
+
const host = config?.host ?? "http://localhost";
|
|
1559
|
+
const port = config?.port ?? 11434;
|
|
1560
|
+
const cleanHost = host.replace(/\/+$/, "");
|
|
1561
|
+
const hasPort = /:\d+$/.test(cleanHost);
|
|
1562
|
+
this.baseUrl = hasPort ? cleanHost : `${cleanHost}:${port}`;
|
|
1563
|
+
}
|
|
1564
|
+
async ping() {
|
|
1565
|
+
try {
|
|
1566
|
+
await fetchJson(`${this.baseUrl}/api/version`, { timeoutMs: 2e3 });
|
|
1567
|
+
return true;
|
|
1568
|
+
} catch {
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async listModels(_modality) {
|
|
1573
|
+
if (_modality && _modality !== "llm") return [];
|
|
1574
|
+
const [localData, catalogData, runningData] = await Promise.all([
|
|
1575
|
+
fetchJson(`${this.baseUrl}/api/tags`, { timeoutMs: 5e3 }).catch(() => null),
|
|
1576
|
+
fetchJson("https://ollama.com/api/tags", { timeoutMs: 5e3 }).catch(() => null),
|
|
1577
|
+
fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 }).catch(() => null)
|
|
1578
|
+
]);
|
|
1579
|
+
const runningNames = /* @__PURE__ */ new Set();
|
|
1580
|
+
if (runningData?.models) {
|
|
1581
|
+
for (const m of runningData.models) {
|
|
1582
|
+
runningNames.add(m.name);
|
|
1583
|
+
runningNames.add(m.model);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
const models = /* @__PURE__ */ new Map();
|
|
1587
|
+
if (localData?.models) {
|
|
1588
|
+
for (const m of localData.models) {
|
|
1589
|
+
const isRunning = runningNames.has(m.name) || runningNames.has(m.model);
|
|
1590
|
+
models.set(m.name, this.toModelInfo(m, isRunning ? "running" : "installed", true));
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (catalogData?.models) {
|
|
1594
|
+
for (const m of catalogData.models) {
|
|
1595
|
+
const name = m.name;
|
|
1596
|
+
if (!models.has(name)) {
|
|
1597
|
+
models.set(name, this.toModelInfo(m, "available", false));
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return Array.from(models.values());
|
|
1602
|
+
}
|
|
1603
|
+
toModelInfo(m, status, isLocal) {
|
|
1604
|
+
const name = m.name ?? m.model ?? "unknown";
|
|
1605
|
+
const family = m.details?.family;
|
|
1606
|
+
const logoProvider = inferLogoProvider(name, family);
|
|
1607
|
+
return {
|
|
1608
|
+
id: name,
|
|
1609
|
+
provider: "ollama",
|
|
1610
|
+
name,
|
|
1611
|
+
modality: "llm",
|
|
1612
|
+
local: true,
|
|
1613
|
+
cost: { price: 0, unit: "free" },
|
|
1614
|
+
logo: getProviderLogo(logoProvider),
|
|
1615
|
+
status,
|
|
1616
|
+
localInfo: {
|
|
1617
|
+
sizeBytes: m.size ?? 0,
|
|
1618
|
+
family: family ?? m.details?.family,
|
|
1619
|
+
parameterSize: m.details?.parameter_size,
|
|
1620
|
+
quantization: m.details?.quantization_level,
|
|
1621
|
+
format: m.details?.format,
|
|
1622
|
+
digest: m.digest,
|
|
1623
|
+
modifiedAt: m.modified_at,
|
|
1624
|
+
running: status === "running",
|
|
1625
|
+
runtime: "ollama"
|
|
1626
|
+
},
|
|
1627
|
+
capabilities: {
|
|
1628
|
+
contextWindow: 128e3,
|
|
1629
|
+
supportsVision: supportsVision(name),
|
|
1630
|
+
supportsStreaming: true
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
async chat(options) {
|
|
1635
|
+
const start = Date.now();
|
|
1636
|
+
const messages = options.messages.map((m) => ({
|
|
1637
|
+
role: m.role,
|
|
1638
|
+
content: m.content
|
|
1639
|
+
}));
|
|
1640
|
+
const body = {
|
|
1641
|
+
model: options.model ?? "llama3.2",
|
|
1642
|
+
messages,
|
|
1643
|
+
stream: false
|
|
1644
|
+
};
|
|
1645
|
+
if (options.temperature !== void 0 || options.maxTokens !== void 0) {
|
|
1646
|
+
body.options = {};
|
|
1647
|
+
if (options.temperature !== void 0) body.options.temperature = options.temperature;
|
|
1648
|
+
if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
|
|
1649
|
+
}
|
|
1650
|
+
const res = await fetch(`${this.baseUrl}/api/chat`, {
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
headers: { "Content-Type": "application/json" },
|
|
1653
|
+
body: JSON.stringify(body)
|
|
1654
|
+
});
|
|
1655
|
+
if (!res.ok) {
|
|
1656
|
+
throw new Error(`Ollama chat failed: ${res.status} ${await res.text()}`);
|
|
1657
|
+
}
|
|
1658
|
+
const data = await res.json();
|
|
1659
|
+
return {
|
|
1660
|
+
content: data.message?.content ?? "",
|
|
1661
|
+
provider: "ollama",
|
|
1662
|
+
model: options.model ?? "llama3.2",
|
|
1663
|
+
modality: "llm",
|
|
1664
|
+
latencyMs: Date.now() - start,
|
|
1665
|
+
usage: {
|
|
1666
|
+
cost: 0,
|
|
1667
|
+
input: data.prompt_eval_count ?? 0,
|
|
1668
|
+
output: data.eval_count ?? 0,
|
|
1669
|
+
unit: "tokens"
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
stream(options) {
|
|
1674
|
+
const self = this;
|
|
1675
|
+
const start = Date.now();
|
|
1676
|
+
let aborted = false;
|
|
1677
|
+
let resolveResult = null;
|
|
1678
|
+
let rejectResult = null;
|
|
1679
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
1680
|
+
resolveResult = resolve;
|
|
1681
|
+
rejectResult = reject;
|
|
1682
|
+
});
|
|
1683
|
+
const messages = options.messages.map((m) => ({
|
|
1684
|
+
role: m.role,
|
|
1685
|
+
content: m.content
|
|
1686
|
+
}));
|
|
1687
|
+
const body = {
|
|
1688
|
+
model: options.model ?? "llama3.2",
|
|
1689
|
+
messages,
|
|
1690
|
+
stream: true
|
|
1691
|
+
};
|
|
1692
|
+
if (options.temperature !== void 0 || options.maxTokens !== void 0) {
|
|
1693
|
+
body.options = {};
|
|
1694
|
+
if (options.temperature !== void 0) body.options.temperature = options.temperature;
|
|
1695
|
+
if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
|
|
1696
|
+
}
|
|
1697
|
+
const asyncIterator = {
|
|
1698
|
+
async *[Symbol.asyncIterator]() {
|
|
1699
|
+
try {
|
|
1700
|
+
const res = await fetch(`${self.baseUrl}/api/chat`, {
|
|
1701
|
+
method: "POST",
|
|
1702
|
+
headers: { "Content-Type": "application/json" },
|
|
1703
|
+
body: JSON.stringify(body)
|
|
1704
|
+
});
|
|
1705
|
+
if (!res.ok) {
|
|
1706
|
+
throw new Error(`Ollama stream failed: ${res.status} ${await res.text()}`);
|
|
1707
|
+
}
|
|
1708
|
+
const reader = res.body.getReader();
|
|
1709
|
+
const decoder = new TextDecoder();
|
|
1710
|
+
let fullContent = "";
|
|
1711
|
+
let finalData = null;
|
|
1712
|
+
let buffer = "";
|
|
1713
|
+
while (!aborted) {
|
|
1714
|
+
const { done, value } = await reader.read();
|
|
1715
|
+
if (done) break;
|
|
1716
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1717
|
+
const lines = buffer.split("\n");
|
|
1718
|
+
buffer = lines.pop() ?? "";
|
|
1719
|
+
for (const line of lines) {
|
|
1720
|
+
if (!line.trim()) continue;
|
|
1721
|
+
try {
|
|
1722
|
+
const chunk = JSON.parse(line);
|
|
1723
|
+
if (chunk.message?.content) {
|
|
1724
|
+
fullContent += chunk.message.content;
|
|
1725
|
+
yield { type: "text_delta", delta: chunk.message.content };
|
|
1726
|
+
}
|
|
1727
|
+
if (chunk.done) {
|
|
1728
|
+
finalData = chunk;
|
|
1729
|
+
}
|
|
1730
|
+
} catch {
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const result = {
|
|
1735
|
+
content: fullContent,
|
|
1736
|
+
provider: "ollama",
|
|
1737
|
+
model: options.model ?? "llama3.2",
|
|
1738
|
+
modality: "llm",
|
|
1739
|
+
latencyMs: Date.now() - start,
|
|
1740
|
+
usage: {
|
|
1741
|
+
cost: 0,
|
|
1742
|
+
input: finalData?.prompt_eval_count ?? 0,
|
|
1743
|
+
output: finalData?.eval_count ?? 0,
|
|
1744
|
+
unit: "tokens"
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
resolveResult?.(result);
|
|
1748
|
+
yield { type: "done", result };
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1751
|
+
rejectResult?.(error);
|
|
1752
|
+
yield { type: "error", error };
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
return {
|
|
1757
|
+
[Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
|
|
1758
|
+
result: () => resultPromise,
|
|
1759
|
+
abort: () => {
|
|
1760
|
+
aborted = true;
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
// --- Extra model management methods ---
|
|
1765
|
+
async *pullModel(name) {
|
|
1766
|
+
const res = await fetch(`${this.baseUrl}/api/pull`, {
|
|
1767
|
+
method: "POST",
|
|
1768
|
+
headers: { "Content-Type": "application/json" },
|
|
1769
|
+
body: JSON.stringify({ name, stream: true })
|
|
1770
|
+
});
|
|
1771
|
+
if (!res.ok) {
|
|
1772
|
+
throw new Error(`Ollama pull failed: ${res.status} ${await res.text()}`);
|
|
1773
|
+
}
|
|
1774
|
+
const reader = res.body.getReader();
|
|
1775
|
+
const decoder = new TextDecoder();
|
|
1776
|
+
let buffer = "";
|
|
1777
|
+
while (true) {
|
|
1778
|
+
const { done, value } = await reader.read();
|
|
1779
|
+
if (done) break;
|
|
1780
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1781
|
+
const lines = buffer.split("\n");
|
|
1782
|
+
buffer = lines.pop() ?? "";
|
|
1783
|
+
for (const line of lines) {
|
|
1784
|
+
if (!line.trim()) continue;
|
|
1785
|
+
try {
|
|
1786
|
+
yield JSON.parse(line);
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
async deleteModel(name) {
|
|
1793
|
+
const res = await fetch(`${this.baseUrl}/api/delete`, {
|
|
1794
|
+
method: "DELETE",
|
|
1795
|
+
headers: { "Content-Type": "application/json" },
|
|
1796
|
+
body: JSON.stringify({ name })
|
|
1797
|
+
});
|
|
1798
|
+
if (!res.ok) {
|
|
1799
|
+
throw new Error(`Ollama delete failed: ${res.status} ${await res.text()}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
async showModel(name) {
|
|
1803
|
+
const res = await fetch(`${this.baseUrl}/api/show`, {
|
|
1804
|
+
method: "POST",
|
|
1805
|
+
headers: { "Content-Type": "application/json" },
|
|
1806
|
+
body: JSON.stringify({ name })
|
|
1807
|
+
});
|
|
1808
|
+
if (!res.ok) {
|
|
1809
|
+
throw new Error(`Ollama show failed: ${res.status} ${await res.text()}`);
|
|
1810
|
+
}
|
|
1811
|
+
return await res.json();
|
|
1812
|
+
}
|
|
1813
|
+
async getRunningModels() {
|
|
1814
|
+
const data = await fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 });
|
|
1815
|
+
return data?.models ?? [];
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// src/providers/hf-local.ts
|
|
1820
|
+
import { readdir, readFile } from "fs/promises";
|
|
1821
|
+
import { join } from "path";
|
|
1822
|
+
import { homedir } from "os";
|
|
1823
|
+
var FETCH_TIMEOUT_MS3 = 5e3;
|
|
1824
|
+
var HF_HUB_API2 = "https://huggingface.co/api/models";
|
|
1825
|
+
var PIPELINE_TAG_TO_MODALITY = {
|
|
1826
|
+
"text-to-image": "image",
|
|
1827
|
+
"text-to-video": "video",
|
|
1828
|
+
"text-to-audio": "tts",
|
|
1829
|
+
"text-to-speech": "tts",
|
|
1830
|
+
"automatic-speech-recognition": "stt"
|
|
1831
|
+
};
|
|
1832
|
+
var CATALOG_QUERIES = [
|
|
1833
|
+
{ pipeline_tag: "text-to-image", limit: 50 },
|
|
1834
|
+
{ pipeline_tag: "text-to-video", limit: 30 },
|
|
1835
|
+
{ pipeline_tag: "text-to-audio", limit: 30 },
|
|
1836
|
+
{ pipeline_tag: "text-to-speech", limit: 30 },
|
|
1837
|
+
{ pipeline_tag: "automatic-speech-recognition", limit: 30 },
|
|
1838
|
+
{ pipeline_tag: "text-to-image", limit: 100, library: "diffusers" }
|
|
1839
|
+
];
|
|
1840
|
+
async function fetchJsonTimeout(url, timeoutMs = FETCH_TIMEOUT_MS3) {
|
|
1841
|
+
const controller = new AbortController();
|
|
1842
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1843
|
+
try {
|
|
1844
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
1845
|
+
if (!res.ok) return null;
|
|
1846
|
+
return await res.json();
|
|
1847
|
+
} catch {
|
|
1848
|
+
return null;
|
|
1849
|
+
} finally {
|
|
1850
|
+
clearTimeout(timer);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
var HfLocalProvider = class {
|
|
1854
|
+
id = "hf-local";
|
|
1855
|
+
name = "HuggingFace Local Models";
|
|
1856
|
+
modalities = ["image", "video", "tts", "stt"];
|
|
1857
|
+
isLocal = true;
|
|
1858
|
+
cachedModels = null;
|
|
1859
|
+
async ping() {
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
async listModels(modality) {
|
|
1863
|
+
if (!this.cachedModels) {
|
|
1864
|
+
const [catalog, installed] = await Promise.all([
|
|
1865
|
+
this.fetchCatalog(),
|
|
1866
|
+
this.scanLocalCache()
|
|
1867
|
+
]);
|
|
1868
|
+
const modelMap = /* @__PURE__ */ new Map();
|
|
1869
|
+
for (const m of catalog) modelMap.set(m.id, m);
|
|
1870
|
+
for (const m of installed) modelMap.set(m.id, m);
|
|
1871
|
+
this.cachedModels = Array.from(modelMap.values());
|
|
1872
|
+
}
|
|
1873
|
+
if (modality) return this.cachedModels.filter((m) => m.modality === modality);
|
|
1874
|
+
return this.cachedModels;
|
|
1875
|
+
}
|
|
1876
|
+
async fetchCatalog() {
|
|
1877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1878
|
+
const models = [];
|
|
1879
|
+
const logo = getProviderLogo("huggingface");
|
|
1880
|
+
const results = await Promise.allSettled(
|
|
1881
|
+
CATALOG_QUERIES.map(async (q) => {
|
|
1882
|
+
const params = new URLSearchParams({
|
|
1883
|
+
pipeline_tag: q.pipeline_tag,
|
|
1884
|
+
sort: "downloads",
|
|
1885
|
+
limit: String(q.limit)
|
|
1886
|
+
});
|
|
1887
|
+
if (q.library) params.set("library", q.library);
|
|
1888
|
+
return fetchJsonTimeout(`${HF_HUB_API2}?${params}`);
|
|
1889
|
+
})
|
|
1890
|
+
);
|
|
1891
|
+
for (const result of results) {
|
|
1892
|
+
if (result.status !== "fulfilled" || !Array.isArray(result.value)) continue;
|
|
1893
|
+
for (const entry of result.value) {
|
|
1894
|
+
const id = entry.id ?? entry.modelId;
|
|
1895
|
+
if (!id || seen.has(id)) continue;
|
|
1896
|
+
seen.add(id);
|
|
1897
|
+
const pipelineTag = entry.pipeline_tag ?? "";
|
|
1898
|
+
const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
|
|
1899
|
+
models.push({
|
|
1900
|
+
id,
|
|
1901
|
+
provider: "hf-local",
|
|
1902
|
+
name: id.split("/").pop() ?? id,
|
|
1903
|
+
modality,
|
|
1904
|
+
local: true,
|
|
1905
|
+
cost: { price: 0, unit: "free" },
|
|
1906
|
+
logo,
|
|
1907
|
+
status: "available",
|
|
1908
|
+
localInfo: {
|
|
1909
|
+
sizeBytes: 0,
|
|
1910
|
+
runtime: "huggingface",
|
|
1911
|
+
family: entry.library_name
|
|
1912
|
+
},
|
|
1913
|
+
capabilities: {}
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return models;
|
|
1918
|
+
}
|
|
1919
|
+
async scanLocalCache() {
|
|
1920
|
+
const models = [];
|
|
1921
|
+
const cacheDir = join(homedir(), ".cache", "huggingface", "hub");
|
|
1922
|
+
const logo = getProviderLogo("huggingface");
|
|
1923
|
+
try {
|
|
1924
|
+
const entries = await readdir(cacheDir, { withFileTypes: true });
|
|
1925
|
+
for (const entry of entries) {
|
|
1926
|
+
if (!entry.isDirectory() || !entry.name.startsWith("models--")) continue;
|
|
1927
|
+
const parts = entry.name.replace("models--", "").split("--");
|
|
1928
|
+
const modelId = parts.join("/");
|
|
1929
|
+
const modelDir = join(cacheDir, entry.name);
|
|
1930
|
+
let snapshotHash;
|
|
1931
|
+
try {
|
|
1932
|
+
snapshotHash = (await readFile(join(modelDir, "refs", "main"), "utf-8")).trim();
|
|
1933
|
+
} catch {
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
let pipelineTag = "";
|
|
1937
|
+
const snapshotDir = join(modelDir, "snapshots", snapshotHash);
|
|
1938
|
+
try {
|
|
1939
|
+
const modelIndex = JSON.parse(await readFile(join(snapshotDir, "model_index.json"), "utf-8"));
|
|
1940
|
+
if (modelIndex._class_name?.includes("Stable") || modelIndex._class_name?.includes("Flux")) {
|
|
1941
|
+
pipelineTag = "text-to-image";
|
|
1942
|
+
} else if (modelIndex._class_name?.includes("Video") || modelIndex._class_name?.includes("Animate")) {
|
|
1943
|
+
pipelineTag = "text-to-video";
|
|
1944
|
+
}
|
|
1945
|
+
} catch {
|
|
1946
|
+
try {
|
|
1947
|
+
const config = JSON.parse(await readFile(join(snapshotDir, "config.json"), "utf-8"));
|
|
1948
|
+
if (config.task_specific_params?.["text-to-image"]) pipelineTag = "text-to-image";
|
|
1949
|
+
else if (config.model_type?.includes("whisper")) pipelineTag = "automatic-speech-recognition";
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
|
|
1954
|
+
models.push({
|
|
1955
|
+
id: modelId,
|
|
1956
|
+
provider: "hf-local",
|
|
1957
|
+
name: modelId.split("/").pop() ?? modelId,
|
|
1958
|
+
modality,
|
|
1959
|
+
local: true,
|
|
1960
|
+
cost: { price: 0, unit: "free" },
|
|
1961
|
+
logo,
|
|
1962
|
+
status: "installed",
|
|
1963
|
+
localInfo: {
|
|
1964
|
+
sizeBytes: 0,
|
|
1965
|
+
runtime: "huggingface",
|
|
1966
|
+
diskPath: snapshotDir
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
} catch {
|
|
1971
|
+
}
|
|
1972
|
+
return models;
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
|
|
1976
|
+
// src/providers/whisper-local.ts
|
|
1977
|
+
import { execFile } from "child_process";
|
|
1978
|
+
import { access } from "fs/promises";
|
|
1979
|
+
import { join as join2 } from "path";
|
|
1980
|
+
import { homedir as homedir2 } from "os";
|
|
1981
|
+
var WHISPER_MODELS = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "turbo"];
|
|
1982
|
+
function runPython(code, timeoutMs = 5e3) {
|
|
1983
|
+
return new Promise((resolve, reject) => {
|
|
1984
|
+
const proc = execFile("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
|
|
1985
|
+
if (err) reject(err);
|
|
1986
|
+
else resolve(stdout.trim());
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
async function fileExists(path) {
|
|
1991
|
+
try {
|
|
1992
|
+
await access(path);
|
|
1993
|
+
return true;
|
|
1994
|
+
} catch {
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
var WhisperLocalProvider = class {
|
|
1999
|
+
id = "whisper-local";
|
|
2000
|
+
name = "Whisper (Local)";
|
|
2001
|
+
modalities = ["stt"];
|
|
2002
|
+
isLocal = true;
|
|
2003
|
+
runtime = null;
|
|
2004
|
+
async ping() {
|
|
2005
|
+
return await this.detectRuntime() !== null;
|
|
2006
|
+
}
|
|
2007
|
+
async detectRuntime() {
|
|
2008
|
+
if (this.runtime) return this.runtime;
|
|
2009
|
+
try {
|
|
2010
|
+
await runPython('import faster_whisper; print("ok")');
|
|
2011
|
+
this.runtime = "faster-whisper";
|
|
2012
|
+
return this.runtime;
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
try {
|
|
2016
|
+
await runPython("import whisper; print(whisper.__version__)");
|
|
2017
|
+
this.runtime = "whisper";
|
|
2018
|
+
return this.runtime;
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
async listModels(_modality) {
|
|
2024
|
+
if (_modality && _modality !== "stt") return [];
|
|
2025
|
+
const runtime = await this.detectRuntime();
|
|
2026
|
+
if (!runtime) return [];
|
|
2027
|
+
const logo = getProviderLogo("huggingface");
|
|
2028
|
+
const models = [];
|
|
2029
|
+
for (const name of WHISPER_MODELS) {
|
|
2030
|
+
const installed = await this.isModelCached(name, runtime);
|
|
2031
|
+
models.push({
|
|
2032
|
+
id: `whisper-${name}`,
|
|
2033
|
+
provider: "whisper-local",
|
|
2034
|
+
name: `Whisper ${name}`,
|
|
2035
|
+
modality: "stt",
|
|
2036
|
+
local: true,
|
|
2037
|
+
cost: { price: 0, unit: "free" },
|
|
2038
|
+
logo,
|
|
2039
|
+
status: installed ? "installed" : "available",
|
|
2040
|
+
localInfo: {
|
|
2041
|
+
sizeBytes: 0,
|
|
2042
|
+
runtime
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
return models;
|
|
2047
|
+
}
|
|
2048
|
+
async isModelCached(name, runtime) {
|
|
2049
|
+
if (runtime === "whisper") {
|
|
2050
|
+
return fileExists(join2(homedir2(), ".cache", "whisper", `${name}.pt`));
|
|
2051
|
+
}
|
|
2052
|
+
const hfDir = join2(homedir2(), ".cache", "huggingface", "hub", `models--Systran--faster-whisper-${name}`);
|
|
2053
|
+
return fileExists(hfDir);
|
|
2054
|
+
}
|
|
2055
|
+
async transcribe(options) {
|
|
2056
|
+
const runtime = await this.detectRuntime();
|
|
2057
|
+
if (!runtime) throw new Error("Whisper is not installed");
|
|
2058
|
+
const model = options.model?.replace("whisper-", "") ?? "base";
|
|
2059
|
+
const lang = options.language ? `--language ${options.language}` : "";
|
|
2060
|
+
const task = options.task ?? "transcribe";
|
|
2061
|
+
if (runtime === "faster-whisper") {
|
|
2062
|
+
const code = `
|
|
2063
|
+
import json, sys
|
|
2064
|
+
from faster_whisper import WhisperModel
|
|
2065
|
+
model = WhisperModel("${model}")
|
|
2066
|
+
segments, info = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
|
|
2067
|
+
segs = [{"start": s.start, "end": s.end, "text": s.text} for s in segments]
|
|
2068
|
+
print(json.dumps({"text": " ".join(s["text"] for s in segs), "language": info.language, "duration": info.duration, "segments": segs}))
|
|
2069
|
+
`;
|
|
2070
|
+
const output = await runPython(code, 12e4);
|
|
2071
|
+
return JSON.parse(output);
|
|
2072
|
+
} else {
|
|
2073
|
+
const code = `
|
|
2074
|
+
import json, whisper
|
|
2075
|
+
model = whisper.load_model("${model}")
|
|
2076
|
+
result = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
|
|
2077
|
+
segs = [{"start": s["start"], "end": s["end"], "text": s["text"]} for s in result.get("segments", [])]
|
|
2078
|
+
print(json.dumps({"text": result["text"], "language": result.get("language", ""), "duration": 0, "segments": segs}))
|
|
2079
|
+
`;
|
|
2080
|
+
const output = await runPython(code, 12e4);
|
|
2081
|
+
return JSON.parse(output);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
|
|
2086
|
+
// src/providers/audiocraft.ts
|
|
2087
|
+
import { execFile as execFile2 } from "child_process";
|
|
2088
|
+
import { access as access2 } from "fs/promises";
|
|
2089
|
+
import { join as join3 } from "path";
|
|
2090
|
+
import { homedir as homedir3 } from "os";
|
|
2091
|
+
var AUDIOCRAFT_MODELS = [
|
|
2092
|
+
{ id: "musicgen-small", name: "MusicGen Small" },
|
|
2093
|
+
{ id: "musicgen-medium", name: "MusicGen Medium" },
|
|
2094
|
+
{ id: "musicgen-large", name: "MusicGen Large" },
|
|
2095
|
+
{ id: "musicgen-melody", name: "MusicGen Melody" },
|
|
2096
|
+
{ id: "audiogen-medium", name: "AudioGen Medium" }
|
|
2097
|
+
];
|
|
2098
|
+
function runPython2(code, timeoutMs = 5e3) {
|
|
2099
|
+
return new Promise((resolve, reject) => {
|
|
2100
|
+
execFile2("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
|
|
2101
|
+
if (err) reject(err);
|
|
2102
|
+
else resolve(stdout.trim());
|
|
2103
|
+
});
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
async function fileExists2(path) {
|
|
2107
|
+
try {
|
|
2108
|
+
await access2(path);
|
|
2109
|
+
return true;
|
|
2110
|
+
} catch {
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
var AudioCraftProvider = class {
|
|
2115
|
+
id = "audiocraft";
|
|
2116
|
+
name = "AudioCraft (Local)";
|
|
2117
|
+
modalities = ["music"];
|
|
2118
|
+
isLocal = true;
|
|
2119
|
+
detected = null;
|
|
2120
|
+
async ping() {
|
|
2121
|
+
if (this.detected !== null) return this.detected;
|
|
2122
|
+
try {
|
|
2123
|
+
await runPython2('import audiocraft; print("ok")');
|
|
2124
|
+
this.detected = true;
|
|
2125
|
+
} catch {
|
|
2126
|
+
this.detected = false;
|
|
2127
|
+
}
|
|
2128
|
+
return this.detected;
|
|
2129
|
+
}
|
|
2130
|
+
async listModels(_modality) {
|
|
2131
|
+
if (_modality && _modality !== "music") return [];
|
|
2132
|
+
if (!await this.ping()) return [];
|
|
2133
|
+
const logo = getProviderLogo("meta");
|
|
2134
|
+
const models = [];
|
|
2135
|
+
for (const m of AUDIOCRAFT_MODELS) {
|
|
2136
|
+
const hfDir = join3(homedir3(), ".cache", "huggingface", "hub", `models--facebook--${m.id}`);
|
|
2137
|
+
const installed = await fileExists2(hfDir);
|
|
2138
|
+
models.push({
|
|
2139
|
+
id: m.id,
|
|
2140
|
+
provider: "audiocraft",
|
|
2141
|
+
name: m.name,
|
|
2142
|
+
modality: "music",
|
|
2143
|
+
local: true,
|
|
2144
|
+
cost: { price: 0, unit: "free" },
|
|
2145
|
+
logo,
|
|
2146
|
+
status: installed ? "installed" : "available",
|
|
2147
|
+
localInfo: {
|
|
2148
|
+
sizeBytes: 0,
|
|
2149
|
+
runtime: "audiocraft"
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
return models;
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
// src/providers/openai-compat.ts
|
|
2158
|
+
var FETCH_TIMEOUT_MS4 = 5e3;
|
|
2159
|
+
var KNOWN_LOCAL_SERVERS = [
|
|
2160
|
+
{ port: 8080, name: "llama.cpp / LocalAI", id: "llamacpp" },
|
|
2161
|
+
{ port: 1234, name: "LM Studio", id: "lmstudio" },
|
|
2162
|
+
{ port: 8e3, name: "vLLM", id: "vllm" },
|
|
2163
|
+
{ port: 5e3, name: "TabbyAPI", id: "tabbyapi" },
|
|
2164
|
+
{ port: 5001, name: "KoboldCpp", id: "koboldcpp" },
|
|
2165
|
+
{ port: 1337, name: "Jan", id: "jan" }
|
|
2166
|
+
];
|
|
2167
|
+
async function fetchJsonTimeout2(url, headers, timeoutMs = FETCH_TIMEOUT_MS4) {
|
|
2168
|
+
const controller = new AbortController();
|
|
2169
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2170
|
+
try {
|
|
2171
|
+
const res = await fetch(url, { signal: controller.signal, headers });
|
|
2172
|
+
if (!res.ok) return null;
|
|
2173
|
+
return await res.json();
|
|
2174
|
+
} catch {
|
|
2175
|
+
return null;
|
|
2176
|
+
} finally {
|
|
2177
|
+
clearTimeout(timer);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
var OpenAICompatProvider = class {
|
|
2181
|
+
id;
|
|
2182
|
+
name;
|
|
2183
|
+
modalities = ["llm"];
|
|
2184
|
+
isLocal = true;
|
|
2185
|
+
baseUrl;
|
|
2186
|
+
headers;
|
|
2187
|
+
constructor(config) {
|
|
2188
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2189
|
+
this.id = config.id ?? `openai-compat-${new URL(config.baseUrl).port}`;
|
|
2190
|
+
this.name = config.name ?? `OpenAI-Compatible (${this.baseUrl})`;
|
|
2191
|
+
this.headers = { "Content-Type": "application/json" };
|
|
2192
|
+
if (config.apiKey) {
|
|
2193
|
+
this.headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
async ping() {
|
|
2197
|
+
const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers, 2e3);
|
|
2198
|
+
return data !== null;
|
|
2199
|
+
}
|
|
2200
|
+
async listModels(_modality) {
|
|
2201
|
+
if (_modality && _modality !== "llm") return [];
|
|
2202
|
+
const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers);
|
|
2203
|
+
if (!data?.data || !Array.isArray(data.data)) return [];
|
|
2204
|
+
const logo = getProviderLogo("openai");
|
|
2205
|
+
return data.data.map((m) => ({
|
|
2206
|
+
id: m.id,
|
|
2207
|
+
provider: this.id,
|
|
2208
|
+
name: m.id,
|
|
2209
|
+
modality: "llm",
|
|
2210
|
+
local: true,
|
|
2211
|
+
cost: { price: 0, unit: "free" },
|
|
2212
|
+
logo,
|
|
2213
|
+
status: "running",
|
|
2214
|
+
localInfo: {
|
|
2215
|
+
sizeBytes: 0,
|
|
2216
|
+
runtime: this.id
|
|
2217
|
+
},
|
|
2218
|
+
capabilities: {
|
|
2219
|
+
supportsStreaming: true
|
|
2220
|
+
}
|
|
2221
|
+
}));
|
|
2222
|
+
}
|
|
2223
|
+
async chat(options) {
|
|
2224
|
+
const start = Date.now();
|
|
2225
|
+
const model = options.model ?? "default";
|
|
2226
|
+
const body = {
|
|
2227
|
+
model,
|
|
2228
|
+
messages: options.messages,
|
|
2229
|
+
stream: false
|
|
2230
|
+
};
|
|
2231
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
2232
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
2233
|
+
if (options.jsonMode) body.response_format = { type: "json_object" };
|
|
2234
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
2235
|
+
method: "POST",
|
|
2236
|
+
headers: this.headers,
|
|
2237
|
+
body: JSON.stringify(body)
|
|
2238
|
+
});
|
|
2239
|
+
if (!res.ok) throw new Error(`OpenAI-compat chat failed: ${res.status} ${await res.text()}`);
|
|
2240
|
+
const data = await res.json();
|
|
2241
|
+
const choice = data.choices?.[0];
|
|
2242
|
+
return {
|
|
2243
|
+
content: choice?.message?.content ?? "",
|
|
2244
|
+
provider: this.id,
|
|
2245
|
+
model,
|
|
2246
|
+
modality: "llm",
|
|
2247
|
+
latencyMs: Date.now() - start,
|
|
2248
|
+
usage: {
|
|
2249
|
+
cost: 0,
|
|
2250
|
+
input: data.usage?.prompt_tokens ?? 0,
|
|
2251
|
+
output: data.usage?.completion_tokens ?? 0,
|
|
2252
|
+
unit: "tokens"
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
stream(options) {
|
|
2257
|
+
const self = this;
|
|
2258
|
+
const start = Date.now();
|
|
2259
|
+
let aborted = false;
|
|
2260
|
+
let resolveResult = null;
|
|
2261
|
+
let rejectResult = null;
|
|
2262
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
2263
|
+
resolveResult = resolve;
|
|
2264
|
+
rejectResult = reject;
|
|
2265
|
+
});
|
|
2266
|
+
const model = options.model ?? "default";
|
|
2267
|
+
const body = {
|
|
2268
|
+
model,
|
|
2269
|
+
messages: options.messages,
|
|
2270
|
+
stream: true
|
|
2271
|
+
};
|
|
2272
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
2273
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
2274
|
+
const asyncIterator = {
|
|
2275
|
+
async *[Symbol.asyncIterator]() {
|
|
2276
|
+
try {
|
|
2277
|
+
const res = await fetch(`${self.baseUrl}/v1/chat/completions`, {
|
|
2278
|
+
method: "POST",
|
|
2279
|
+
headers: self.headers,
|
|
2280
|
+
body: JSON.stringify(body)
|
|
2281
|
+
});
|
|
2282
|
+
if (!res.ok) throw new Error(`Stream failed: ${res.status} ${await res.text()}`);
|
|
2283
|
+
const reader = res.body.getReader();
|
|
2284
|
+
const decoder = new TextDecoder();
|
|
2285
|
+
let fullContent = "";
|
|
2286
|
+
let buffer = "";
|
|
2287
|
+
while (!aborted) {
|
|
2288
|
+
const { done, value } = await reader.read();
|
|
2289
|
+
if (done) break;
|
|
2290
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2291
|
+
const lines = buffer.split("\n");
|
|
2292
|
+
buffer = lines.pop() ?? "";
|
|
2293
|
+
for (const line of lines) {
|
|
2294
|
+
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
|
|
2295
|
+
try {
|
|
2296
|
+
const chunk = JSON.parse(line.slice(6));
|
|
2297
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
2298
|
+
if (delta) {
|
|
2299
|
+
fullContent += delta;
|
|
2300
|
+
yield { type: "text_delta", delta };
|
|
2301
|
+
}
|
|
2302
|
+
} catch {
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const result = {
|
|
2307
|
+
content: fullContent,
|
|
2308
|
+
provider: self.id,
|
|
2309
|
+
model,
|
|
2310
|
+
modality: "llm",
|
|
2311
|
+
latencyMs: Date.now() - start,
|
|
2312
|
+
usage: { cost: 0, unit: "tokens" }
|
|
2313
|
+
};
|
|
2314
|
+
resolveResult?.(result);
|
|
2315
|
+
yield { type: "done", result };
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2318
|
+
rejectResult?.(error);
|
|
2319
|
+
yield { type: "error", error };
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2323
|
+
return {
|
|
2324
|
+
[Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
|
|
2325
|
+
result: () => resultPromise,
|
|
2326
|
+
abort: () => {
|
|
2327
|
+
aborted = true;
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
async function detectOpenAICompatServers() {
|
|
2333
|
+
const providers = [];
|
|
2334
|
+
const results = await Promise.allSettled(
|
|
2335
|
+
KNOWN_LOCAL_SERVERS.map(async (server) => {
|
|
2336
|
+
const baseUrl = `http://localhost:${server.port}`;
|
|
2337
|
+
const provider = new OpenAICompatProvider({
|
|
2338
|
+
baseUrl,
|
|
2339
|
+
name: server.name,
|
|
2340
|
+
id: server.id
|
|
2341
|
+
});
|
|
2342
|
+
const ok = await provider.ping();
|
|
2343
|
+
if (ok) return provider;
|
|
2344
|
+
return null;
|
|
2345
|
+
})
|
|
2346
|
+
);
|
|
2347
|
+
for (const result of results) {
|
|
2348
|
+
if (result.status === "fulfilled" && result.value) {
|
|
2349
|
+
providers.push(result.value);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
return providers;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
1374
2355
|
// src/noosphere.ts
|
|
1375
2356
|
var Noosphere = class {
|
|
1376
2357
|
config;
|
|
@@ -1561,6 +2542,30 @@ var Noosphere = class {
|
|
|
1561
2542
|
getUsage(options) {
|
|
1562
2543
|
return this.tracker.getSummary(options);
|
|
1563
2544
|
}
|
|
2545
|
+
// --- Local Model Management ---
|
|
2546
|
+
async installModel(name) {
|
|
2547
|
+
if (!this.initialized) await this.init();
|
|
2548
|
+
const provider = this.registry.getProvider("ollama");
|
|
2549
|
+
if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
|
|
2550
|
+
return provider.pullModel(name);
|
|
2551
|
+
}
|
|
2552
|
+
async uninstallModel(name) {
|
|
2553
|
+
if (!this.initialized) await this.init();
|
|
2554
|
+
const provider = this.registry.getProvider("ollama");
|
|
2555
|
+
if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
|
|
2556
|
+
await provider.deleteModel(name);
|
|
2557
|
+
}
|
|
2558
|
+
async getHardware() {
|
|
2559
|
+
if (!this.initialized) await this.init();
|
|
2560
|
+
const provider = this.registry.getProvider("ollama");
|
|
2561
|
+
if (!provider) return { ollama: false, runningModels: [] };
|
|
2562
|
+
try {
|
|
2563
|
+
const runningModels = await provider.getRunningModels();
|
|
2564
|
+
return { ollama: true, runningModels };
|
|
2565
|
+
} catch {
|
|
2566
|
+
return { ollama: false, runningModels: [] };
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
1564
2569
|
// --- Lifecycle ---
|
|
1565
2570
|
async dispose() {
|
|
1566
2571
|
for (const provider of this.registry.getAllProviders()) {
|
|
@@ -1611,10 +2616,21 @@ var Noosphere = class {
|
|
|
1611
2616
|
return false;
|
|
1612
2617
|
}
|
|
1613
2618
|
};
|
|
2619
|
+
const ollamaCfg = local["ollama"];
|
|
1614
2620
|
const comfyuiCfg = local["comfyui"];
|
|
1615
2621
|
const piperCfg = local["piper"];
|
|
1616
2622
|
const kokoroCfg = local["kokoro"];
|
|
1617
2623
|
await Promise.allSettled([
|
|
2624
|
+
// Ollama — auto-detect even without explicit config
|
|
2625
|
+
(async () => {
|
|
2626
|
+
const host = ollamaCfg?.host ?? "http://localhost";
|
|
2627
|
+
const port = ollamaCfg?.port ?? 11434;
|
|
2628
|
+
const provider = new OllamaProvider({ host, port });
|
|
2629
|
+
const ok = await provider.ping();
|
|
2630
|
+
if (ok) {
|
|
2631
|
+
this.registry.addProvider(provider);
|
|
2632
|
+
}
|
|
2633
|
+
})(),
|
|
1618
2634
|
// ComfyUI
|
|
1619
2635
|
(async () => {
|
|
1620
2636
|
if (comfyuiCfg?.enabled) {
|
|
@@ -1641,6 +2657,29 @@ var Noosphere = class {
|
|
|
1641
2657
|
this.registry.addProvider(new LocalTTSProvider({ id: "kokoro", name: "Kokoro TTS", host: kokoroCfg.host, port: kokoroCfg.port }));
|
|
1642
2658
|
}
|
|
1643
2659
|
}
|
|
2660
|
+
})(),
|
|
2661
|
+
// HuggingFace local model catalog
|
|
2662
|
+
(async () => {
|
|
2663
|
+
this.registry.addProvider(new HfLocalProvider());
|
|
2664
|
+
})(),
|
|
2665
|
+
// Whisper local STT
|
|
2666
|
+
(async () => {
|
|
2667
|
+
const whisper = new WhisperLocalProvider();
|
|
2668
|
+
const ok = await whisper.ping();
|
|
2669
|
+
if (ok) this.registry.addProvider(whisper);
|
|
2670
|
+
})(),
|
|
2671
|
+
// AudioCraft local music generation
|
|
2672
|
+
(async () => {
|
|
2673
|
+
const audiocraft = new AudioCraftProvider();
|
|
2674
|
+
const ok = await audiocraft.ping();
|
|
2675
|
+
if (ok) this.registry.addProvider(audiocraft);
|
|
2676
|
+
})(),
|
|
2677
|
+
// Auto-detect OpenAI-compatible servers
|
|
2678
|
+
(async () => {
|
|
2679
|
+
const servers = await detectOpenAICompatServers();
|
|
2680
|
+
for (const server of servers) {
|
|
2681
|
+
this.registry.addProvider(server);
|
|
2682
|
+
}
|
|
1644
2683
|
})()
|
|
1645
2684
|
]);
|
|
1646
2685
|
}
|
|
@@ -1691,7 +2730,7 @@ var Noosphere = class {
|
|
|
1691
2730
|
break;
|
|
1692
2731
|
}
|
|
1693
2732
|
const delay = backoffMs * Math.pow(2, attempt);
|
|
1694
|
-
await new Promise((
|
|
2733
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1695
2734
|
}
|
|
1696
2735
|
}
|
|
1697
2736
|
throw lastError ?? new NoosphereError("Generation failed", {
|
|
@@ -1732,10 +2771,16 @@ var Noosphere = class {
|
|
|
1732
2771
|
}
|
|
1733
2772
|
};
|
|
1734
2773
|
export {
|
|
2774
|
+
AudioCraftProvider,
|
|
2775
|
+
HfLocalProvider,
|
|
1735
2776
|
Noosphere,
|
|
1736
2777
|
NoosphereError,
|
|
2778
|
+
OllamaProvider,
|
|
2779
|
+
OpenAICompatProvider,
|
|
1737
2780
|
PROVIDER_IDS,
|
|
1738
2781
|
PROVIDER_LOGOS,
|
|
2782
|
+
WhisperLocalProvider,
|
|
2783
|
+
detectOpenAICompatServers,
|
|
1739
2784
|
getAllProviderLogos,
|
|
1740
2785
|
getProviderLogo
|
|
1741
2786
|
};
|