146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import type {
|
|
OllamaClientOptions,
|
|
OllamaModel,
|
|
OllamaRunningModel,
|
|
OllamaShowResponse,
|
|
OllamaPullProgress,
|
|
OllamaVersionResponse,
|
|
} from './types.js';
|
|
import { parseNdjsonStream } from './ndjson.js';
|
|
|
|
/**
|
|
* Ollama API client for model management operations.
|
|
*
|
|
* Provides typed methods for all non-streaming Ollama endpoints:
|
|
* tags, ps, show, pull, load, unload, delete, version.
|
|
*/
|
|
export class OllamaClient {
|
|
readonly baseUrl: string;
|
|
private readonly timeoutMs: number;
|
|
|
|
constructor(options: OllamaClientOptions) {
|
|
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
}
|
|
|
|
private async fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
|
|
try {
|
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
...init,
|
|
signal: init?.signal ?? controller.signal,
|
|
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(`Ollama ${path} failed (${res.status}): ${text.slice(0, 200)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
/** List all locally available models (GET /api/tags). */
|
|
async tags(): Promise<OllamaModel[]> {
|
|
const data = await this.fetchJson<{ models: OllamaModel[] }>('/api/tags');
|
|
return data.models ?? [];
|
|
}
|
|
|
|
/** List currently running/loaded models (GET /api/ps). */
|
|
async ps(): Promise<OllamaRunningModel[]> {
|
|
const data = await this.fetchJson<{ models: OllamaRunningModel[] }>('/api/ps');
|
|
return data.models ?? [];
|
|
}
|
|
|
|
/** Show model details (POST /api/show). */
|
|
async show(model: string): Promise<OllamaShowResponse> {
|
|
return this.fetchJson<OllamaShowResponse>('/api/show', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name: model }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pull a model from the Ollama registry (POST /api/pull).
|
|
*
|
|
* When `stream: false`, waits for the full download to complete.
|
|
* When `stream: true`, returns an async generator of progress chunks.
|
|
*/
|
|
async pull(model: string, stream?: false): Promise<{ status: string }>;
|
|
async pull(model: string, stream: true): Promise<AsyncGenerator<OllamaPullProgress>>;
|
|
async pull(
|
|
model: string,
|
|
stream: boolean = false
|
|
): Promise<{ status: string } | AsyncGenerator<OllamaPullProgress>> {
|
|
if (!stream) {
|
|
// Model pulls can download GBs — use 10 minute timeout instead of the default
|
|
const pullTimeoutMs = Math.max(this.timeoutMs, 600_000);
|
|
const res = await fetch(`${this.baseUrl}/api/pull`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: model, stream: false }),
|
|
signal: AbortSignal.timeout(pullTimeoutMs),
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(`Ollama pull failed (${res.status}): ${text.slice(0, 200)}`);
|
|
}
|
|
return (await res.json()) as { status: string };
|
|
}
|
|
|
|
// Streaming pull — return async generator (no timeout, consumer controls lifetime)
|
|
const res = await fetch(`${this.baseUrl}/api/pull`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: model, stream: true }),
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(`Ollama pull failed (${res.status}): ${text.slice(0, 200)}`);
|
|
}
|
|
if (!res.body) throw new Error('No response body from Ollama pull');
|
|
|
|
return parseNdjsonStream<OllamaPullProgress>(res.body);
|
|
}
|
|
|
|
/**
|
|
* Load a model into memory (POST /api/generate with empty prompt + keep_alive).
|
|
*
|
|
* @param model - Model name
|
|
* @param keepAlive - How long to keep the model loaded (default: '10m')
|
|
*/
|
|
async load(model: string, keepAlive: string = '10m'): Promise<void> {
|
|
await this.fetchJson('/api/generate', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ model, prompt: '', keep_alive: keepAlive }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Unload a model from memory (POST /api/generate with keep_alive: '0').
|
|
*/
|
|
async unload(model: string): Promise<void> {
|
|
await this.fetchJson('/api/generate', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ model, prompt: '', keep_alive: '0' }),
|
|
});
|
|
}
|
|
|
|
/** Delete a model (DELETE /api/delete). */
|
|
async delete(model: string): Promise<void> {
|
|
await this.fetchJson('/api/delete', {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ name: model }),
|
|
});
|
|
}
|
|
|
|
/** Get Ollama server version (GET /api/version). */
|
|
async version(): Promise<string> {
|
|
const data = await this.fetchJson<OllamaVersionResponse>('/api/version');
|
|
return data.version;
|
|
}
|
|
}
|