276 lines
10 KiB
TypeScript
276 lines
10 KiB
TypeScript
// @ts-nocheck
|
||
|
||
|
||
import { loadOllamaConfig, loadOmlxConfig, loadSparkleConfig, loadModelSource, CONFIG, DEFAULT_OLLAMA, DEFAULT_OMLX, DEFAULT_SPARKLE } from './config.js';
|
||
|
||
export async function probeOllama(timeoutMs = 1500) {
|
||
const cfg = loadOllamaConfig();
|
||
const base = String(cfg.baseUrl || DEFAULT_OLLAMA.baseUrl).replace(/\/+$/, '');
|
||
try {
|
||
const r = await fetch(`${base}/api/version`, { signal: AbortSignal.timeout(timeoutMs) });
|
||
if (!r.ok) return { ok: false, baseUrl: base, model: cfg.model || '', error: `HTTP ${r.status}` };
|
||
return { ok: true, baseUrl: base, model: cfg.model || '' };
|
||
} catch (e) {
|
||
return { ok: false, baseUrl: base, model: cfg.model || '', error: e.message || String(e) };
|
||
}
|
||
}
|
||
|
||
export async function probeOmlx(timeoutMs = 1500) {
|
||
const cfg = loadOmlxConfig();
|
||
const base = String(cfg.baseUrl || DEFAULT_OMLX.baseUrl).replace(/\/+$/, '');
|
||
if (!String(cfg.apiKey || '').trim()) {
|
||
return { ok: false, baseUrl: base, model: cfg.model || '', reason: 'no_key' };
|
||
}
|
||
try {
|
||
const r = await fetch(`${base}/v1/models`, {
|
||
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
||
signal: AbortSignal.timeout(timeoutMs),
|
||
});
|
||
if (!r.ok) return { ok: false, baseUrl: base, model: cfg.model || '', error: `HTTP ${r.status}` };
|
||
return { ok: true, baseUrl: base, model: cfg.model || '' };
|
||
} catch (e) {
|
||
return { ok: false, baseUrl: base, model: cfg.model || '', error: e.message || String(e) };
|
||
}
|
||
}
|
||
|
||
export async function probeSparkle(timeoutMs = 8000) {
|
||
|
||
|
||
const cfg = loadSparkleConfig();
|
||
const base = String(cfg.baseUrl || DEFAULT_SPARKLE.baseUrl).replace(/\/+$/, '');
|
||
try {
|
||
const r = await fetch(`${base}/v1/models`, {
|
||
headers: { Authorization: `Bearer ${cfg.apiKey || 'sparkle'}` },
|
||
signal: AbortSignal.timeout(timeoutMs),
|
||
});
|
||
if (!r.ok) return { ok: false, baseUrl: base, model: cfg.model || '', error: `HTTP ${r.status}` };
|
||
return { ok: true, baseUrl: base, model: cfg.model || '' };
|
||
} catch (e) {
|
||
return { ok: false, baseUrl: base, model: cfg.model || '', error: e.message || String(e) };
|
||
}
|
||
}
|
||
|
||
export function localLlmUnavailableMessage(source, probe = {}) {
|
||
if (source === 'ollama') {
|
||
const base = probe.baseUrl || DEFAULT_OLLAMA.baseUrl;
|
||
return `未检测到 Ollama(${base})。请先安装并启动 Ollama,或在「配」中切换到云端 API / oMLX / Sparkle。`;
|
||
}
|
||
if (source === 'omlx') {
|
||
if (probe.reason === 'no_key') {
|
||
return '未配置 oMLX API Key。请在「配」中填写,或切换到云端 API / Ollama / Sparkle。';
|
||
}
|
||
const base = probe.baseUrl || DEFAULT_OMLX.baseUrl;
|
||
return `未检测到 oMLX(${base})。请先启动 oMLX,或在「配」中切换到云端 API / Ollama / Sparkle。`;
|
||
}
|
||
if (source === 'sparkle') {
|
||
const base = probe.baseUrl || DEFAULT_SPARKLE.baseUrl;
|
||
return `未检测到 Sparkle(${base})。请先启动 Sparkle 推理服务,或在「配」中切换到云端 API / Ollama / oMLX。`;
|
||
}
|
||
return '本地模型服务不可用,请检查配置。';
|
||
}
|
||
|
||
export async function ensureLocalSourceReady(source) {
|
||
const src = source || loadModelSource();
|
||
if (src === 'ollama') {
|
||
const p = await probeOllama();
|
||
if (!p.ok) throw new Error(localLlmUnavailableMessage('ollama', p));
|
||
const cfg = loadOllamaConfig();
|
||
if (!String(cfg.model || '').trim()) {
|
||
throw new Error('未选择 Ollama 模型。请在「配」中刷新模型列表并选择,或切换到云端 API。');
|
||
}
|
||
return p;
|
||
}
|
||
if (src === 'omlx') {
|
||
const p = await probeOmlx();
|
||
if (!p.ok) throw new Error(localLlmUnavailableMessage('omlx', p));
|
||
const cfg = loadOmlxConfig();
|
||
if (!String(cfg.model || '').trim()) {
|
||
throw new Error('未选择 oMLX 模型。请在「配」中刷新模型列表并选择,或切换到云端 API。');
|
||
}
|
||
return p;
|
||
}
|
||
if (src === 'sparkle') {
|
||
const p = await probeSparkle();
|
||
if (!p.ok) throw new Error(localLlmUnavailableMessage('sparkle', p));
|
||
const cfg = loadSparkleConfig();
|
||
if (!String(cfg.model || '').trim()) {
|
||
throw new Error('未选择 Sparkle 模型。请在「配」中刷新模型列表并选择,或切换到云端 API。');
|
||
}
|
||
return p;
|
||
}
|
||
return { ok: true };
|
||
}
|
||
|
||
export function friendlyLocalFetchError(source, err) {
|
||
const msg = String(err?.message || err || '');
|
||
if (/abort|AbortError/i.test(err?.name || '') || /abort/i.test(msg)) {
|
||
return err;
|
||
}
|
||
if (/Failed to fetch|NetworkError|Load failed|ECONNREFUSED|ENOTFOUND|fetch failed|Network request failed/i.test(msg)
|
||
|| err?.name === 'TypeError') {
|
||
return new Error(localLlmUnavailableMessage(source, {
|
||
baseUrl: source === 'omlx'
|
||
? (loadOmlxConfig().baseUrl || DEFAULT_OMLX.baseUrl)
|
||
: source === 'sparkle'
|
||
? (loadSparkleConfig().baseUrl || DEFAULT_SPARKLE.baseUrl)
|
||
: (loadOllamaConfig().baseUrl || DEFAULT_OLLAMA.baseUrl),
|
||
}));
|
||
}
|
||
return err instanceof Error ? err : new Error(msg);
|
||
}
|
||
|
||
export async function unloadOllamaModels() {
|
||
const cfg = loadOllamaConfig();
|
||
let unloaded = 0;
|
||
try {
|
||
const r = await fetch(`${cfg.baseUrl}/api/ps`, { signal: AbortSignal.timeout(1500) });
|
||
if (r.ok) {
|
||
const data = await r.json();
|
||
const models = data.models || [];
|
||
for (const m of models) {
|
||
await fetch(`${cfg.baseUrl}/api/generate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ model: m.name, keep_alive: 0 }),
|
||
keepalive: true,
|
||
});
|
||
unloaded++;
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
|
||
|
||
|
||
try {
|
||
await fetch(`${CONFIG.ZHONGTAI_BASE}/admin/kill-ollama-runner`, {
|
||
method: 'POST',
|
||
keepalive: true,
|
||
signal: AbortSignal.timeout(2000),
|
||
});
|
||
} catch (e) {}
|
||
|
||
return unloaded;
|
||
}
|
||
|
||
export async function unloadOmlxModels() {
|
||
const cfg = loadOmlxConfig();
|
||
if (!cfg.apiKey) return 0;
|
||
const base = cfg.baseUrl.replace(/\/+$/, '');
|
||
try {
|
||
const r = await fetch(`${base}/v1/models`, {
|
||
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
||
});
|
||
if (!r.ok) return 0;
|
||
const data = await r.json();
|
||
const models = data.data || [];
|
||
let unloaded = 0;
|
||
for (const m of models) {
|
||
|
||
const endpoints = [
|
||
`${base}/admin/api/models/${encodeURIComponent(m.id)}/unload`,
|
||
`${base}/v1/models/${encodeURIComponent(m.id)}/unload`,
|
||
];
|
||
for (const url of endpoints) {
|
||
try {
|
||
const resp = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
||
keepalive: true,
|
||
});
|
||
if (resp.ok) { unloaded++; break; }
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
return unloaded;
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
export async function unloadSparkleModels() {
|
||
|
||
|
||
try {
|
||
const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/stop`, {
|
||
method: 'POST',
|
||
keepalive: true,
|
||
signal: AbortSignal.timeout(20000),
|
||
});
|
||
if (!r.ok) return 0;
|
||
const data = await r.json().catch(() => null);
|
||
return data && data.ok ? 1 : 0;
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
export async function unloadAllLocalModels() {
|
||
const ollamaCount = await unloadOllamaModels();
|
||
const omlxCount = await unloadOmlxModels();
|
||
const sparkleCount = await unloadSparkleModels();
|
||
return { ollama: ollamaCount, omlx: omlxCount, sparkle: sparkleCount };
|
||
}
|
||
|
||
export async function probeLocalModelsLoaded() {
|
||
let ollama = 0;
|
||
let omlx = 0;
|
||
let sparkle = 0;
|
||
|
||
try {
|
||
const cfg = loadOllamaConfig();
|
||
const base = (cfg.baseUrl || '').replace(/\/+$/, '');
|
||
if (base) {
|
||
const r = await fetch(`${base}/api/ps`, { signal: AbortSignal.timeout(1500) });
|
||
if (r.ok) {
|
||
const data = await r.json();
|
||
ollama = (data.models || []).length;
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
|
||
try {
|
||
const cfg = loadOmlxConfig();
|
||
const base = (cfg.baseUrl || '').replace(/\/+$/, '');
|
||
if (base && cfg.apiKey) {
|
||
const r = await fetch(`${base}/v1/models`, {
|
||
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
||
signal: AbortSignal.timeout(1500),
|
||
});
|
||
if (r.ok) {
|
||
const data = await r.json();
|
||
|
||
|
||
|
||
const models = data.data || [];
|
||
omlx = models.filter(m => m.loaded || m.status === 'loaded' || m.running).length;
|
||
|
||
if (omlx === 0 && models.some(m => 'loaded' in m || 'status' in m || 'running' in m)) {
|
||
|
||
} else if (omlx === 0 && models.length > 0 && !models.some(m => ('loaded' in m) || ('status' in m) || ('running' in m))) {
|
||
|
||
omlx = 0;
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
|
||
try {
|
||
const cfg = loadSparkleConfig();
|
||
const base = (cfg.baseUrl || '').replace(/\/+$/, '');
|
||
if (base) {
|
||
const r = await fetch(`${base}/v1/models`, {
|
||
headers: { 'Authorization': `Bearer ${cfg.apiKey || 'sparkle'}` },
|
||
signal: AbortSignal.timeout(1500),
|
||
});
|
||
if (r.ok) {
|
||
const data = await r.json();
|
||
|
||
const models = data.data || [];
|
||
sparkle = models.filter(m => m.loaded || m.status === 'loaded' || m.running).length;
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
|
||
return { loaded: ollama + omlx + sparkle > 0, ollama, omlx, sparkle };
|
||
}
|