- 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
121 lines
2.5 KiB
TypeScript
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();
|
|
}
|