From 784e576ed880c088e74ff1d408f425f8926f9eea Mon Sep 17 00:00:00 2001 From: Fred Bricon Date: Tue, 7 Jan 2025 12:50:10 +0100 Subject: [PATCH] Use https://registry.ollama.ai/v2 API to check model staleness We can get rid of the node-html-parser dependency, which was previously used to manually parse the HTML content from https://ollama.com/library/. Signed-off-by: Fred Bricon --- package-lock.json | 115 +----------------------------------- package.json | 3 +- src/ollama/ollamaLibrary.ts | 56 +++++++++--------- 3 files changed, 31 insertions(+), 143 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6d97a8..eaa6742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@redhat-developer/vscode-redhat-telemetry": "^0.9.1", - "node-html-parser": "^6.1.13", "systeminformation": "^5.23.24" }, "devDependencies": { @@ -1193,11 +1192,6 @@ "node": ">= 6" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1522,32 +1516,6 @@ "node": ">= 8" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -1616,57 +1584,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -1708,17 +1625,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/esbuild": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", @@ -2253,6 +2159,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, "bin": { "he": "bin/he" } @@ -2995,15 +2902,6 @@ } } }, - "node_modules/node-html-parser": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", - "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", - "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3024,17 +2922,6 @@ "node": ">=8" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", diff --git a/package.json b/package.json index 140b15a..14ccedd 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ }, "dependencies": { "@redhat-developer/vscode-redhat-telemetry": "^0.9.1", - "node-html-parser": "^6.1.13", "systeminformation": "^5.23.24" } -} \ No newline at end of file +} diff --git a/src/ollama/ollamaLibrary.ts b/src/ollama/ollamaLibrary.ts index 124dc8a..2822e26 100644 --- a/src/ollama/ollamaLibrary.ts +++ b/src/ollama/ollamaLibrary.ts @@ -1,46 +1,42 @@ -import { parse } from 'node-html-parser'; +import crypto from 'crypto'; import { ModelInfo } from '../commons/modelInfo'; +import { formatSize } from '../commons/textUtils'; const cache = new Map();//TODO limit caching lifespan -const INFO_DELIMITER = ' ยท '; - -// This is fugly, extremely brittle, but we have no other choice because The ollama library doesn't seem to expose an API we can query. export async function getRemoteModelInfo(modelId: string): Promise { // Check if the result is already cached if (cache.has(modelId)) { return cache.get(modelId); } const start = Date.now(); - - const url = `https://ollama.com/library/${modelId}`; + const [modelName, tag] = modelId.split(":"); + const url = `https://registry.ollama.ai/v2/library/${modelName}/manifests/${tag}`; try { const response = await fetch(url, { signal: AbortSignal.timeout(3000) }); if (!response.ok) { throw new Error(`Failed to fetch the model page: ${response.statusText}`); } - const html = await response.text(); - const root = parse(html); - const fileExplorer = root.querySelector('#file-explorer'); - const itemsCenter = fileExplorer?.querySelector('.items-center'); - const lastParagraphElement = itemsCenter?.querySelectorAll('p')?.pop(); - - if (lastParagraphElement) { - const lastParagraph = lastParagraphElement.text.trim(); - if (lastParagraph.includes(INFO_DELIMITER)) { - const [digest, size] = lastParagraph.split(INFO_DELIMITER).map(item => item.trim()); - const data: ModelInfo = { - id: modelId, - size, - digest - }; - // Cache the successful result - cache.set(modelId, data); - console.log('Model info:', data); - return data; - } - } + + // First, read the response body as an ArrayBuffer to compute the digest + const buffer = await response.arrayBuffer(); + const digest = getDigest(buffer); + + // Then, decode the ArrayBuffer into a string and parse it as JSON + const text = new TextDecoder().decode(buffer); + const manifest = JSON.parse(text) as { layers: { size: number }[] }; + const modelSize = manifest.layers.reduce((sum, layer) => sum + layer.size, 0); + + const data: ModelInfo = { + id: modelId, + size: formatSize(modelSize), + digest + }; + // Cache the successful result + cache.set(modelId, data); + console.log('Model info:', data); + return data; } catch (error) { console.error(`Error fetching or parsing model info: ${error}`); } finally { @@ -51,4 +47,10 @@ export async function getRemoteModelInfo(modelId: string): Promise