Files
adventure/apps/web/src/lib/ai/providers/ollama.ts
Zaine 3b37368e7d
Some checks failed
CI / test (push) Has been cancelled
initial 2
2026-06-26 09:26:50 +01:00

148 lines
4.8 KiB
TypeScript
Executable File

import type {
AIProvider,
AIProviderConfig,
AIHealthResult,
GenerateTextRequest,
GenerateTextResponse,
} from "../types";
import { isModelAvailable, resolveModelName } from "../model-resolve";
import { redactError, redactUrl } from "../types";
export function createOllamaProvider(): AIProvider {
return {
type: "ollama",
async healthCheck(config: AIProviderConfig): Promise<AIHealthResult> {
const start = Date.now();
try {
const res = await fetch(`${config.baseUrl}/api/tags`, {
signal: AbortSignal.timeout(5000),
});
const latencyMs = Date.now() - start;
if (!res.ok) {
return {
status: "offline",
provider: "ollama",
model: config.model,
latencyMs,
lastSuccessAt: null,
lastFailureAt: new Date().toISOString(),
lastError: redactError(`HTTP ${res.status}`),
baseUrlSafe: redactUrl(config.baseUrl),
contextLength: null,
memoryUsageMb: null,
modelsAvailable: [],
};
}
const data = await res.json();
const models = (data.models ?? []).map(
(m: { name: string }) => m.name
);
const modelReady = isModelAvailable(config.model, models);
const resolvedModel = resolveModelName(config.model, models);
return {
status:
models.length === 0
? "degraded"
: modelReady || resolvedModel
? "online"
: "degraded",
provider: "ollama",
model: resolvedModel ?? config.model,
latencyMs,
lastSuccessAt: new Date().toISOString(),
lastFailureAt: modelReady ? null : new Date().toISOString(),
lastError: modelReady
? null
: models.length === 0
? "No models installed in Ollama"
: `Model "${config.model}" not found. Available: ${models.slice(0, 5).join(", ")}`,
baseUrlSafe: redactUrl(config.baseUrl),
contextLength: null,
memoryUsageMb: null,
modelsAvailable: models,
};
} catch (e) {
return {
status: "offline",
provider: "ollama",
model: config.model,
latencyMs: Date.now() - start,
lastSuccessAt: null,
lastFailureAt: new Date().toISOString(),
lastError: redactError(e instanceof Error ? e.message : "Connection failed"),
baseUrlSafe: redactUrl(config.baseUrl),
contextLength: null,
memoryUsageMb: null,
modelsAvailable: [],
};
}
},
async listModels(config: AIProviderConfig) {
const res = await fetch(`${config.baseUrl}/api/tags`, {
signal: AbortSignal.timeout(5000),
});
if (!res.ok) return [];
const data = await res.json();
return (data.models ?? []).map((m: { name: string }) => m.name);
},
async generateText(
config: AIProviderConfig,
req: GenerateTextRequest
): Promise<GenerateTextResponse> {
const start = Date.now();
const requested = req.model || config.model;
let model = requested;
const tagsRes = await fetch(`${config.baseUrl}/api/tags`, {
signal: AbortSignal.timeout(5000),
});
if (tagsRes.ok) {
const tagsData = await tagsRes.json();
const models = (tagsData.models ?? []).map(
(m: { name: string }) => m.name as string
);
const resolved = resolveModelName(requested, models);
if (resolved) {
model = resolved;
} else if (models.length === 0) {
throw new Error(`Ollama error: no models installed`);
}
if (model !== requested) {
console.error(
`[adventureos-ai] ollama: resolved model "${requested}" -> "${model}"`
);
}
}
const res = await fetch(`${config.baseUrl}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
prompt: req.prompt,
system: req.system,
stream: false,
format: req.format === "json" ? "json" : undefined,
options: {
temperature: req.temperature ?? config.temperature,
num_predict: req.maxTokens ?? config.maxTokens,
},
}),
signal: AbortSignal.timeout(req.timeoutMs ?? config.timeoutMs),
});
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(
`Ollama error: HTTP ${res.status}${body ? `${body.slice(0, 200)}` : ""}`
);
}
const data = await res.json();
return {
text: data.response ?? "",
model,
latencyMs: Date.now() - start,
};
},
};
}