Files
chatgpt-mcp/src/providers/ollama.js
T
2026-06-15 13:35:31 +01:00

135 lines
4.0 KiB
JavaScript

// Ollama chat provider adapter.
// Calls Ollama's /api/chat endpoint (OpenAI-compatible format) using native fetch().
// No external SDK dependency — zero new dependencies.
const DEFAULT_BASE_URL = "http://localhost:11434";
const DEFAULT_MODEL = "qwen3:latest";
const DEFAULT_TEMPERATURE = 0.2;
const DEFAULT_TIMEOUT_SECONDS = 60;
/**
* Strip trailing slashes from base URL so path concatenation is always correct.
*/
function normalizeBaseUrl(raw) {
return (raw || "").replace(/\/+$/, "");
}
/**
* Derive a safe error category string from a caught error.
* Does not create new Error classes — just returns a categorising label.
*/
function getErrorKind(error) {
if (error?.name === "AbortError" || /timed?out/i.test(String(error.message ?? ""))) {
return "OllamaTimeoutError";
}
const status = error?.status ?? error?.response?.status;
if (typeof status === "number") {
if (status === 404) return "OllamaModelNotFoundError";
if (status === 422) return "OllamaValidationError";
if (status === 501) return "OllamaApiNotAvailableError";
}
return "OllamaRequestError";
}
/**
* Extract a safe error message, never leaking secrets or internals.
*/
function safeErrorMessage(error) {
const msg = String(error.message ?? "");
if (!msg) return "unknown Ollama error";
return msg;
}
/**
* Chat provider backed by Ollama's /api/chat endpoint.
* Implements { send(reviewRequest, config) => Promise<{ content: string }> }.
*/
export const ollamaProvider = {
/**
* @param {{ prompt?: string }} reviewRequest - ProviderRequest (minimal — only prompt used).
* @param {object} config - Full config from loadConfig().
* @returns {Promise<{ content: string }>} Advisory response text.
*/
async send(reviewRequest, config) {
const baseUrl = normalizeBaseUrl(config?.ollamaBaseUrl || DEFAULT_BASE_URL);
const model = config?.ollamaModel || DEFAULT_MODEL;
const temperature =
config?.ollamaTemperature != null
? Number(config.ollamaTemperature)
: DEFAULT_TEMPERATURE;
const timeoutSeconds =
config?.ollamaTimeout != null
? Number(config.ollamaTimeout)
: DEFAULT_TIMEOUT_SECONDS;
const prompt = reviewRequest?.prompt || "";
// Build the request body in OpenAI-compatible chat format.
const requestBody = JSON.stringify({
model,
messages: [{ role: "system", content: prompt }],
stream: false,
options: {
temperature: isNaN(temperature) ? DEFAULT_TEMPERATURE : temperature,
},
});
// Build AbortController for timeout.
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
let response;
try {
response = await fetch(`${baseUrl}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: requestBody,
signal: controller.signal,
});
} catch (error) {
clearTimeout(timeoutId);
const kind = getErrorKind(error);
throw new Error(
`Ollama API error (${kind}): ${safeErrorMessage(error)}`
);
} finally {
clearTimeout(timeoutId);
}
// Handle non-2xx status codes.
if (!response.ok) {
let bodyText = "";
try {
bodyText = await response.text();
} catch (_) {
/* ignore unparseable error bodies */
}
const errObj = new Error(`HTTP ${response.status}`);
errObj.status = response.status;
const kind = getErrorKind(errObj);
const detail = bodyText ? ` — ${bodyText.slice(0, 200)}` : "";
throw new Error(
`Ollama API error (${kind}): HTTP ${response.status}${detail}`
);
}
// Parse JSON response.
let data;
try {
data = await response.json();
} catch (_) {
throw new Error(
"Ollama API error (OllamaRequestError): invalid JSON response."
);
}
// Ollama /api/chat returns: { model, message: { role, content }, done }
const content = data?.message?.content ?? "";
return { content };
},
};