feat: add ollama provider
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
// 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 };
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user