Skip to content

Commit c629eae

Browse files
authored
Fix gateway swagger (#1884)
OpenAPI does not support zod unknown types (any/unknown). To support them, you have to add an openapi argument with metadata for the OpenAPI parser to understand what that type is. This fix adds these openapi tags, making the API docs work again
1 parent 1f1708b commit c629eae

File tree

9 files changed

+202
-26
lines changed

9 files changed

+202
-26
lines changed

apps/gateway/src/openApi/schemas/ai.ts

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
StreamEventTypes,
55
traceContextSchema,
66
} from '@latitude-data/constants'
7-
import { messageSchema } from '@latitude-data/core/constants'
87

98
export const languageModelUsageSchema = z.object({
109
completionTokens: z.number().optional(),
@@ -15,11 +14,131 @@ export const languageModelUsageSchema = z.object({
1514
export const toolCallSchema = z.object({
1615
id: z.string(),
1716
name: z.string(),
18-
arguments: z.record(z.string(), z.any()),
17+
arguments: z.record(z.string(), z.unknown()).openapi({
18+
type: 'object',
19+
additionalProperties: true,
20+
description: 'Tool call arguments as key-value pairs',
21+
}),
22+
})
23+
24+
const textContentSchema = z.object({
25+
type: z.literal('text'),
26+
text: z.string(),
27+
})
28+
29+
const imageContentSchema = z.object({
30+
type: z.literal('image'),
31+
image: z
32+
.string()
33+
.or(z.instanceof(Uint8Array))
34+
.or(z.instanceof(ArrayBuffer))
35+
.or(z.instanceof(URL))
36+
.openapi({
37+
type: 'string',
38+
description:
39+
'Image data as string (URL, base64), Uint8Array, ArrayBuffer, or URL object',
40+
format: 'binary',
41+
}),
42+
mimeType: z.string().optional(),
43+
})
44+
45+
const fileContentSchema = z.object({
46+
type: z.literal('file'),
47+
file: z
48+
.string()
49+
.or(z.instanceof(Uint8Array))
50+
.or(z.instanceof(ArrayBuffer))
51+
.or(z.instanceof(URL))
52+
.openapi({
53+
type: 'string',
54+
description:
55+
'File data as string (URL, base64), Uint8Array, ArrayBuffer, or URL object',
56+
format: 'binary',
57+
}),
58+
mimeType: z.string(),
59+
})
60+
61+
const toolCallContentSchema = z.object({
62+
type: z.literal('tool-call'),
63+
toolCallId: z.string(),
64+
toolName: z.string(),
65+
args: z.record(z.string(), z.unknown()).openapi({
66+
type: 'object',
67+
additionalProperties: true,
68+
description: 'Tool call arguments as key-value pairs',
69+
}),
1970
})
2071

21-
export const configSchema = z.record(z.string(), z.any())
22-
export const providerLogSchema = z.record(z.string(), z.any())
72+
const toolResultContentSchema = z.object({
73+
type: z.literal('tool-result'),
74+
toolCallId: z.string(),
75+
toolName: z.string(),
76+
result: z.unknown().openapi({
77+
type: 'object',
78+
additionalProperties: true,
79+
description: 'Tool result as any JSON-serializable value',
80+
}),
81+
isError: z.boolean().optional(),
82+
})
83+
84+
export const messageSchema = z
85+
.object({
86+
role: z.literal('system'),
87+
content: z.string().or(z.array(textContentSchema)),
88+
})
89+
.or(
90+
z.object({
91+
role: z.literal('user'),
92+
content: z
93+
.string()
94+
.or(
95+
z.array(
96+
textContentSchema.or(imageContentSchema).or(fileContentSchema),
97+
),
98+
),
99+
name: z.string().optional(),
100+
}),
101+
)
102+
.or(
103+
z.object({
104+
role: z.literal('assistant'),
105+
content: z
106+
.string()
107+
.or(z.array(textContentSchema.or(toolCallContentSchema))),
108+
toolCalls: z
109+
.array(
110+
z.object({
111+
id: z.string(),
112+
name: z.string(),
113+
arguments: z.record(z.string(), z.unknown()).openapi({
114+
type: 'object',
115+
additionalProperties: true,
116+
description: 'Tool call arguments as key-value pairs',
117+
}),
118+
}),
119+
)
120+
.optional(),
121+
}),
122+
)
123+
.or(
124+
z.object({
125+
role: z.literal('tool'),
126+
content: z.array(toolResultContentSchema),
127+
}),
128+
)
129+
130+
export const messagesSchema = z.array(messageSchema)
131+
132+
export const configSchema = z.record(z.string(), z.any()).openapi({
133+
type: 'object',
134+
additionalProperties: true,
135+
description: 'Configuration as key-value pairs',
136+
})
137+
export const providerLogSchema = z.record(z.string(), z.any()).openapi({
138+
type: 'object',
139+
additionalProperties: true,
140+
description: 'Provider log as key-value pairs',
141+
})
23142
export const chainStepResponseSchema = z.discriminatedUnion('streamType', [
24143
z.object({
25144
streamType: z.literal('text'),
@@ -31,7 +150,11 @@ export const chainStepResponseSchema = z.discriminatedUnion('streamType', [
31150
}),
32151
z.object({
33152
streamType: z.literal('object'),
34-
object: z.any(),
153+
object: z.any().openapi({
154+
type: 'object',
155+
additionalProperties: true,
156+
description: 'Stream object data (any JSON-serializable value)',
157+
}),
35158
text: z.string(),
36159
usage: languageModelUsageSchema,
37160
documentLogUuid: z.string().optional(),
@@ -58,7 +181,11 @@ export const chainEventDtoResponseSchema = z.discriminatedUnion('streamType', [
58181
export const legacyChainEventDtoSchema = z.discriminatedUnion('event', [
59182
z.object({
60183
event: z.literal(StreamEventTypes.Provider),
61-
data: z.record(z.string(), z.any()),
184+
data: z.record(z.string(), z.any()).openapi({
185+
type: 'object',
186+
additionalProperties: true,
187+
description: 'Provider event data as key-value pairs',
188+
}),
62189
}),
63190
z.object({
64191
event: z.literal(StreamEventTypes.Latitude),
@@ -79,7 +206,11 @@ export const legacyChainEventDtoSchema = z.discriminatedUnion('event', [
79206
type: z.literal(LegacyChainEventTypes.Complete),
80207
config: configSchema,
81208
messages: z.array(messageSchema).optional(),
82-
object: z.record(z.string(), z.any()).optional(),
209+
object: z.record(z.string(), z.any()).optional().openapi({
210+
type: 'object',
211+
additionalProperties: true,
212+
description: 'Complete event object data as key-value pairs',
213+
}),
83214
response: chainEventDtoResponseSchema,
84215
uuid: z.string().optional(),
85216
}),
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from '@hono/zod-openapi'
2-
import { messageSchema } from '@latitude-data/core/constants'
2+
import { messageSchema } from './ai'
33

44
export const conversationPresenterSchema = z.object({
5-
uuid: z.string(),
5+
uuid: z.string().openapi({ description: 'Conversation UUID' }),
66
conversation: z.array(messageSchema),
77
})

apps/gateway/src/openApi/schemas/errors.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@ const BaseErrorSchema = z.object({
1212
name: z.string().openapi({ description: 'The name of the error' }),
1313
errorCode: z.string().openapi({ description: 'The error code identifier' }),
1414
message: z.string().openapi({ description: 'Detailed error message' }),
15-
details: z
16-
.object({})
17-
.passthrough()
18-
.optional()
19-
.openapi({ description: 'Additional error details' }),
15+
details: z.object({}).passthrough().optional().openapi({
16+
type: 'object',
17+
additionalProperties: true,
18+
description: 'Additional error details',
19+
}),
2020
})
2121

2222
const HTTPExceptionErrorSchema = BaseErrorSchema.extend({
23-
details: z.object({ cause: z.any().optional() }).optional(),
23+
details: z
24+
.object({
25+
cause: z
26+
.any()
27+
.optional()
28+
.openapi({ type: 'object', additionalProperties: true }),
29+
})
30+
.optional(),
2431
}).openapi({
2532
description: 'Error response for HTTP exceptions',
2633
example: {
@@ -32,7 +39,10 @@ const HTTPExceptionErrorSchema = BaseErrorSchema.extend({
3239
})
3340

3441
const UnprocessableEntityErrorSchema = BaseErrorSchema.extend({
35-
details: z.any().optional(),
42+
details: z
43+
.any()
44+
.optional()
45+
.openapi({ type: 'object', additionalProperties: true }),
3646
})
3747
.and(z.object({ dbErrorRef: ChainErrorDetailSchema }).optional())
3848
.openapi({
@@ -50,7 +60,10 @@ const UnprocessableEntityErrorSchema = BaseErrorSchema.extend({
5060
})
5161

5262
const BadRequestErrorSchema = BaseErrorSchema.extend({
53-
details: z.any().optional(),
63+
details: z
64+
.any()
65+
.optional()
66+
.openapi({ type: 'object', additionalProperties: true }),
5467
}).openapi({
5568
description: 'Error response for Latitude-specific errors',
5669
example: {
@@ -64,7 +77,10 @@ const BadRequestErrorSchema = BaseErrorSchema.extend({
6477
const InternalServerErrorSchema = BaseErrorSchema.extend({
6578
details: z
6679
.object({
67-
cause: z.any().optional(), // Adjust `z.any()` to a more specific schema if possible
80+
cause: z
81+
.any()
82+
.optional()
83+
.openapi({ type: 'object', additionalProperties: true }),
6884
})
6985
.optional(),
7086
}).openapi({

apps/gateway/src/presenters/documentPresenter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ export const documentPresenterSchema = z.object({
1414
path: z.string(),
1515
content: z.string(),
1616
contentHash: z.string().optional(),
17-
config: z.record(z.string(), z.any()),
17+
config: z.record(z.string(), z.any()).openapi({
18+
type: 'object',
19+
additionalProperties: true,
20+
description: 'Document configuration as key-value pairs',
21+
}),
1822
parameters: z.record(z.string(), z.object({ type: z.enum(ParameterType) })),
1923
provider: z.enum(Providers).optional(),
2024
})

apps/gateway/src/routes/api/v3/conversations/chat/chat.route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import {
44
internalInfoSchema,
55
legacyChainEventDtoSchema,
66
runSyncAPIResponseSchema,
7+
messageSchema,
78
} from '$/openApi/schemas'
89
import { ROUTES } from '$/routes'
910
import { conversationsParamsSchema } from '$/routes/api/v3/conversations/paramsSchema'
1011
import { createRoute, z } from '@hono/zod-openapi'
11-
import { messageSchema } from '@latitude-data/core/constants'
1212
import { traceContextSchema } from '@latitude-data/constants/tracing'
1313

1414
export const chatRoute = createRoute({

apps/gateway/src/routes/api/v3/projects/versions/documents/logs/create.route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import http from '$/common/http'
22
import { GENERIC_ERROR_RESPONSES } from '$/openApi/responses/errorResponses'
3+
import { messageSchema } from '$/openApi/schemas'
34
import { ROUTES } from '$/routes'
45
import { createRoute, z } from '@hono/zod-openapi'
5-
import { LogSources, messageSchema } from '@latitude-data/core/constants'
6+
import { LogSources } from '@latitude-data/core/constants'
67

78
const documentLogSchema = z.object({
89
id: z.number(),
910
uuid: z.string(),
1011
commitId: z.number(),
1112
resolvedContent: z.string(),
1213
contentHash: z.string(),
13-
parameters: z.record(z.string(), z.any()),
14+
parameters: z.record(z.string(), z.any()).openapi({
15+
type: 'object',
16+
additionalProperties: true,
17+
description: 'Document parameters as key-value pairs',
18+
}),
1419
customIdentifier: z.string().optional(),
1520
duration: z.number().optional(),
1621
source: z.enum(LogSources),

apps/gateway/src/routes/api/v3/projects/versions/documents/run/run.route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ export const runRoute = createRoute({
2424
path: z.string(),
2525
stream: z.boolean().default(false),
2626
customIdentifier: z.string().optional(),
27-
parameters: z.record(z.string(), z.any()).optional().default({}),
27+
parameters: z
28+
.record(z.string(), z.any())
29+
.optional()
30+
.default({})
31+
.openapi({
32+
type: 'object',
33+
additionalProperties: true,
34+
description: 'Document parameters as key-value pairs',
35+
}),
2836
tools: z.array(z.string()).optional().default([]),
2937
userMessage: z.string().optional(),
3038
background: z.boolean().optional(),

apps/gateway/src/routes/api/v3/tools/results/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { z } from '@hono/zod-openapi'
66

77
export const clientToolResultBodySchema = z.object({
88
toolCallId: z.string(),
9-
result: z.any(),
9+
result: z.any().openapi({
10+
description: 'Tool execution result (any JSON-serializable value)',
11+
type: 'object',
12+
additionalProperties: true,
13+
}),
1014
isError: z.boolean().default(false),
1115
})
1216

apps/gateway/src/routes/webhook/integration/webhook.route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ export const integrationLegacyWebhookRoute = createRoute({
1515
body: {
1616
content: {
1717
[http.MediaTypes.JSON]: {
18-
schema: z.any(),
18+
schema: z.any().openapi({
19+
description: 'Webhook payload (any JSON-serializable value)',
20+
type: 'object',
21+
additionalProperties: true,
22+
}),
1923
},
2024
},
2125
},
@@ -38,7 +42,11 @@ export const integrationWebhookRoute = createRoute({
3842
body: {
3943
content: {
4044
[http.MediaTypes.JSON]: {
41-
schema: z.any(),
45+
schema: z.any().openapi({
46+
description: 'Webhook payload (any JSON-serializable value)',
47+
type: 'object',
48+
additionalProperties: true,
49+
}),
4250
},
4351
},
4452
},

0 commit comments

Comments
 (0)