-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathstructChat.ts
More file actions
174 lines (155 loc) · 6.31 KB
/
Copy pathstructChat.ts
File metadata and controls
174 lines (155 loc) · 6.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import { z } from 'zod';
import { z as zv4 } from 'zod/v4';
import { zodToJsonSchema } from "zod-to-json-schema";
import { ResponseFormat } from "../models/components/responseformat.js";
import * as components from "../models/components/index.js";
/**
* Recursively makes a JSON schema strict-mode compatible:
* - Sets additionalProperties: false on all objects
* - Makes optional properties required with nullable type
* Mirrors Python SDK's rec_strict_json_schema + optional handling.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- operating on raw JSON schema objects
function makeStrictJsonSchema(node: any): any {
if (typeof node !== 'object' || node === null) return node;
const result = { ...node };
if (result.type === 'object' && result.properties && typeof result.properties === 'object') {
const required = new Set<string>(Array.isArray(result.required) ? result.required : []);
const props: Record<string, any> = {};
for (const [key, value] of Object.entries(result.properties)) {
const processed = makeStrictJsonSchema(value);
if (!required.has(key)) {
required.add(key);
if (Array.isArray(processed.type)) {
if (!processed.type.includes('null')) props[key] = { ...processed, type: [...processed.type, 'null'] };
else props[key] = processed;
} else if (Array.isArray(processed.anyOf)) {
if (!processed.anyOf.some((s: any) => s.type === 'null'))
props[key] = { ...processed, anyOf: [...processed.anyOf, { type: 'null' }] };
else props[key] = processed;
} else if (processed.type) {
props[key] = { ...processed, type: [processed.type, 'null'] };
} else {
props[key] = processed;
}
} else {
props[key] = processed;
}
}
result.properties = props;
result.required = [...required];
result.additionalProperties = false;
}
if (result.items) result.items = makeStrictJsonSchema(result.items);
for (const key of ['anyOf', 'oneOf', 'allOf']) {
if (Array.isArray(result[key]))
result[key] = result[key].map(makeStrictJsonSchema);
}
for (const key of ['$defs', 'definitions']) {
if (result[key] && typeof result[key] === 'object') {
const defs: Record<string, any> = {};
for (const [k, v] of Object.entries(result[key]))
defs[k] = makeStrictJsonSchema(v);
result[key] = defs;
}
}
return result;
}
function toJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
let jsonSchema: Record<string, unknown>;
// Detect Zod v4 schemas by checking for _zod property (v3 schemas have _def only)
if ('_zod' in schema) {
jsonSchema = zv4.toJSONSchema(schema as any) as Record<string, unknown>;
} else {
jsonSchema = zodToJsonSchema(schema, { target: "openAi" }) as Record<string, unknown>;
}
delete jsonSchema['$schema'];
return makeStrictJsonSchema(jsonSchema);
}
export function transformToChatCompletionRequest<T extends z.ZodTypeAny>(
parsedRequest: ParsedChatCompletionRequest<T>
): components.ChatCompletionRequest {
const { responseFormat, ...rest } = parsedRequest;
// Transform responseFormat from z.ZodType to ResponseFormat
const transformedResponseFormat: ResponseFormat | undefined = responseFormatFromZodObject(responseFormat);
return {
...rest,
...(transformedResponseFormat ? { responseFormat: transformedResponseFormat } : {}),
} as components.ChatCompletionRequest;
}
export type ParsedChatCompletionRequest<T extends z.ZodTypeAny> = Omit<components.ChatCompletionRequest, 'responseFormat'> & {
responseFormat: T;
};
export type ParsedAssistantMessage<T extends z.ZodTypeAny> = components.AssistantMessage & {
parsed?: z.infer<T> | undefined;
}
export type ParsedChatCompletionChoice<T extends z.ZodTypeAny> = Omit<components.ChatCompletionChoice, 'message'> & {
message?: ParsedAssistantMessage<T> | undefined;
};
export type ParsedChatCompletionResponse<T extends z.ZodTypeAny> = Omit<components.ChatCompletionResponse, 'choices'> & {
choices?: Array<ParsedChatCompletionChoice<T>> | undefined;
};
export function convertToParsedChatCompletionResponse<T extends z.ZodTypeAny>(response: components.ChatCompletionResponse, responseFormat: T): ParsedChatCompletionResponse<T> {
if (response.choices === undefined || response.choices.length === 0) {
return {
...response,
choices: response.choices === undefined ? undefined : [],
};
}
const parsedChoices: Array<ParsedChatCompletionChoice<T>> = [];
for (const _choice of response.choices) {
if (_choice.message === null || typeof _choice.message === 'undefined') {
parsedChoices.push({..._choice, message: undefined});
} else if (
_choice.message.content !== null
&& typeof _choice.message.content !== 'undefined'
&& !Array.isArray(_choice.message.content)
) {
let parsed: z.infer<T> | undefined;
try {
parsed = responseFormat.safeParse(JSON.parse(_choice.message.content)).data;
} catch {
// JSON.parse can throw if the model returns malformed JSON
// (e.g. truncated output when finish_reason is "length").
// Leave parsed as undefined so callers can detect the failure.
parsed = undefined;
}
parsedChoices.push({
..._choice,
message: {
..._choice.message,
parsed,
},
});
} else {
// content is null, undefined, or an array of content chunks;
// preserve the choice without a parsed field.
parsedChoices.push({
..._choice,
message: {
..._choice.message,
parsed: undefined,
},
});
}
}
return {
...response,
choices: parsedChoices,
};
}
// Function to convert Zod schema to strict JSON schema
export function responseFormatFromZodObject<T extends z.ZodTypeAny>(responseFormat: T): ResponseFormat {
const responseJsonSchema = toJsonSchema(responseFormat);
// It is not possible to get the variable name of a Zod object at runtime in TypeScript so we're using a placeholder name.
// This has not impact on the parsing as the initial Zod object is used to parse the response.
const placeholderName = "placeholderName"
return {
type: "json_schema",
jsonSchema: {
name: placeholderName,
schemaDefinition: responseJsonSchema,
strict: true,
},
};
}