feat: add ollama provider
This commit is contained in:
@@ -41,5 +41,9 @@ export function loadConfig() {
|
|||||||
maxLogChars: parseNum('CHATGPT_MCP_MAX_LOG_CHARS', process.env.CHATGPT_MCP_MAX_LOG_CHARS, 10000),
|
maxLogChars: parseNum('CHATGPT_MCP_MAX_LOG_CHARS', process.env.CHATGPT_MCP_MAX_LOG_CHARS, 10000),
|
||||||
redactSecrets: parseBool('CHATGPT_MCP_REDACT_SECRETS', process.env.CHATGPT_MCP_REDACT_SECRETS, true),
|
redactSecrets: parseBool('CHATGPT_MCP_REDACT_SECRETS', process.env.CHATGPT_MCP_REDACT_SECRETS, true),
|
||||||
chatgptMcpProvider: process.env.CHATGPT_MCP_PROVIDER || 'openai',
|
chatgptMcpProvider: process.env.CHATGPT_MCP_PROVIDER || 'openai',
|
||||||
|
ollamaBaseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||||
|
ollamaModel: process.env.OLLAMA_MODEL || 'qwen3:latest',
|
||||||
|
ollamaTemperature: parseNum('OLLAMA_TEMPERATURE', process.env.OLLAMA_TEMPERATURE, 0.2),
|
||||||
|
ollamaTimeout: parseNum('OLLAMA_TIMEOUT', process.env.OLLAMA_TIMEOUT, 60),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
import { openaiProvider } from "./openai.js";
|
import { openaiProvider } from "./openai.js";
|
||||||
import { manualExportProvider } from "./manual-export.js";
|
import { manualExportProvider } from "./manual-export.js";
|
||||||
|
import { ollamaProvider } from "./ollama.js";
|
||||||
|
|
||||||
const SUPPORTED_PROVIDERS = new Set(["openai", "manual"]);
|
const SUPPORTED_PROVIDERS = new Set(["openai", "manual", "ollama"]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a chat provider from configuration.
|
* Create a chat provider from configuration.
|
||||||
@@ -28,6 +29,8 @@ export function createChatProvider(config) {
|
|||||||
return openaiProvider;
|
return openaiProvider;
|
||||||
case "manual":
|
case "manual":
|
||||||
return manualExportProvider;
|
return manualExportProvider;
|
||||||
|
case "ollama":
|
||||||
|
return ollamaProvider;
|
||||||
default:
|
default:
|
||||||
// Should not reach here because of the set check above.
|
// Should not reach here because of the set check above.
|
||||||
throw new Error(`Unknown provider "${providerName}".`);
|
throw new Error(`Unknown provider "${providerName}".`);
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
},
|
||||||
|
};
|
||||||
+105
-2
@@ -5,7 +5,7 @@ const originalEnv = { ...process.env };
|
|||||||
|
|
||||||
function setEnv(partial) {
|
function setEnv(partial) {
|
||||||
for (const key of Object.keys(process.env)) {
|
for (const key of Object.keys(process.env)) {
|
||||||
if (key.startsWith('OPENAI_') || key.startsWith('CHATGPT_MCP_')) {
|
if (key.startsWith('OPENAI_') || key.startsWith('CHATGPT_MCP_') || key.startsWith('OLLAMA_')) {
|
||||||
delete process.env[key];
|
delete process.env[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
for (const key of Object.keys(process.env)) {
|
for (const key of Object.keys(process.env)) {
|
||||||
if (key.startsWith('OPENAI_') || key.startsWith('CHATGPT_MCP_')) {
|
if (key.startsWith('OPENAI_') || key.startsWith('CHATGPT_MCP_') || key.startsWith('OLLAMA_')) {
|
||||||
delete process.env[key];
|
delete process.env[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,10 @@ describe('loadConfig', () => {
|
|||||||
expect(cfg.maxFiles).toBe(5);
|
expect(cfg.maxFiles).toBe(5);
|
||||||
expect(cfg.maxLogChars).toBe(10000);
|
expect(cfg.maxLogChars).toBe(10000);
|
||||||
expect(cfg.redactSecrets).toBe(true);
|
expect(cfg.redactSecrets).toBe(true);
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434');
|
||||||
|
expect(cfg.ollamaModel).toBe('qwen3:latest');
|
||||||
|
expect(cfg.ollamaTemperature).toBe(0.2);
|
||||||
|
expect(cfg.ollamaTimeout).toBe(60);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom string values', () => {
|
it('applies custom string values', () => {
|
||||||
@@ -162,3 +166,102 @@ describe('loadConfig chatgptMcpProvider', () => {
|
|||||||
expect(cfg.chatgptMcpProvider).toBe('ollama');
|
expect(cfg.chatgptMcpProvider).toBe('ollama');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Ollama env vars ---
|
||||||
|
|
||||||
|
describe('loadConfig ollama env vars', () => {
|
||||||
|
it('returns default ollama values when no OLLAMA_ vars set', () => {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434');
|
||||||
|
expect(cfg.ollamaModel).toBe('qwen3:latest');
|
||||||
|
expect(cfg.ollamaTemperature).toBe(0.2);
|
||||||
|
expect(cfg.ollamaTimeout).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies custom OLLAMA_BASE_URL', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11435' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11435');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns raw OLLAMA_BASE_URL — trailing slash normalization is in the provider', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434/' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies custom OLLAMA_MODEL', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_MODEL: 'llama3' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaModel).toBe('llama3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies custom OLLAMA_TEMPERATURE', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TEMPERATURE: '0.7' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaTemperature).toBe(0.7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies custom OLLAMA_TIMEOUT', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TIMEOUT: '120' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaTimeout).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on invalid OLLAMA_TEMPERATURE', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TEMPERATURE: 'abc' });
|
||||||
|
expect(() => loadConfig()).toThrow(
|
||||||
|
'Configuration error: OLLAMA_TEMPERATURE must be a positive number, got "abc".'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on negative OLLAMA_TIMEOUT', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TIMEOUT: '-10' });
|
||||||
|
expect(() => loadConfig()).toThrow(
|
||||||
|
'Configuration error: OLLAMA_TIMEOUT must be a positive number, got "-10".'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with ollama provider and all ollama env vars', () => {
|
||||||
|
setEnv({
|
||||||
|
OPENAI_API_KEY: 'sk-test',
|
||||||
|
CHATGPT_MCP_PROVIDER: 'ollama',
|
||||||
|
OLLAMA_BASE_URL: 'http://ollama-server:8080',
|
||||||
|
OLLAMA_MODEL: 'mistral',
|
||||||
|
OLLAMA_TEMPERATURE: '0.5',
|
||||||
|
OLLAMA_TIMEOUT: '90',
|
||||||
|
});
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.chatgptMcpProvider).toBe('ollama');
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://ollama-server:8080');
|
||||||
|
expect(cfg.ollamaModel).toBe('mistral');
|
||||||
|
expect(cfg.ollamaTemperature).toBe(0.5);
|
||||||
|
expect(cfg.ollamaTimeout).toBe(90);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadConfig ollama env vars with trailing slash', () => {
|
||||||
|
it('returns raw base URL — normalization happens in the provider', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434//' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434//');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns raw base URL with multiple slashes', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434///' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434///');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles base URL without trailing slash', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty base URL as default', () => {
|
||||||
|
setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: '' });
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -54,10 +54,21 @@ describe("supported providers", () => {
|
|||||||
// --- Unsupported providers ---
|
// --- Unsupported providers ---
|
||||||
|
|
||||||
describe("unsupported providers", () => {
|
describe("unsupported providers", () => {
|
||||||
it("throws on ollama provider", () => {
|
it("supports ollama provider", () => {
|
||||||
expect(() => createChatProvider({ chatgptMcpProvider: "ollama" })).toThrow(
|
const provider = createChatProvider({ chatgptMcpProvider: "ollama" });
|
||||||
/Unsupported chat provider "ollama"/,
|
expect(provider).toBeDefined();
|
||||||
);
|
expect(typeof provider.send).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the same singleton instance for repeated calls with 'ollama'", () => {
|
||||||
|
const p1 = createChatProvider({ chatgptMcpProvider: "ollama" });
|
||||||
|
const p2 = createChatProvider({ chatgptMcpProvider: "ollama" });
|
||||||
|
expect(p1).toBe(p2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is different from openai provider", () => {
|
||||||
|
const ollamaP = createChatProvider({ chatgptMcpProvider: "ollama" });
|
||||||
|
expect(ollamaP).not.toBe(openaiProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws on unknown provider name", () => {
|
it("throws on unknown provider name", () => {
|
||||||
@@ -259,3 +270,132 @@ describe("manual provider", () => {
|
|||||||
expect(manualExportProvider).toBe(manualP);
|
expect(manualExportProvider).toBe(manualP);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Ollama provider send (fetch-mocked) ---
|
||||||
|
|
||||||
|
describe("ollama provider send", () => {
|
||||||
|
it("builds correct request body with config values", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ model: "qwen3", message: { role: "assistant", content: "Hello!" }, done: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a custom ollamaProvider with mocked fetch
|
||||||
|
const { ollamaProvider } = await import("../../src/providers/ollama.js");
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = fetchMock;
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
chatgptMcpProvider: "ollama",
|
||||||
|
ollamaBaseUrl: "http://test:11434",
|
||||||
|
ollamaModel: "test-model",
|
||||||
|
ollamaTemperature: 0.5,
|
||||||
|
ollamaTimeout: 30,
|
||||||
|
};
|
||||||
|
const result = await ollamaProvider.send({ prompt: "What is AI?" }, config);
|
||||||
|
expect(result.content).toBe("Hello!");
|
||||||
|
const calls = fetchMock.mock.calls;
|
||||||
|
expect(calls.length).toBe(1);
|
||||||
|
const [url, init] = calls[0];
|
||||||
|
expect(url).toBe("http://test:11434/api/chat");
|
||||||
|
expect(init.method).toBe("POST");
|
||||||
|
expect(JSON.parse(init.body)).toEqual({
|
||||||
|
model: "test-model",
|
||||||
|
messages: [{ role: "system", content: "What is AI?" }],
|
||||||
|
stream: false,
|
||||||
|
options: { temperature: 0.5 },
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses defaults when no config ollama fields are set", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ model: "qwen3", message: { role: "assistant", content: "OK" }, done: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ollamaProvider } = await import("../../src/providers/ollama.js");
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = fetchMock;
|
||||||
|
try {
|
||||||
|
const config = {};
|
||||||
|
const result = await ollamaProvider.send({ prompt: "hi" }, config);
|
||||||
|
expect(result.content).toBe("OK");
|
||||||
|
const [url, init] = fetchMock.mock.calls[0];
|
||||||
|
const body = JSON.parse(init.body);
|
||||||
|
expect(url).toBe("http://localhost:11434/api/chat");
|
||||||
|
expect(body.model).toBe("qwen3:latest");
|
||||||
|
expect(body.options.temperature).toBe(0.2);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles non-2xx response with error category", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 404, text: async () => "model not found" });
|
||||||
|
|
||||||
|
const { ollamaProvider } = await import("../../src/providers/ollama.js");
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = fetchMock;
|
||||||
|
try {
|
||||||
|
const config = {};
|
||||||
|
await expect(
|
||||||
|
ollamaProvider.send({ prompt: "hi" }, config),
|
||||||
|
).rejects.toThrow(/Ollama API error \(OllamaModelNotFoundError\)/);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles fetch network errors", async () => {
|
||||||
|
const fetchMock = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||||
|
|
||||||
|
const { ollamaProvider } = await import("../../src/providers/ollama.js");
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = fetchMock;
|
||||||
|
try {
|
||||||
|
const config = {};
|
||||||
|
await expect(
|
||||||
|
ollamaProvider.send({ prompt: "hi" }, config),
|
||||||
|
).rejects.toThrow(/Ollama API error \(OllamaRequestError\)/);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles invalid JSON response", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({ ok: true, text: async () => "not json" });
|
||||||
|
|
||||||
|
const { ollamaProvider } = await import("../../src/providers/ollama.js");
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = fetchMock;
|
||||||
|
try {
|
||||||
|
const config = {};
|
||||||
|
await expect(
|
||||||
|
ollamaProvider.send({ prompt: "hi" }, config),
|
||||||
|
).rejects.toThrow(/invalid JSON response/);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty content when message.content is missing", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ model: "test", done: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ollamaProvider } = await import("../../src/providers/ollama.js");
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = fetchMock;
|
||||||
|
try {
|
||||||
|
const config = {};
|
||||||
|
const result = await ollamaProvider.send({ prompt: "hi" }, config);
|
||||||
|
expect(result.content).toBe("");
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user