import { tryParseJson } from "@oh-my-pi/pi-utils";
import { formatBytes } from "../../tools/render-utils";
import type { RenderResult, SpecialHandler } from "./types";
import { buildResult, decodeHtmlEntities, loadPage } from "./types";
interface OllamaTagDetails {
parent_model?: string;
format?: string;
family?: string;
families?: string[] | null;
parameter_size?: string;
quantization_level?: string;
}
interface OllamaTagModel {
name?: string;
model?: string;
modified_at?: string;
size?: number;
digest?: string;
details?: OllamaTagDetails;
}
interface OllamaTagsResponse {
models?: OllamaTagModel[];
}
const VALID_HOSTNAMES = new Set(["ollama.com", "www.ollama.com"]);
const RESERVED_ROOTS = new Set([
"models",
"blog",
"docs",
"download",
"cloud",
"signin",
"signout",
"search",
"api",
"terms",
"privacy",
"license",
"settings",
]);
function extractMetaDescription(html: string): string | null {
const patterns = [
/]+name=["']description["'][^>]*content=["']([^"']+)["']/i,
/]+property=["']og:description["'][^>]*content=["']([^"']+)["']/i,
/]+property=["']twitter:description["'][^>]*content=["']([^"']+)["']/i,
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match?.[1]) {
return decodeHtmlEntities(match[1].trim());
}
}
return null;
}
function extractParameterSizes(html: string): string[] {
const sizes = new Set();
const pattern = /x-test-size[^>]*>([^<]+)<\/span>/gi;
let match = pattern.exec(html);
while (match) {
const raw = match[1]?.trim();
if (raw) {
sizes.add(raw.toUpperCase());
}
match = pattern.exec(html);
}
return Array.from(sizes);
}
function extractTagsFromHtml(html: string, baseRef: string): string[] {
const tags = new Set();
const pattern = /href=["']\/library\/([^"']+)["']/gi;
let match = pattern.exec(html);
while (match) {
const raw = match[1]?.trim();
if (raw) {
const decoded = decodeHtmlEntities(raw);
if (decoded === baseRef || decoded.startsWith(`${baseRef}:`)) {
tags.add(decoded);
}
}
match = pattern.exec(html);
}
return Array.from(tags);
}
function buildModelPath(parts: string[]): string {
return parts.map(part => encodeURIComponent(part)).join("/");
}
function parseOllamaUrl(url: string): { modelRef: string; baseRef: string; pageUrl: string } | null {
try {
const parsed = new URL(url);
if (!VALID_HOSTNAMES.has(parsed.hostname)) return null;
const parts = parsed.pathname.split("/").filter(Boolean);
if (parts.length === 0) return null;
if (parts[0] === "library" && parts.length >= 2) {
const modelRef = decodeURIComponent(parts[1]);
const baseRef = modelRef.split(":")[0] ?? modelRef;
const pageUrl = `${parsed.origin}/${buildModelPath(["library", baseRef])}`;
return { modelRef, baseRef, pageUrl };
}
if (parts.length >= 2 && !RESERVED_ROOTS.has(parts[0])) {
const namespace = decodeURIComponent(parts[0]);
const model = decodeURIComponent(parts[1]);
const modelBase = model.split(":")[0] ?? model;
const modelRef = `${namespace}/${model}`;
const baseRef = `${namespace}/${modelBase}`;
const pageUrl = `${parsed.origin}/${buildModelPath([namespace, modelBase])}`;
return { modelRef, baseRef, pageUrl };
}
} catch {}
return null;
}
function sortTags(tags: string[]): string[] {
return tags.sort((a, b) => {
const aLatest = a.endsWith(":latest");
const bLatest = b.endsWith(":latest");
if (aLatest && !bLatest) return -1;
if (!aLatest && bLatest) return 1;
return a.localeCompare(b);
});
}
function formatTagList(tags: string[], maxItems: number): string {
const limited = tags.slice(0, maxItems);
const formatted = limited.map(tag => `\`${tag}\``).join(", ");
if (tags.length > maxItems) {
return `${formatted} (and ${tags.length - maxItems} more)`;
}
return formatted;
}
function collectParameterSizes(models: OllamaTagModel[], htmlSizes: string[]): string[] {
const sizes = new Set();
for (const model of models) {
const param = model.details?.parameter_size?.trim();
if (param) sizes.add(param.toUpperCase());
}
for (const size of htmlSizes) {
sizes.add(size);
}
return Array.from(sizes);
}
export const handleOllama: SpecialHandler = async (
url: string,
timeout: number,
signal?: AbortSignal,
): Promise => {
try {
const parsed = parseOllamaUrl(url);
if (!parsed) return null;
const { modelRef, baseRef, pageUrl } = parsed;
const fetchedAt = new Date().toISOString();
const tagsUrl = "https://ollama.com/api/tags";
const [tagsResult, pageResult] = await Promise.all([
loadPage(tagsUrl, { timeout, signal, headers: { Accept: "application/json" } }),
loadPage(pageUrl, { timeout, signal }),
]);
const tagsData = tagsResult.ok ? tryParseJson(tagsResult.content) : null;
const html = pageResult.ok ? pageResult.content : "";
const description = html ? extractMetaDescription(html) : null;
const htmlParameterSizes = html ? extractParameterSizes(html) : [];
const htmlTags = html ? extractTagsFromHtml(html, baseRef) : [];
const baseLower = baseRef.toLowerCase();
const models = tagsData?.models ?? [];
const matchingModels = models.filter(model => {
const name = (model.model ?? model.name ?? "").toLowerCase();
return name === baseLower || name.startsWith(`${baseLower}:`);
});
const tagRef = modelRef.includes(":") ? modelRef : null;
const selectedTag = tagRef ? matchingModels.find(model => (model.model ?? model.name ?? "") === tagRef) : null;
const availableTagsRaw = matchingModels
.map(model => model.model ?? model.name ?? "")
.filter(tag => tag.length > 0);
const availableTags = sortTags(Array.from(new Set(availableTagsRaw)));
const fallbackTags = sortTags(Array.from(new Set(htmlTags)));
const tagsToUse = availableTags.length > 0 ? availableTags : fallbackTags;
const parameterSizes = collectParameterSizes(selectedTag ? [selectedTag] : matchingModels, htmlParameterSizes);
const sizes = matchingModels.map(model => model.size).filter((size): size is number => typeof size === "number");
let sizeLine: string | null = null;
if (selectedTag?.size) {
sizeLine = formatBytes(selectedTag.size);
} else if (sizes.length > 0) {
const minSize = Math.min(...sizes);
const maxSize = Math.max(...sizes);
sizeLine = minSize === maxSize ? formatBytes(minSize) : `${formatBytes(minSize)} - ${formatBytes(maxSize)}`;
}
let md = `# ${baseRef}\n\n`;
if (description) md += `${description}\n\n`;
md += `**Model:** ${baseRef}\n`;
if (tagRef) md += `**Tag:** ${tagRef}\n`;
if (parameterSizes.length > 0) md += `**Parameters:** ${parameterSizes.join(", ")}\n`;
if (sizeLine) {
const label = sizeLine.includes(" - ") ? "Size Range" : "Size";
md += `**${label}:** ${sizeLine}\n`;
}
if (tagsToUse.length > 0) {
md += `**Available Tags:** ${formatTagList(tagsToUse, 40)}\n`;
}
return buildResult(md, {
url,
finalUrl: pageResult.ok ? pageResult.finalUrl : url,
method: "ollama",
fetchedAt,
notes: ["Fetched via Ollama API"],
});
} catch {}
return null;
};