212 lines
4.7 KiB
TypeScript
212 lines
4.7 KiB
TypeScript
const DEFAULT_OLLAMA_HOST = 'http://127.0.0.1:11434';
|
|
const OLLAMA_DOWNLOAD_URL = 'https://ollama.com/download';
|
|
const PREFERRED_MODEL = 'gpt-oss:20b';
|
|
|
|
export interface OllamaStatus {
|
|
isRunning: boolean;
|
|
host: string;
|
|
error?: string;
|
|
}
|
|
|
|
export interface OllamaModel {
|
|
name: string;
|
|
size: number;
|
|
digest: string;
|
|
modified_at: string;
|
|
}
|
|
|
|
export interface PullProgress {
|
|
status: string;
|
|
digest?: string;
|
|
total?: number;
|
|
completed?: number;
|
|
}
|
|
|
|
/**
|
|
* Check if Ollama is running on the default port
|
|
*/
|
|
export async function checkOllamaStatus(): Promise<OllamaStatus> {
|
|
try {
|
|
// Create an AbortController for timeout
|
|
const controller = new AbortController();
|
|
const timeoutId = window.setTimeout(() => controller.abort(), 2000);
|
|
|
|
try {
|
|
// Ollama exposes a health endpoint at /api/tags
|
|
const response = await fetch(`${DEFAULT_OLLAMA_HOST}/api/tags`, {
|
|
method: 'GET',
|
|
signal: controller.signal,
|
|
});
|
|
|
|
window.clearTimeout(timeoutId);
|
|
|
|
return {
|
|
isRunning: response.ok,
|
|
host: DEFAULT_OLLAMA_HOST,
|
|
};
|
|
} catch (err) {
|
|
window.clearTimeout(timeoutId);
|
|
throw err;
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
isRunning: false,
|
|
host: DEFAULT_OLLAMA_HOST,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the Ollama download URL
|
|
*/
|
|
export function getOllamaDownloadUrl(): string {
|
|
return OLLAMA_DOWNLOAD_URL;
|
|
}
|
|
|
|
/**
|
|
* Get the preferred model name
|
|
*/
|
|
export function getPreferredModel(): string {
|
|
return PREFERRED_MODEL;
|
|
}
|
|
|
|
/**
|
|
* Check which models are available in Ollama
|
|
*/
|
|
export async function getOllamaModels(): Promise<OllamaModel[]> {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = window.setTimeout(() => controller.abort(), 5000);
|
|
|
|
try {
|
|
const response = await fetch(`${DEFAULT_OLLAMA_HOST}/api/tags`, {
|
|
method: 'GET',
|
|
signal: controller.signal,
|
|
});
|
|
|
|
window.clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to get models: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.models || [];
|
|
} catch (err) {
|
|
window.clearTimeout(timeoutId);
|
|
throw err;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to get Ollama models:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a specific model is available
|
|
*/
|
|
export async function hasModel(modelName: string): Promise<boolean> {
|
|
const models = await getOllamaModels();
|
|
return models.some((model) => model.name === modelName);
|
|
}
|
|
|
|
/**
|
|
* Pull a model from Ollama
|
|
*/
|
|
export async function pullOllamaModel(
|
|
modelName: string,
|
|
onProgress?: (progress: PullProgress) => void
|
|
): Promise<boolean> {
|
|
try {
|
|
const response = await fetch(`${DEFAULT_OLLAMA_HOST}/api/pull`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: modelName,
|
|
stream: true,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to pull model: ${response.statusText}`);
|
|
}
|
|
|
|
const reader = response.body?.getReader();
|
|
if (!reader) {
|
|
throw new Error('No response body');
|
|
}
|
|
|
|
const decoder = new window.TextDecoder();
|
|
let done = false;
|
|
|
|
while (!done) {
|
|
const { value, done: readerDone } = await reader.read();
|
|
done = readerDone;
|
|
|
|
if (value) {
|
|
const text = decoder.decode(value);
|
|
const lines = text.split('\n').filter((line) => line.trim());
|
|
|
|
for (const line of lines) {
|
|
try {
|
|
const progress = JSON.parse(line) as PullProgress;
|
|
if (onProgress) {
|
|
onProgress(progress);
|
|
}
|
|
} catch {
|
|
// Ignore parse errors
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to pull model:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Poll for Ollama availability
|
|
* @param onDetected Callback when Ollama is detected
|
|
* @param intervalMs Polling interval in milliseconds
|
|
* @returns A function to stop polling
|
|
*/
|
|
export function pollForOllama(
|
|
onDetected: (status: OllamaStatus) => void,
|
|
intervalMs: number = 5000
|
|
): () => void {
|
|
let intervalId: number | null = null;
|
|
let isPolling = true;
|
|
|
|
const poll = async () => {
|
|
if (!isPolling) return;
|
|
|
|
const status = await checkOllamaStatus();
|
|
if (status.isRunning) {
|
|
onDetected(status);
|
|
stopPolling();
|
|
}
|
|
};
|
|
|
|
const stopPolling = () => {
|
|
isPolling = false;
|
|
if (intervalId) {
|
|
window.clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
};
|
|
|
|
// Start polling immediately
|
|
poll();
|
|
|
|
// Then poll at intervals
|
|
intervalId = window.setInterval(poll, intervalMs);
|
|
|
|
return stopPolling;
|
|
}
|