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
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-29 19:42:24 -03:00
1 parent 1545020afe
commit a8bb7cc22d
33 files changed
+1298 -235

No files matched your search

+52 -4
View File
@@ -1,5 +1,7 @@
import { NextRequest } from "next/server";
import { pullModel, ensureOllamaAvailable } from "@/lib/ollama";
import { getCatalogModels } from "@/lib/catalog";
import { addSavedRemoteModel } from "@/lib/user-remote-models";
export const dynamic = "force-dynamic";
@@ -26,6 +28,11 @@ export async function POST(req: NextRequest) {
return;
}
// Buscar metadados do catálogo para melhorar mensagens de erro
const baseName = String(model).split(":")[0];
const catalog = await getCatalogModels();
const catalogEntry = catalog.find((m) => m.name === baseName);
const response = await pullModel(model);
for await (const chunk of response) {
@@ -34,18 +41,59 @@ export async function POST(req: NextRequest) {
);
}
// Persistir referência do modelo baixado para reutilização futura
try {
await addSavedRemoteModel({ name: String(model), installed: true });
} catch {}
controller.enqueue(
new TextEncoder().encode(
JSON.stringify({ status: "success" }) + "\n",
),
);
controller.close();
} catch (error) {
} catch (error: any) {
console.error("Pull streaming error:", error);
const message = typeof error?.message === "string" ? error.message : "Failed to pull model";
const baseName = String(model).split(":")[0];
let hint: string | undefined;
// Recarregar entrada de catálogo para enriquecer erro
let catalogEntry: { url?: string; tags_count?: number } | undefined;
try {
const catalog = await getCatalogModels();
catalogEntry = catalog.find((m) => m.name === baseName);
} catch {}
// Heurísticas: manifest não encontrado → provavelmente falta tag válida
if (/manifest/i.test(message) || /does not exist/i.test(message)) {
hint = "Modelo/tag não encontrado no registro. Selecione uma tag válida ou verifique a página do modelo.";
}
const payload: Record<string, unknown> = {
error: "Failed to pull model",
detail: message,
model,
name: baseName,
};
// Anexar URL do catálogo quando disponível
if (catalogEntry?.url) {
payload.url = catalogEntry.url;
}
// Se usuário não especificou tag (sem ":") e sabemos que há múltiplas tags
if (!String(model).includes(":") && typeof catalogEntry?.tags_count === "number") {
payload.tags_count = catalogEntry.tags_count;
if (!hint && catalogEntry.tags_count > 0) {
hint = "Este modelo pode não ter a tag 'latest'. Escolha uma tag específica.";
}
}
if (hint) payload.hint = hint;
controller.enqueue(
new TextEncoder().encode(
JSON.stringify({ error: "Failed to pull model" }) + "\n",
),
new TextEncoder().encode(JSON.stringify(payload) + "\n"),
);
controller.close();
}
+16 -6
View File
@@ -1,17 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import { searchRemoteModels } from "@/lib/ollama";
import { getSavedRemoteModels } from "@/lib/user-remote-models";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const query = searchParams.get("q") || undefined;
const remoteModels = await searchRemoteModels(query);
const saved = await getSavedRemoteModels();
const q = (query || "").trim().toLowerCase();
const filtered = q
? saved.filter((m) => m.name.toLowerCase().includes(q))
: saved;
return NextResponse.json({
success: true,
models: remoteModels,
});
const models = filtered.map((m) => ({
name: m.name,
description: m.description,
url: m.url,
tags_count: m.tags_count,
installed: m.installed === true,
remote: true,
}));
return NextResponse.json({ success: true, models });
} catch (error) {
console.error("Error searching remote models:", error);
return NextResponse.json(
+74
View File
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { getCatalogModels } from "@/lib/catalog";
// Cache simples em memória para evitar múltiplos scrapes por nome
const TAGS_CACHE = new Map<string, { tags: string[]; expiresAt: number }>();
const CACHE_TTL_MS = 60 * 60 * 1000; // 1h
async function fetchTagsFromPage(url: string): Promise<string[]> {
try {
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) return [];
const html = await res.text();
// Heurística best-effort: procurar padrões de tag na página
// Exemplos possíveis: ":7b", ":8b", ":q4_K_S", etc.
// Evitar listas enormes ou falsas-positivas: limitar tamanho e caracteres
const tagRegex = /:\s*([A-Za-z0-9._-]{1,32})/g;
const found = new Set<string>();
let match: RegExpExecArray | null;
while ((match = tagRegex.exec(html)) !== null) {
const tag = match[1];
if (tag && !/^(http|https)$/i.test(tag)) {
found.add(tag);
}
}
// Retornar até 50 tags para não sobrecarregar UI
return Array.from(found).slice(0, 50);
} catch {
return [];
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const name = (searchParams.get("name") || "").trim();
if (!name) {
return NextResponse.json(
{ error: "Missing name" },
{ status: 400 },
);
}
// Cache
const cached = TAGS_CACHE.get(name);
const now = Date.now();
if (cached && cached.expiresAt > now) {
return NextResponse.json({ success: true, name, tags: cached.tags });
}
const catalog = await getCatalogModels();
const entry = catalog.find((m) => m.name === name);
const url = entry?.url;
// Sem URL conhecida → não conseguimos buscar tags no registro
if (!url) {
TAGS_CACHE.set(name, { tags: [], expiresAt: now + CACHE_TTL_MS });
return NextResponse.json({ success: true, name, tags: [] });
}
const tags = await fetchTagsFromPage(url);
TAGS_CACHE.set(name, { tags, expiresAt: now + CACHE_TTL_MS });
return NextResponse.json({ success: true, name, url, tags });
} catch (error) {
console.error("Error fetching model tags:", error);
return NextResponse.json(
{ error: "Failed to fetch tags" },
{ status: 500 },
);
}
}