llmatic
Version:
Use self-hosted LLMs with an OpenAI compatible API
145 lines (122 loc) • 3.94 kB
text/typescript
import type { Cradle } from "./container.ts";
import type { OperationHandler } from "./operation-handler.ts";
import fastifyStatic from "@fastify/static";
import ajvModule from "ajv";
import fastify from "fastify";
import openapiGlue from "fastify-openapi-glue";
import { glob } from "glob";
import yaml from "js-yaml";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import swaggerUiDist from "swagger-ui-dist";
import traverse from "traverse";
// https://github.com/ajv-validator/ajv/issues/2132
// eslint-disable-next-line @typescript-eslint/naming-convention
const Ajv = ajvModule.default;
// FIXME: fix the types
const createOpenapiGlueService = async ({ container }: Partial<Cradle>) => {
const routeHandlerFiles = await glob("**/*.handler.[tj]s", {
cwd: new URL("handlers", import.meta.url),
absolute: true,
});
const handlers = await Promise.all(
routeHandlerFiles.map(async (file) => {
const { default: handlerConstructor } = (await import(file)) as {
default: (...arguments_: any[]) => OperationHandler;
};
return container!.build(handlerConstructor);
}),
);
return Object.fromEntries(
handlers.map((handler) => [
handler.operationId,
handler.handle.bind(handler),
]),
);
};
// FIXME: fix the types
const configureOpenapiGlue = async ({
container,
fastifyServer,
openapiDocument,
}: Partial<Cradle> & { openapiDocument: any }) => {
const schemaCompilers = {
body: new Ajv(),
params: new Ajv(),
querystring: new Ajv(),
headers: new Ajv(),
};
fastifyServer!.setValidatorCompiler((request) => {
if (!request.httpPart) {
throw new Error("Missing httpPart");
}
const compiler = schemaCompilers[request.httpPart] as
| ajvModule.default
| undefined;
if (!compiler) {
throw new Error(`Missing compiler for ${request.httpPart}`);
}
// OpenAI OAS is not entirely valid/compatible, so we need to remove some properties
// eslint-disable-next-line unicorn/no-array-for-each
traverse(request.schema).forEach(function (value) {
if (!this.key) return;
if (this.isLeaf && ["nullable", "x-oaiTypeLabel"].includes(this.key)) {
this.remove();
}
if (this.key === "example") {
this.remove();
}
if (this.isLeaf && this.key === "format" && value === "binary") {
this.remove();
}
});
return compiler.compile(request.schema);
});
const service = await createOpenapiGlueService({ container });
await fastifyServer!.register(openapiGlue, {
specification: openapiDocument as Record<string, unknown>,
prefix: "/v1",
service,
securityHandlers: {},
});
};
// FIXME: fix the types
const configureSwaggerUi = async ({
fastifyServer,
openapiDocument,
}: Partial<Cradle> & { openapiDocument: any }) => {
await fastifyServer!.register(fastifyStatic, {
root: swaggerUiDist.getAbsoluteFSPath(),
prefix: "/swagger-ui/",
});
fastifyServer!.get("/", (request, reply) =>
reply.sendFile(
"index.html",
fileURLToPath(new URL("../public", import.meta.url)),
),
);
fastifyServer!.get("/api.oas.yml", (request, reply) => {
const newOas = {
...(openapiDocument as Record<string, unknown>),
servers: [
{
url: `${request.protocol}://${request.hostname}/v1`,
},
],
};
return reply.type("text/yaml").send(yaml.dump(newOas));
});
};
export const createFastifyServer = async ({ container }: Cradle) => {
const fastifyServer = fastify({
logger: true,
});
const openapiDocument = yaml.load(
await fs.promises.readFile(new URL("../api.oas.yml", import.meta.url), {
encoding: "utf8",
}),
);
await configureSwaggerUi({ fastifyServer, openapiDocument });
await configureOpenapiGlue({ container, fastifyServer, openapiDocument });
return fastifyServer;
};