feat(backend): Ollama embeddings preflight on session create/resume (503 hard-fail)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -43,7 +43,10 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
|
||||
|
||||
app.addHook("preHandler", authPreHandler(config.authMode));
|
||||
app.get("/health", async () => ({ status: "ok" }));
|
||||
sessionRoutes(app, { mgr, tht: tht as ThtRunner, hub, getSettings });
|
||||
sessionRoutes(app, {
|
||||
mgr, tht: tht as ThtRunner, hub, getSettings,
|
||||
ollamaEnsureTimeoutSec: Math.round(config.ollamaEnsureTimeoutMs / 1000),
|
||||
});
|
||||
sqlRoutes(app, { tht: tht as ThtRunner });
|
||||
metaRoutes(app, { harnessDir: config.harnessDir, listModels });
|
||||
settingsRoutes(app, { cfg: config, listModels });
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface AppConfig {
|
||||
defaults: { provider?: string; model?: string; thinking?: string };
|
||||
maxPiProcesses: number;
|
||||
settingsFile: string;
|
||||
ollamaEnsureTimeoutMs: number;
|
||||
}
|
||||
export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
return {
|
||||
@@ -15,5 +16,6 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
defaults: { provider: env.PI_PROVIDER, model: env.PI_MODEL, thinking: env.PI_THINKING },
|
||||
maxPiProcesses: Number(env.MAX_PI_PROCESSES ?? 4),
|
||||
settingsFile: env.SETTINGS_FILE ?? "data/settings.json",
|
||||
ollamaEnsureTimeoutMs: Number(env.OLLAMA_ENSURE_TIMEOUT_MS ?? 60000),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import { getUser } from "../auth/auth.js";
|
||||
|
||||
export function sessionRoutes(
|
||||
app: FastifyInstance,
|
||||
d: { mgr: PiProcessManager; tht: ThtRunner; hub: SseHub; getSettings: () => Settings },
|
||||
d: { mgr: PiProcessManager; tht: ThtRunner; hub: SseHub; getSettings: () => Settings; ollamaEnsureTimeoutSec: number },
|
||||
) {
|
||||
app.post("/sessions", async (req, reply) => {
|
||||
const b = req.body as { question: string; name?: string };
|
||||
const s = d.getSettings();
|
||||
const ensure = await d.tht.ollamaEnsure(s.workspace ?? "", d.ollamaEnsureTimeoutSec);
|
||||
if (!ensure.ok) return reply.code(503).send({ error: ensure.error ?? "Ollama/embeddings non disponibili" });
|
||||
// Settings (global) supply workspace/provider/model/thinking. The new-question
|
||||
// form sends only the question text. `workspace` selects the tht `-c <config>`.
|
||||
const { id } = await d.tht.sessionNew({
|
||||
@@ -48,6 +50,8 @@ export function sessionRoutes(
|
||||
});
|
||||
app.post("/sessions/:id/resume", async (req, reply) => {
|
||||
const id = (req.params as any).id;
|
||||
const ensure = await d.tht.ollamaEnsure(d.getSettings().workspace ?? "", d.ollamaEnsureTimeoutSec);
|
||||
if (!ensure.ok) return reply.code(503).send({ error: ensure.error ?? "Ollama/embeddings non disponibili" });
|
||||
const manifest = (await d.tht.sessionShow(id)) as { status?: string; archived?: boolean } | null;
|
||||
if (manifest?.status === "finalized" || manifest?.archived) {
|
||||
return reply.code(409).send({ error: "sessione in sola lettura (finalizzata o archiviata)" });
|
||||
|
||||
@@ -10,6 +10,7 @@ const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.
|
||||
test("loop F1: crea sessione → SSE riceve il widget → risponde → 204", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
ollamaEnsure: async () => ({ ok: true }),
|
||||
sessionNew: async () => ({ id: "s1" }),
|
||||
sessionShow: async (_id: string) => ({ id: "s1", provider: undefined, model: undefined, thinking: undefined }),
|
||||
sessionList: async () => [],
|
||||
|
||||
@@ -9,7 +9,7 @@ const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.
|
||||
|
||||
function mutApp(thtRunner: any) {
|
||||
return buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner,
|
||||
thtRunner: { ollamaEnsure: async () => ({ ok: true }), ...thtRunner },
|
||||
getSettings: () => ({ workspace: "w" }) as any,
|
||||
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
||||
});
|
||||
@@ -20,6 +20,7 @@ test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+a
|
||||
let spawnArg: any;
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
ollamaEnsure: async () => ({ ok: true }),
|
||||
sessionNew: async (o: any) => { sessionNewArg = o; return { id: "s1" }; },
|
||||
sessionList: async () => [{ id: "s1" }],
|
||||
} as any,
|
||||
@@ -40,6 +41,7 @@ test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+a
|
||||
test("POST /sessions/:id/response inoltra al bridge (no error)", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
ollamaEnsure: async () => ({ ok: true }),
|
||||
sessionNew: async () => ({ id: "s1" }),
|
||||
sessionList: async () => [],
|
||||
} as any,
|
||||
@@ -118,3 +120,47 @@ test("POST resume on an archived session is refused with 409", async () => {
|
||||
const res = await app.inject({ method: "POST", url: "/sessions/s1/resume" });
|
||||
expect(res.statusCode).toBe(409);
|
||||
});
|
||||
|
||||
test("POST /sessions refuses with 503 when ollamaEnsure fails (no session created)", async () => {
|
||||
let createdCalled = false;
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
ollamaEnsure: async () => ({ ok: false, stage: "model", error: "modello non installato" }),
|
||||
sessionNew: async () => { createdCalled = true; return { id: "s1" }; },
|
||||
} as any,
|
||||
getSettings: () => ({ workspace: "psd" }) as any,
|
||||
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
||||
});
|
||||
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
||||
expect(res.statusCode).toBe(503);
|
||||
expect(res.json().error).toContain("non installato");
|
||||
expect(createdCalled).toBe(false);
|
||||
});
|
||||
|
||||
test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
|
||||
let ensureWs: string | undefined;
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
ollamaEnsure: async (ws: string) => { ensureWs = ws; return { ok: true }; },
|
||||
sessionNew: async () => ({ id: "s1" }),
|
||||
} as any,
|
||||
getSettings: () => ({ workspace: "psd" }) as any,
|
||||
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
||||
});
|
||||
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
||||
expect(res.json()).toEqual({ id: "s1" });
|
||||
expect(ensureWs).toBe("psd");
|
||||
});
|
||||
|
||||
test("POST /sessions/:id/resume refuses with 503 when ollamaEnsure fails", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
ollamaEnsure: async () => ({ ok: false, error: "Ollama down" }),
|
||||
sessionShow: async () => ({ status: "open", archived: false }),
|
||||
} as any,
|
||||
getSettings: () => ({ workspace: "psd" }) as any,
|
||||
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
||||
});
|
||||
const res = await app.inject({ method: "POST", url: "/sessions/s1/resume" });
|
||||
expect(res.statusCode).toBe(503);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user