Skip to content

Commit

Permalink
OpenAPI zod Hono integration
Browse files Browse the repository at this point in the history
  • Loading branch information
neoxelox authored and andresgutgon committed Dec 20, 2024
1 parent 6218b9b commit acffc2c
Show file tree
Hide file tree
Showing 81 changed files with 1,578 additions and 757 deletions.
7 changes: 4 additions & 3 deletions apps/gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org latitude-l5 --project latitude-llm-app ./dist && sentry-cli sourcemaps upload --org latitude-l5 --project latitude-llm-app ./dist"
},
"dependencies": {
"@hono/node-server": "^1.12.0",
"@hono/zod-validator": "^0.2.2",
"@hono/node-server": "^1.13.2",
"@hono/swagger-ui": "^0.4.1",
"@hono/zod-openapi": "^0.16.4",
"@latitude-data/compiler": "workspace:^",
"@latitude-data/constants": "workspace:^",
"@latitude-data/core": "workspace:^",
Expand All @@ -27,7 +28,7 @@
"@sentry/node": "^8.30.0",
"@t3-oss/env-core": "^0.10.1",
"drizzle-orm": "^0.33.0",
"hono": "^4.5.3",
"hono": "^4.6.6",
"lodash-es": "^4.17.21",
"promptl-ai": "^0.3.3",
"rate-limiter-flexible": "^5.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Hypertext Transfer Protocol (HTTP) response status codes.
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
*/
enum HttpStatusCodes {
enum Status {
/**
* The server has received the request headers and the client should proceed to send the request body
* (in the case of a request for which a body needs to be sent; for example, a POST request).
Expand Down Expand Up @@ -381,4 +381,32 @@ enum HttpStatusCodes {
NETWORK_AUTHENTICATION_REQUIRED = 511,
}

export default HttpStatusCodes
/**
* Hypertext Transfer Protocol (HTTP) request methods.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods}
*/
enum Methods {
GET = 'get',
HEAD = 'head',
POST = 'post',
PUT = 'put',
DELETE = 'delete',
CONNECT = 'connect',
OPTIONS = 'options',
TRACE = 'trace',
PATCH = 'patch',
}

/**
* Common HTTP Media Types (MIME Types)
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types}
*/
enum MediaTypes {
JSON = 'application/json',
SSE = 'text/event-stream',
TEXT = 'text/plain',
HTML = 'text/html',
XML = 'application/xml',
}

export default { Status, Methods, MediaTypes }
5 changes: 2 additions & 3 deletions apps/gateway/src/middlewares/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import {
UnprocessableEntityError,
} from '@latitude-data/core/lib/errors'
import { ChainError } from '@latitude-data/core/services/chains/ChainErrors/index'
import http from '$/common/http'
import { captureException } from '$/common/sentry'
import { HTTPException } from 'hono/http-exception'

import HttpStatusCodes from '../common/httpStatusCodes'

function unprocessableExtraParameters(error: UnprocessableEntityError) {
const isChainError = error instanceof ChainError
if (!isChainError) return { name: error.name, errorCode: error.name }
Expand Down Expand Up @@ -69,7 +68,7 @@ const errorHandlerMiddleware = (err: Error) => {
message: err.message,
details: { cause: err.cause },
},
{ status: HttpStatusCodes.INTERNAL_SERVER_ERROR },
{ status: http.Status.INTERNAL_SERVER_ERROR },
)
}
}
Expand Down
53 changes: 53 additions & 0 deletions apps/gateway/src/openApi/configureOpenAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { OpenAPIHono } from '@hono/zod-openapi'

import { swaggerUI } from '@hono/swagger-ui'

import packageJson from '../../package.json'
import { tags } from '$/openApi/tags'

const isDev = process.env.NODE_ENV === 'development'
let servers = [
{ url: 'https://gateway.latitude.so', description: 'Latitude production' },
]

if (isDev) {
servers = [
{ url: 'http://localhost:8787', description: 'Latitude development' },
...servers,
]
}

export const openAPIObjectConfig = {
openapi: '3.1.0',
info: { title: 'Latitude API', version: packageJson.version },
tags: tags,
security: [{ Bearer: [] }],
externalDocs: {
url: 'https://docs.latitude.so',
description: 'Latitude Documentation',
},
servers,
}

export default function configureOpenAPI(app: OpenAPIHono) {
app.openAPIRegistry.registerComponent('securitySchemes', 'Bearer', {
type: 'http',
scheme: 'bearer',
bearerFormat: 'token',
description: 'Latitude API Key',
})

app.doc31('/doc', openAPIObjectConfig)
app.get(
'/ui',
swaggerUI({
url: '/doc',
docExpansion: 'list',
requestSnippetsEnabled: true,
syntaxHighlight: {
activated: true,
theme: ['nord'],
},
}),
)
}
9 changes: 9 additions & 0 deletions apps/gateway/src/openApi/createApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { OpenAPIHono } from '@hono/zod-openapi'

export function createRouter() {
return new OpenAPIHono({ strict: false })
}

export default function createApp() {
return new OpenAPIHono({ strict: false })
}
42 changes: 42 additions & 0 deletions apps/gateway/src/openApi/responses/errorResponses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import http from '$/common/http'
import {
BadRequestErrorSchema,
HTTPExceptionErrorSchema,
InternalServerErrorSchema,
UnprocessableEntityErrorSchema,
} from '$/openApi/schemas'

export const GENERIC_ERROR_RESPONSES = {
[http.Status.NOT_FOUND]: {
description: 'The requested resource was not found.',
content: {
[http.MediaTypes.JSON]: {
schema: HTTPExceptionErrorSchema,
},
},
},
[http.Status.UNPROCESSABLE_ENTITY]: {
description: 'The request was valid but could not be processed.',
content: {
[http.MediaTypes.JSON]: {
schema: UnprocessableEntityErrorSchema,
},
},
},
[http.Status.BAD_REQUEST]: {
description: 'The request was invalid or cannot be processed.',
content: {
[http.MediaTypes.JSON]: {
schema: BadRequestErrorSchema,
},
},
},
[http.Status.INTERNAL_SERVER_ERROR]: {
description: 'An unexpected error occurred.',
content: {
[http.MediaTypes.JSON]: {
schema: InternalServerErrorSchema,
},
},
},
}
98 changes: 98 additions & 0 deletions apps/gateway/src/openApi/schemas/ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { z } from '@hono/zod-openapi'
import { ChainEventTypes, StreamEventTypes } from '@latitude-data/constants'
import { messageSchema } from '@latitude-data/core/browser'

export const languageModelUsageSchema = z.object({
completionTokens: z.number().optional(),
promptTokens: z.number().optional(),
totalTokens: z.number().optional(),
})

export const toolCallSchema = z.object({
id: z.string(),
name: z.string(),
arguments: z.record(z.any()),
})

export const configSchema = z.object({}).passthrough()
export const providerLogSchema = z.object({}).passthrough()
export const chainStepResponseSchema = z.discriminatedUnion('streamType', [
z.object({
streamType: z.literal('text'),
text: z.string(),
usage: languageModelUsageSchema,
toolCalls: z.array(toolCallSchema),
documentLogUuid: z.string().optional(),
providerLog: providerLogSchema.optional(),
}),
z.object({
streamType: z.literal('object'),
object: z.any(),
text: z.string(),
usage: languageModelUsageSchema,
documentLogUuid: z.string().optional(),
providerLog: providerLogSchema.optional(),
}),
])

export const chainCallResponseDtoSchema = z.discriminatedUnion('streamType', [
chainStepResponseSchema.options[0].omit({
documentLogUuid: true,
providerLog: true,
}),
chainStepResponseSchema.options[1].omit({
documentLogUuid: true,
providerLog: true,
}),
])

export const chainEventDtoResponseSchema = z.discriminatedUnion('streamType', [
chainStepResponseSchema.options[0].omit({ providerLog: true }),
chainStepResponseSchema.options[1].omit({ providerLog: true }),
])

export const chainEventDtoSchema = z.discriminatedUnion('event', [
z.object({
event: z.literal(StreamEventTypes.Provider),
data: z.object({}).passthrough(),
}),
z.object({
event: z.literal(StreamEventTypes.Latitude),
data: z.discriminatedUnion('type', [
z.object({
type: z.literal(ChainEventTypes.Step),
config: configSchema,
isLastStep: z.boolean(),
messages: z.array(messageSchema),
uuid: z.string().optional(),
}),
z.object({
type: z.literal(ChainEventTypes.StepComplete),
response: chainEventDtoResponseSchema,
uuid: z.string().optional(),
}),
z.object({
type: z.literal(ChainEventTypes.Complete),
config: configSchema,
messages: z.array(messageSchema).optional(),
object: z.object({}).passthrough().optional(),
response: chainEventDtoResponseSchema,
uuid: z.string().optional(),
}),
z.object({
type: z.literal(ChainEventTypes.Error),
error: z.object({
name: z.string(),
message: z.string(),
stack: z.string().optional(),
}),
}),
]),
}),
])

export const runSyncAPIResponseSchema = z.object({
uuid: z.string(),
conversation: z.array(messageSchema),
response: chainCallResponseDtoSchema,
})
85 changes: 85 additions & 0 deletions apps/gateway/src/openApi/schemas/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { z } from '@hono/zod-openapi'

const ChainErrorDetailSchema = z.object({
entityUuid: z
.string()
.uuid()
.openapi({ description: 'UUID of the related entity' }),
entityType: z.string().openapi({ description: 'Type of the related entity' }),
})

const BaseErrorSchema = z.object({
name: z.string().openapi({ description: 'The name of the error' }),
errorCode: z.string().openapi({ description: 'The error code identifier' }),
message: z.string().openapi({ description: 'Detailed error message' }),
details: z
.object({})
.passthrough()
.optional()
.openapi({ description: 'Additional error details' }),
})

const HTTPExceptionErrorSchema = BaseErrorSchema.extend({
details: z.object({ cause: z.any().optional() }).optional(),
}).openapi({
description: 'Error response for HTTP exceptions',
example: {
name: 'HTTPException',
errorCode: 'HTTPException',
message: 'Not Found',
details: { cause: 'Resource not found' },
},
})

const UnprocessableEntityErrorSchema = BaseErrorSchema.extend({
details: z.any().optional(),
})
.and(z.object({ dbErrorRef: ChainErrorDetailSchema }).optional())
.openapi({
description: 'Error response for unprocessable entities',
example: {
name: 'DocumentRunError',
errorCode: 'SomeErrorCode',
message: 'Validation failed',
details: {},
dbErrorRef: {
entityUuid: '123e4567-e89b-12d3-a456-426614174000',
entityType: 'Document',
},
},
})

const BadRequestErrorSchema = BaseErrorSchema.extend({
details: z.any().optional(),
}).openapi({
description: 'Error response for Latitude-specific errors',
example: {
name: 'LatitudeError',
errorCode: 'LatitudeError',
message: 'A latitude-specific error occurred',
details: {},
},
})

const InternalServerErrorSchema = BaseErrorSchema.extend({
details: z
.object({
cause: z.any().optional(), // Adjust `z.any()` to a more specific schema if possible
})
.optional(),
}).openapi({
description: 'Error response for internal server errors',
example: {
name: 'InternalServerError',
errorCode: 'InternalServerError',
message: 'An unexpected error occurred',
details: { cause: 'Null reference exception' },
},
})

export {
HTTPExceptionErrorSchema,
UnprocessableEntityErrorSchema,
BadRequestErrorSchema,
InternalServerErrorSchema,
}
3 changes: 3 additions & 0 deletions apps/gateway/src/openApi/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './errors'
export * from './utils'
export * from './ai'
10 changes: 10 additions & 0 deletions apps/gateway/src/openApi/schemas/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from '@hono/zod-openapi'
import { LogSources } from '@latitude-data/core/browser'

export const internalInfoSchema = z.object({
__internal: z
.object({
source: z.nativeEnum(LogSources).optional(),
})
.optional(),
})
Loading

0 comments on commit acffc2c

Please sign in to comment.