From bb1673256297c679d5bd641c76b5b5914a5e96a3 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 3 Feb 2026 00:27:09 -0800 Subject: [PATCH] feat: add Ollama client for local LLM support Co-Authored-By: Claude Opus 4.5 --- src/models/index.ts | 1 + src/models/local/index.ts | 1 + src/models/local/ollama.test.ts | 30 ++++++++++++++++++++++ src/models/local/ollama.ts | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/models/local/index.ts create mode 100644 src/models/local/ollama.test.ts create mode 100644 src/models/local/ollama.ts diff --git a/src/models/index.ts b/src/models/index.ts index 11c7316..be1ada3 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,3 +1,4 @@ export { AnthropicClient, type AnthropicClientConfig } from './anthropic.js'; export { OpenAIClient, type OpenAIClientConfig } from './openai.js'; +export { OllamaClient, type OllamaClientConfig } from './local/index.js'; export type { Message, ChatRequest, ChatResponse, ModelClient } from './types.js'; diff --git a/src/models/local/index.ts b/src/models/local/index.ts new file mode 100644 index 0000000..a854196 --- /dev/null +++ b/src/models/local/index.ts @@ -0,0 +1 @@ +export { OllamaClient, type OllamaClientConfig } from './ollama.js'; diff --git a/src/models/local/ollama.test.ts b/src/models/local/ollama.test.ts new file mode 100644 index 0000000..8a8447e --- /dev/null +++ b/src/models/local/ollama.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, vi } from 'vitest'; +import { OllamaClient } from './ollama.js'; + +vi.mock('ollama', () => ({ + Ollama: vi.fn().mockImplementation(() => ({ + chat: vi.fn().mockResolvedValue({ + message: { content: 'Hello from Ollama!' }, + done_reason: 'stop', + prompt_eval_count: 10, + eval_count: 5, + }), + })), +})); + +describe('OllamaClient', () => { + it('sends messages and returns response', async () => { + const client = new OllamaClient({ + model: 'llama3.2', + }); + + const response = await client.chat({ + messages: [{ role: 'user', content: 'Hello' }], + }); + + expect(response.content).toBe('Hello from Ollama!'); + expect(response.stopReason).toBe('stop'); + expect(response.usage.inputTokens).toBe(10); + expect(response.usage.outputTokens).toBe(5); + }); +}); diff --git a/src/models/local/ollama.ts b/src/models/local/ollama.ts new file mode 100644 index 0000000..94361c1 --- /dev/null +++ b/src/models/local/ollama.ts @@ -0,0 +1,45 @@ +import { Ollama } from 'ollama'; +import type { ChatRequest, ChatResponse, ModelClient } from '../types.js'; + +export interface OllamaClientConfig { + host?: string; + model: string; +} + +export class OllamaClient implements ModelClient { + private client: Ollama; + private model: string; + + constructor(config: OllamaClientConfig) { + this.client = new Ollama({ + host: config.host ?? 'http://localhost:11434', + }); + this.model = config.model; + } + + async chat(request: ChatRequest): Promise { + const messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = []; + + if (request.system) { + messages.push({ role: 'system', content: request.system }); + } + + for (const msg of request.messages) { + messages.push({ role: msg.role, content: msg.content }); + } + + const response = await this.client.chat({ + model: this.model, + messages, + }); + + return { + content: response.message.content, + stopReason: response.done_reason ?? 'stop', + usage: { + inputTokens: response.prompt_eval_count ?? 0, + outputTokens: response.eval_count ?? 0, + }, + }; + } +}