147
apps/web/src/lib/ai/providers/ollama.ts
Executable file
147
apps/web/src/lib/ai/providers/ollama.ts
Executable file
@@ -0,0 +1,147 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user