Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ This is a collection of JS libraries to interact with the Hugging Face API, with
- [@huggingface/gguf](packages/gguf/README.md): A GGUF parser that works on remotely hosted files.
- [@huggingface/tasks](packages/tasks/README.md): The definition files and source-of-truth for the Hub's main primitives like pipeline tasks, model libraries, etc.
- [@huggingface/space-header](packages/space-header/README.md): Use the Space `mini_header` outside Hugging Face
- [@huggingface/ollama-utils](packages/ollama-utils/README.md): Various utilities for maintaining Ollama compatibility with models on Hugging Face hub.


We use modern features to avoid polyfills and dependencies, so the libraries will only work on modern browsers / Node.js >= 18 / Bun / Deno.
Expand Down
1 change: 1 addition & 0 deletions packages/ollama-utils/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
5 changes: 5 additions & 0 deletions packages/ollama-utils/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pnpm-lock.yaml
# In order to avoid code samples to have tabs, they don't display well on npm
README.md
dist
src/automap.ts
11 changes: 11 additions & 0 deletions packages/ollama-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `@huggingface/ollama-utils`

Various utilities for maintaining Ollama compatibility with models on Hugging Face hub.

Documentation: https://huggingface.co/docs/hub/en/ollama

## Chat template converter

```ts
const
```
58 changes: 58 additions & 0 deletions packages/ollama-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@huggingface/ollama-utils",
"packageManager": "[email protected]",
"version": "0.0.1",
"description": "Various utilities for maintaining Ollama compatibility with models on Hugging Face hub",
"repository": "https://github.com/huggingface/huggingface.js.git",
"publishConfig": {
"access": "public"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
}
},
"browser": {
"./src/utils/FileBlob.ts": false,
"./dist/index.js": "./dist/browser/index.js",
"./dist/index.mjs": "./dist/browser/index.mjs"
},
"engines": {
"node": ">=20"
},
"source": "index.ts",
"scripts": {
"lint": "eslint --quiet --fix --ext .cjs,.ts .",
"lint:check": "eslint --ext .cjs,.ts .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prepublishOnly": "pnpm run build",
"build": "tsup src/index.ts --format cjs,esm --clean && tsc --emitDeclarationOnly --declaration",
"build:automap": "tsx scripts/generate-automap.ts",
"test": "vitest run",
"check": "tsc"
},
"files": [
"dist",
"src",
"tsconfig.json"
],
"keywords": [
"huggingface",
"hub",
"gguf"
],
"author": "Hugging Face",
"license": "MIT",
"dependencies": {
"@huggingface/jinja": "workspace:^"
},
"devDependencies": {
"@types/node": "^20.12.8"
}
}
27 changes: 27 additions & 0 deletions packages/ollama-utils/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

161 changes: 161 additions & 0 deletions packages/ollama-utils/scripts/generate-automap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* Script for generating llm.ts
* The source data is taken from llama.cpp
*/

import { gguf } from "../../gguf/src/gguf";
import { appendFileSync, writeFileSync } from "node:fs";

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;

interface OutputItem {
model: string;
gguf: string;
ollama: {
template: string;
tokens: string[];
params?: any;
};
}

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 () => {
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 });

//////// Get template ////////

nDoing = 0;
nAll = modelsWithTag.length;
let seenTemplate = new Set();
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) => `https://registry.ollama.com/v2/${model}/blobs/${digest}`;
const manifest = await (await fetch(`https://registry.ollama.com/v2/${model}/manifests/${tag}`)).json();
if (!manifest.layers) {
console.log(" --> [X] No layers");
continue;
}
const modelUrl = getBlobUrl(manifest.layers.find((l) => l.mediaType.match(/\.model/)).digest);
const ggufData = await gguf(modelUrl);
const { metadata } = ggufData;
const ggufTmpl = metadata["tokenizer.chat_template"];
if (ggufTmpl) {
if (seenTemplate.has(ggufTmpl)) {
console.log(" --> Already seen this GGUF template, skip...");
continue;
}
seenTemplate.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);
appendFileSync("ollama_tmp.jsonl", JSON.stringify(record) + "\n");
} 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("DONE");
output.sort((a, b) => a.model.localeCompare(b.model));

writeFileSync(
"./src/chat-template-automap.ts",
`
// This file is auto generated, please do not modify manually
// To update it, run "pnpm run build:automap"

import { OllamaChatTemplateMapEntry } from "./types";

export const OLLAMA_CHAT_TEMPLATE_MAPPING: OllamaChatTemplateMapEntry[] = ${JSON.stringify(output, null, "\t")};
`.trim()
);
})();
Loading
Loading