148 lines
4.8 KiB
TypeScript
Executable File
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,
|
|
};
|
|
},
|
|
};
|
|
}
|