Files
OllaHub/lib/ollama.ts
T
Evandro Fonseca Junior a8bb7cc22d feat(ollama): enhance model pull with improved error handling, persistence, and UI
- Add catalog metadata lookup for better error messages with hints (e.g., for missing manifests or tags)
- Persist downloaded model references in saved remote models for future reuse
- Replace searchRemoteModels import with getSavedRemoteModels in search route
- Enhance pull progress UI to display download bytes, digest, and fallback message when data is unavailable
- Improve error payload with model details, URL, and tags count to guide users toward valid tags or models
2025-10-29 19:42:24 -03:00

201 lines
5.6 KiB
TypeScript
Raw 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.
import { Ollama } from "ollama";
import os from "node:os";
import { spawn } from "node:child_process";
import { getCatalogModels } from "./catalog";
export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
export async function listModelsViaSdk() {
try {
const client = new Ollama({ host: OLLAMA_HOST });
const res = await client.list();
return res?.models ?? [];
} catch (e) {
throw e;
}
}
export async function listModelsViaHttp() {
const url = `${OLLAMA_HOST}/api/tags`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) throw new Error(`Ollama HTTP error ${res.status}`);
const data = await res.json();
return data?.models ?? [];
}
async function isOllamaHealthy(): Promise<boolean> {
try {
await listModelsViaHttp();
return true;
} catch {
return false;
}
}
async function waitUntilHealthy(timeoutMs: number, intervalMs = 800): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await isOllamaHealthy()) return true;
await new Promise((r) => setTimeout(r, intervalMs));
}
return false;
}
export async function ensureOllamaAvailable(options?: { timeoutMs?: number }): Promise<boolean> {
const timeoutMs = options?.timeoutMs ?? 20000;
if (await isOllamaHealthy()) return true;
// Windows: tentar iniciar via PowerShell script (WSL-first fallback Windows)
if (os.platform() === "win32") {
try {
const child = spawn(
"powershell",
[
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/ensure-ollama.ps1",
],
{ stdio: "ignore", detached: true }
);
child.unref();
} catch (e) {
// Ignorar, tentaremos apenas aguardar
}
}
// Aguardar saúde
return await waitUntilHealthy(timeoutMs);
}
export async function chatWithStream(
model: string,
messages: Array<{ role: string; content: string }>,
options?: Record<string, unknown>,
system?: string,
tools?: Array<any>
) {
const client = new Ollama({ host: OLLAMA_HOST });
const chatOptions: any = {
model,
messages: system
? [{ role: "system", content: system }, ...messages]
: messages,
stream: true,
options: { ...(options || {}), keep_alive: "5m" },
};
// Adicionar tools se fornecidos
if (tools && tools.length > 0) {
chatOptions.tools = tools;
}
return client.chat(chatOptions);
}
export async function pullModel(model: string) {
const client = new Ollama({ host: OLLAMA_HOST });
return client.pull({ model, stream: true });
}
export async function checkModelExists(modelName: string): Promise<boolean> {
try {
const models = await listModelsViaSdk();
return models.some((model) => model.name === modelName);
} catch (error) {
console.error("Erro ao verificar modelos:", error);
return false;
}
}
export async function ensureModelExists(
modelName: string,
description?: string
): Promise<void> {
const exists = await checkModelExists(modelName);
if (!exists) {
console.log(`📥 Baixando modelo ${modelName}...`);
if (description) {
console.log(`ℹ️ ${description}`);
}
const stream = await pullModel(modelName);
let downloadedBytes = 0;
let totalBytes = 0;
let progress = 0;
// Processar stream para mostrar progresso
for await (const chunk of stream) {
if (chunk.status === "downloading" && chunk.completed && chunk.total) {
downloadedBytes = chunk.completed;
totalBytes = chunk.total;
progress = Math.round((downloadedBytes / totalBytes) * 100);
// Criar barra de progresso visual
const barLength = 20;
const filledLength = Math.round((progress / 100) * barLength);
const bar =
"█".repeat(filledLength) + "░".repeat(barLength - filledLength);
// Mostrar progresso com informações
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(1);
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
process.stdout.write(
`\r📥 ${modelName}: [${bar}] ${progress}% (${downloadedMB}MB/${totalMB}MB)`
);
} else if (chunk.status === "success") {
console.log(`\n✅ Modelo ${modelName} baixado com sucesso!`);
if (description) {
console.log(
`🎯 ${description.replace("será usado", "agora funciona")}`
);
}
break;
}
}
} else {
console.log(`✅ Modelo ${modelName} já está disponível`);
}
}
export async function deleteModel(modelName: string): Promise<void> {
const client = new Ollama({ host: OLLAMA_HOST });
await client.delete({ model: modelName });
}
export async function searchRemoteModels(query?: string): Promise<any[]> {
try {
const catalog = await getCatalogModels();
// Normalizar pesquisa
const q = (query || "").trim().toLowerCase();
const filtered = q
? catalog.filter((m) => {
const name = m.name?.toLowerCase() || "";
const desc = m.description?.toLowerCase() || "";
return name.includes(q) || desc.includes(q);
})
: catalog;
// Modelos instalados
const installedModels = await listModelsViaSdk();
const installedNames = new Set(installedModels.map((m) => m.name));
// Retorno no formato esperado pelo frontend
return filtered.map((m) => ({
name: m.name,
description: m.description,
url: m.url,
tags_count: m.tags_count,
installed: installedNames.has(m.name),
remote: true,
}));
} catch (error) {
console.error("Erro ao buscar modelos remotos:", error);
return [];
}
}