Agent/utils/model-manager.ts
2026-08-06 22:17:39 +08:00

276 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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 };
}