From bd9f058f265db70f90ceff959e2c788c1fbf7b20 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Mon, 20 Jul 2026 12:43:25 -0400 Subject: [PATCH] Use official ollama npm client for all LLM calls Replaces hand-rolled fetch to /api/chat with the ollama@0.6.3 package. The package supplies proper Accept/Content-Type/User-Agent headers and auto-handles the Ollama protocol, fixing the CDN 403s Node's bare fetch gets against ollama.com during filing. Both organize and search LLM ranking now route through controller/ollama.js. Signed-off-by: William Mantly --- controller/ollama.js | 50 +++++++++++++++++++------------------------- controller/search.js | 36 ++++--------------------------- package-lock.json | 16 ++++++++++++++ package.json | 1 + 4 files changed, 43 insertions(+), 60 deletions(-) diff --git a/controller/ollama.js b/controller/ollama.js index 7deda45..10514b8 100644 --- a/controller/ollama.js +++ b/controller/ollama.js @@ -1,7 +1,18 @@ 'use strict'; +const { Ollama } = require('ollama'); const conf = require('>/conf'); +// Shared Ollama client. The npm package adds its own Accept / Content-Type / User-Agent +// headers and handles the Ollama chat protocol, which avoids the CDN-level 403s Node's +// bare fetch gets against ollama.com. +const client = new Ollama({ + host: conf.ollama.url, + headers: { + 'Authorization': `Bearer ${conf.ollama.apiKey}`, + }, +}); + // Pull the first JSON object out of a model response( tolerates ```json fences / prose). function extractJSON(content){ let text = String(content).replace(/```json/gi, '').replace(/```/g, '').trim(); @@ -19,35 +30,18 @@ function extractJSON(content){ } } -// One-shot JSON chat against the Ollama cloud model. +// One-shot JSON chat against the configured Ollama model. async function chatJSON(system, user){ - let res = await fetch(`${conf.ollama.url}/api/chat`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${conf.ollama.apiKey}`, - 'User-Agent': 'tpbproxy/1.0 (organize; +https://github.com/wmantly/tpbproxy)', - }, - body: JSON.stringify({ - model: conf.ollama.model, - stream: false, - format: 'json', - messages: [ - { role: 'system', content: system }, - { role: 'user', content: user }, - ], - }), + let res = await client.chat({ + model: conf.ollama.model, + stream: false, + format: 'json', + messages: [ + { role: 'system', content: system }, + { role: 'user', content: user }, + ], }); - if(!res.ok){ - let body = await res.text().catch(() => ''); - console.error(`Ollama chat failed (${res.status}):`, body.slice(0, 500)); - let error = new Error('OllamaError'); - error.message = `Ollama chat failed( ${res.status})`; - error.status = 502; - throw error; - } - let data = await res.json(); - return extractJSON(data.message && data.message.content); + return extractJSON(res.message && res.message.content); } -module.exports = { chatJSON, extractJSON }; +module.exports = { client, chatJSON, extractJSON }; diff --git a/controller/search.js b/controller/search.js index b07fcb1..7bc981e 100644 --- a/controller/search.js +++ b/controller/search.js @@ -22,17 +22,6 @@ function humanSize(bytes){ return `${bytes.toFixed(bytes < 10 && i > 0 ? 1 : 0)} ${units[i]}`; } -async function fetchJSON(url, options){ - let res = await fetch(url, options); - if(!res.ok){ - let error = new Error(`UpstreamError`); - error.message = `Request to ${url.split('?')[0]} failed( ${res.status})`; - error.status = 502; - throw error; - } - return await res.json(); -} - // Turn a raw TPB category id into 'movie' | 'tv' | null. function videoKind(category){ category = parseInt(category, 10); @@ -179,27 +168,10 @@ async function rankReleases(candidates, meta){ imdb: t.imdb || null, })); - let body = { - model: conf.ollama.model, - stream: false, - format: 'json', - messages: [ - { role: 'system', content: buildSystemPrompt(meta.title, meta.year, meta.today) }, - { role: 'user', content: JSON.stringify(compact) }, - ], - }; - - let data = await fetchJSON(`${conf.ollama.url}/api/chat`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${conf.ollama.apiKey}`, - 'User-Agent': 'tpbproxy/1.0 (search; +https://github.com/wmantly/tpbproxy)', - }, - body: JSON.stringify(body), - }); - - let parsed = extractJSON(data.message && data.message.content); + let parsed = await ollama.chatJSON( + buildSystemPrompt(meta.title, meta.year, meta.today), + JSON.stringify(compact) + ); let options = Array.isArray(parsed.options) ? parsed.options : []; if(!options.length) throw new Error('Model returned no options'); return options; diff --git a/package-lock.json b/package-lock.json index 8467121..957e159 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "lru-native2": "^1.2.6", "moment": "^2.30.1", "mustache": "^4.2.0", + "ollama": "^0.6.3", "p2psub": "^0.1.9", "sequelize": "^6.35.2", "sequelize-cli": "^6.6.2", @@ -2933,6 +2934,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ollama": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", + "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", + "license": "MIT", + "dependencies": { + "whatwg-fetch": "^3.6.20" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4411,6 +4421,12 @@ "node": ">=12" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", diff --git a/package.json b/package.json index 744e1db..6738d68 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "lru-native2": "^1.2.6", "moment": "^2.30.1", "mustache": "^4.2.0", + "ollama": "^0.6.3", "p2psub": "^0.1.9", "sequelize": "^6.35.2", "sequelize-cli": "^6.6.2",