local-ai-platform/src/server.ts
Brandon Wolfgang 8cfcb1a3cf feat: integrate Ollama inference with API and readiness checks
- Add an Ollama client for Qwen3 8B generation with request timeouts and HTTP error handling
- Implement POST /generate with request validation and sanitized failure responses
- Update GET /ready to report Ollama availability and return HTTP 503 when unavailable
- Add unit and integration tests for generation, dependency health, and failure handling
- Document implemented API endpoints, setup instructions, scripts, and remaining planned capabilities in README
2026-10-21 21:37:51 -05:00

121 lines
2.5 KiB
TypeScript

import Fastify, { type FastifyError } from "fastify";
import { z } from "zod";
import { checkOllamaHealth, generateResponse } from "./ollama.js";
const echoSchema = z.object({
message: z.string().min(1).max(500),
});
export const buildApp = (
generate = generateResponse,
checkHealth = checkOllamaHealth,
) => {
const app = Fastify({
logger: true,
});
app.setErrorHandler((error: FastifyError, request, reply) => {
request.log.error(error);
const candidateStatusCode = error.statusCode ?? 500;
const statusCode =
candidateStatusCode >= 400 && candidateStatusCode < 500
? candidateStatusCode
: 500;
return reply.code(statusCode).send({
error: statusCode === 500 ? "Internal Server Error" : "Request Error",
});
});
app.setNotFoundHandler((_request, reply) => {
return reply.code(404).send({
error: "Not Found",
});
});
app.get("/health", async () => ({
status: "ok",
}));
app.get("/ready", async (_request, reply) => {
const ollamaReady = await checkHealth();
if (!ollamaReady) {
return reply.code(503).send({
status: "not-ready",
dependencies: {
ollama: "unavailable",
},
});
}
return {
status: "ready",
dependencies: {
ollama: "available",
},
};
});
app.post("/echo", async (request, reply) => {
const result = echoSchema.safeParse(request.body);
if (!result.success) {
return reply.code(400).send({
error: "Invalid request body",
});
}
return {
message: result.data.message,
};
});
app.post("/generate", async (request, reply) => {
const result = echoSchema.safeParse(request.body);
if (!result.success) {
return reply.code(400).send({
error: "Invalid request body",
});
}
try {
const response = await generate(result.data.message);
return { response };
} catch (error) {
request.log.error({ err: error }, "Ollama generation failed");
return reply.code(503).send({
error: "Generation service unavailable",
});
}
});
app.get("/test-error", async () => {
throw new Error("Sensitive internal detail");
});
return app;
};
const start = async () => {
const app = buildApp();
try {
await app.listen({
host: "127.0.0.1",
port: 3000,
});
} catch (error) {
app.log.error(error);
process.exit(1);
}
};
if (import.meta.url === `file://${process.argv[1]}`) {
void start();
}