packages/ollama-utils/scripts/generate-automap.ts (193 lines of code) (raw):

/** * Script for generating llm.ts * The source data is taken from llama.cpp */ import type { GGUFParseOutput } from "../../gguf/src/gguf"; import { gguf } from "../../gguf/src/gguf"; import { appendFileSync, writeFileSync, existsSync } from "node:fs"; import path from "node:path"; const DEBUG = process.env.DEBUG; const RE_SPECIAL_TOKEN = /<[|_A-Za-z0-9]+>|\[[A-Z]+\]|<\uFF5C[\u2581A-Za-z]+\uFF5C>/g; const MAX_NUMBER_OF_TAGS_PER_MODEL = 5; const N_WORKERS = 16; const OUTPUT_FILE = path.join(__dirname, "../src/chat-template-automap.ts"); const BLACKLISTED_MODELS = (model: string, tag: string) => { // some models are know to give ServiceUnavailable return model === "library/deepseek-r1" && tag === "7b"; }; interface OutputItem { model: string; gguf: string; ollama: { template: string; tokens: string[]; // eslint-disable-next-line params?: any; }; } interface OllamaManifestLayer { digest: string; mediaType: string; size: number; } interface OllamaManifest { layers: OllamaManifestLayer[]; } const getSpecialTokens = (tmpl: string): string[] => { const matched = tmpl.match(RE_SPECIAL_TOKEN); const tokens = Array.from(matched || []); return Array.from(new Set(tokens)); // deduplicate }; (async () => { if (DEBUG) writeFileSync("ollama_tmp.jsonl", ""); // clear the file const models: string[] = []; const output: OutputItem[] = []; const html = await (await fetch("https://ollama.com/library")).text(); const matched = html.match(/href="\/library\/[^"]+/g); if (!matched) { throw new Error("cannot find any model url"); } for (let i = 0; i < matched.length; i++) { models.push(matched[i].replace('href="/', "")); } console.log({ models }); //////// Get tags //////// let nDoing = 0; let nAll = models.length; const modelsWithTag: string[] = []; const workerGetTags = async () => { while (true) { const model = models.shift(); if (!model) return; nDoing++; console.log(`Getting tags ${nDoing} / ${nAll}`); const html = await (await fetch(`https://ollama.com/${model}`)).text(); const matched = html.match(/href="\/library\/[^"]+/g); if (!matched) { throw new Error("cannot find any tag url"); } for (let i = 0; i < matched.length && i < MAX_NUMBER_OF_TAGS_PER_MODEL; i++) { const midAndTag: string = matched[i].replace('href="/', ""); if (midAndTag.match(/:/) && !midAndTag.match(/\/blobs/)) { modelsWithTag.push(midAndTag); } } } }; await Promise.all( Array(N_WORKERS) .fill(null) .map(() => workerGetTags()) ); console.log({ modelsWithTag }); //////// merging with old file if necessary //////// const seenGGUFTemplate = new Set<string>(); if (existsSync(OUTPUT_FILE)) { const oldOutput = await import(OUTPUT_FILE); oldOutput.OLLAMA_CHAT_TEMPLATE_MAPPING.forEach((item: OutputItem) => { seenGGUFTemplate.add(item.gguf); output.push(item); }); } //////// Get template //////// nDoing = 0; nAll = modelsWithTag.length; const addedModels: string[] = []; const skippedModelsDueToErr: string[] = []; const workerGetTemplate = async () => { while (true) { const modelWithTag = modelsWithTag.shift(); if (!modelWithTag) return; nDoing++; const [model, tag] = modelWithTag.split(":"); console.log(`Fetch template ${nDoing} / ${nAll} | model=${model} tag=${tag}`); const getBlobUrl = (digest: string) => `https://registry.ollama.com/v2/${model}/blobs/${digest}`; const manifest: OllamaManifest = await ( await fetch(`https://registry.ollama.com/v2/${model}/manifests/${tag}`) ).json(); if (!manifest.layers) { console.log(" --> [X] No layers"); continue; } const layerModelUrl = manifest.layers.find((l) => l.mediaType.match(/\.model/)); if (!layerModelUrl) { console.log(" --> [X] No model is found"); continue; } const modelUrl = getBlobUrl(layerModelUrl.digest); let ggufData: GGUFParseOutput; if (BLACKLISTED_MODELS(model, tag)) { console.log(" --> [X] Blacklisted model, skip"); continue; } try { ggufData = await gguf(modelUrl); } catch (e) { console.log(` --> [X] Skipping ${modelWithTag} due to error while calling gguf()`, e); skippedModelsDueToErr.push(modelWithTag); continue; } const { metadata } = ggufData; const ggufTmpl = metadata["tokenizer.chat_template"]; if (ggufTmpl) { try { if (seenGGUFTemplate.has(ggufTmpl)) { console.log(" --> Already seen this GGUF template, skip..."); continue; } seenGGUFTemplate.add(ggufTmpl); console.log(" --> GGUF chat template OK"); const tmplBlob = manifest.layers.find((l) => l.mediaType.match(/\.template/)); if (!tmplBlob) continue; const ollamaTmplUrl = getBlobUrl(tmplBlob.digest); if (!ollamaTmplUrl) { console.log(" --> [X] No ollama template"); continue; } const ollamaTmpl = await (await fetch(ollamaTmplUrl)).text(); console.log(" --> All OK"); const record: OutputItem = { model: modelWithTag, gguf: ggufTmpl, ollama: { template: ollamaTmpl, tokens: getSpecialTokens(ggufTmpl), }, }; // get params const ollamaParamsBlob = manifest.layers.find((l) => l.mediaType.match(/\.params/)); const ollamaParamsUrl = ollamaParamsBlob ? getBlobUrl(ollamaParamsBlob.digest) : null; if (ollamaParamsUrl) { console.log(" --> Got params"); record.ollama.params = await (await fetch(ollamaParamsUrl)).json(); } output.push(record); addedModels.push(modelWithTag); if (DEBUG) appendFileSync("ollama_tmp.jsonl", JSON.stringify(record) + "\n"); } catch (e) { console.log(` --> [X] Skipping ${modelWithTag} due to error`, e); skippedModelsDueToErr.push(modelWithTag); continue; } } else { console.log(" --> [X] No GGUF template"); continue; } //console.log({modelUrl, ggufData}); //break; } }; await Promise.all( Array(N_WORKERS) .fill(null) .map(() => workerGetTemplate()) ); console.log("===================================="); console.log("DONE"); console.log("Added templates for:"); console.log(addedModels.join("\n")); console.log("Skipped these models due to error:"); console.log(skippedModelsDueToErr.join("\n")); output.sort((a, b) => a.model.localeCompare(b.model)); writeFileSync( OUTPUT_FILE, ` // This file is auto generated, please do not modify manually // To update it, run "pnpm run build:automap" import type { OllamaChatTemplateMapEntry } from "./types"; /** * Skipped these models due to error: ${skippedModelsDueToErr.map((m) => ` * - ${m}`).join("\n")} */ export const OLLAMA_CHAT_TEMPLATE_MAPPING: OllamaChatTemplateMapEntry[] = ${JSON.stringify(output, null, "\t")}; `.trim() ); })();