Skip to content

Commit 21b25dc

Browse files
authored
brianyin/ajs-323-support-openai-strict-schema (#816)
1 parent b10503d commit 21b25dc

File tree

11 files changed

+541
-23
lines changed

11 files changed

+541
-23
lines changed

.changeset/weak-mangos-happen.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@livekit/agents-plugin-openai': patch
3+
'@livekit/agents-plugins-test': patch
4+
'@livekit/agents': patch
5+
---
6+
7+
Support strict tool schema for openai-competible model

agents/src/inference/llm.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface InferenceLLMOptions {
8888
apiKey: string;
8989
apiSecret: string;
9090
modelOptions: ChatCompletionOptions;
91+
strictToolSchema?: boolean;
9192
}
9293

9394
export interface GatewayOptions {
@@ -109,10 +110,19 @@ export class LLM extends llm.LLM {
109110
apiKey?: string;
110111
apiSecret?: string;
111112
modelOptions?: InferenceLLMOptions['modelOptions'];
113+
strictToolSchema?: boolean;
112114
}) {
113115
super();
114116

115-
const { model, provider, baseURL, apiKey, apiSecret, modelOptions } = opts;
117+
const {
118+
model,
119+
provider,
120+
baseURL,
121+
apiKey,
122+
apiSecret,
123+
modelOptions,
124+
strictToolSchema = false,
125+
} = opts;
116126

117127
const lkBaseURL = baseURL || process.env.LIVEKIT_INFERENCE_URL || DEFAULT_BASE_URL;
118128
const lkApiKey = apiKey || process.env.LIVEKIT_INFERENCE_API_KEY || process.env.LIVEKIT_API_KEY;
@@ -133,6 +143,7 @@ export class LLM extends llm.LLM {
133143
apiKey: lkApiKey,
134144
apiSecret: lkApiSecret,
135145
modelOptions: modelOptions || {},
146+
strictToolSchema,
136147
};
137148

138149
this.client = new OpenAI({
@@ -203,6 +214,7 @@ export class LLM extends llm.LLM {
203214
toolCtx,
204215
connOptions,
205216
modelOptions,
217+
strictToolSchema: this.opts.strictToolSchema ?? false, // default to false if not set
206218
gatewayOptions: {
207219
apiKey: this.opts.apiKey,
208220
apiSecret: this.opts.apiSecret,
@@ -217,6 +229,7 @@ export class LLMStream extends llm.LLMStream {
217229
private providerFmt: llm.ProviderFormat;
218230
private client: OpenAI;
219231
private modelOptions: Record<string, unknown>;
232+
private strictToolSchema: boolean;
220233

221234
private gatewayOptions?: GatewayOptions;
222235
private toolCallId?: string;
@@ -236,6 +249,7 @@ export class LLMStream extends llm.LLMStream {
236249
connOptions,
237250
modelOptions,
238251
providerFmt,
252+
strictToolSchema,
239253
}: {
240254
model: LLMModels;
241255
provider?: string;
@@ -246,6 +260,7 @@ export class LLMStream extends llm.LLMStream {
246260
connOptions: APIConnectOptions;
247261
modelOptions: Record<string, unknown>;
248262
providerFmt?: llm.ProviderFormat;
263+
strictToolSchema: boolean;
249264
},
250265
) {
251266
super(llm, { chatCtx, toolCtx, connOptions });
@@ -255,6 +270,7 @@ export class LLMStream extends llm.LLMStream {
255270
this.providerFmt = providerFmt || 'openai';
256271
this.modelOptions = modelOptions;
257272
this.model = model;
273+
this.strictToolSchema = strictToolSchema;
258274
}
259275

260276
protected async run(): Promise<void> {
@@ -269,16 +285,26 @@ export class LLMStream extends llm.LLMStream {
269285
)) as OpenAI.ChatCompletionMessageParam[];
270286

271287
const tools = this.toolCtx
272-
? Object.entries(this.toolCtx).map(([name, func]) => ({
273-
type: 'function' as const,
274-
function: {
275-
name,
276-
description: func.description,
277-
parameters: llm.toJsonSchema(
278-
func.parameters,
279-
) as unknown as OpenAI.Chat.Completions.ChatCompletionFunctionTool['function']['parameters'],
280-
},
281-
}))
288+
? Object.entries(this.toolCtx).map(([name, func]) => {
289+
const oaiParams = {
290+
type: 'function' as const,
291+
function: {
292+
name,
293+
description: func.description,
294+
parameters: llm.toJsonSchema(
295+
func.parameters,
296+
true,
297+
this.strictToolSchema,
298+
) as unknown as OpenAI.Chat.Completions.ChatCompletionFunctionTool['function']['parameters'],
299+
} as OpenAI.Chat.Completions.ChatCompletionFunctionTool['function'],
300+
};
301+
302+
if (this.strictToolSchema) {
303+
oaiParams.function.strict = true;
304+
}
305+
306+
return oaiParams;
307+
})
282308
: undefined;
283309

284310
const requestOptions: Record<string, unknown> = { ...this.modelOptions };

agents/src/llm/__snapshots__/zod-utils.test.ts.snap

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,221 @@ exports[`Zod Utils > zodSchemaToJsonSchema > Zod v4 schemas > should handle v4 s
339339
"type": "object",
340340
}
341341
`;
342+
343+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should handle arrays in strict mode 1`] = `
344+
{
345+
"$schema": "http://json-schema.org/draft-07/schema#",
346+
"additionalProperties": false,
347+
"properties": {
348+
"numbers": {
349+
"items": {
350+
"type": "number",
351+
},
352+
"type": "array",
353+
},
354+
"tags": {
355+
"items": {
356+
"type": "string",
357+
},
358+
"type": "array",
359+
},
360+
},
361+
"required": [
362+
"tags",
363+
"numbers",
364+
],
365+
"type": "object",
366+
}
367+
`;
368+
369+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should handle default values in strict mode 1`] = `
370+
{
371+
"$schema": "http://json-schema.org/draft-07/schema#",
372+
"additionalProperties": false,
373+
"properties": {
374+
"active": {
375+
"default": true,
376+
"type": "boolean",
377+
},
378+
"name": {
379+
"type": "string",
380+
},
381+
"role": {
382+
"default": "user",
383+
"type": "string",
384+
},
385+
},
386+
"required": [
387+
"name",
388+
"role",
389+
"active",
390+
],
391+
"type": "object",
392+
}
393+
`;
394+
395+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should handle nested objects in strict mode 1`] = `
396+
{
397+
"$schema": "http://json-schema.org/draft-07/schema#",
398+
"additionalProperties": false,
399+
"properties": {
400+
"metadata": {
401+
"additionalProperties": false,
402+
"properties": {
403+
"created": {
404+
"type": "string",
405+
},
406+
},
407+
"required": [
408+
"created",
409+
],
410+
"type": "object",
411+
},
412+
"user": {
413+
"additionalProperties": false,
414+
"properties": {
415+
"email": {
416+
"anyOf": [
417+
{
418+
"type": "string",
419+
},
420+
{
421+
"type": "null",
422+
},
423+
],
424+
},
425+
"name": {
426+
"type": "string",
427+
},
428+
},
429+
"required": [
430+
"name",
431+
"email",
432+
],
433+
"type": "object",
434+
},
435+
},
436+
"required": [
437+
"user",
438+
"metadata",
439+
],
440+
"type": "object",
441+
}
442+
`;
443+
444+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should handle nullable fields in strict mode 1`] = `
445+
{
446+
"$schema": "http://json-schema.org/draft-07/schema#",
447+
"additionalProperties": false,
448+
"properties": {
449+
"optional": {
450+
"anyOf": [
451+
{
452+
"type": "string",
453+
},
454+
{
455+
"type": "null",
456+
},
457+
],
458+
},
459+
"required": {
460+
"type": "string",
461+
},
462+
},
463+
"required": [
464+
"required",
465+
"optional",
466+
],
467+
"type": "object",
468+
}
469+
`;
470+
471+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should handle optional fields in strict mode 1`] = `
472+
{
473+
"$schema": "http://json-schema.org/draft-07/schema#",
474+
"additionalProperties": false,
475+
"properties": {
476+
"optional": {
477+
"anyOf": [
478+
{
479+
"type": "string",
480+
},
481+
{
482+
"type": "null",
483+
},
484+
],
485+
},
486+
"required": {
487+
"type": "string",
488+
},
489+
},
490+
"required": [
491+
"required",
492+
"optional",
493+
],
494+
"type": "object",
495+
}
496+
`;
497+
498+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should handle v3 schemas in strict mode 1`] = `
499+
{
500+
"$schema": "https://json-schema.org/draft/2019-09/schema#",
501+
"additionalProperties": false,
502+
"properties": {
503+
"age": {
504+
"type": [
505+
"number",
506+
"null",
507+
],
508+
},
509+
"name": {
510+
"type": "string",
511+
},
512+
},
513+
"required": [
514+
"name",
515+
"age",
516+
],
517+
"type": "object",
518+
}
519+
`;
520+
521+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should produce standard JSON schema with strict: false 1`] = `
522+
{
523+
"$schema": "http://json-schema.org/draft-07/schema#",
524+
"additionalProperties": false,
525+
"properties": {
526+
"age": {
527+
"type": "number",
528+
},
529+
"name": {
530+
"type": "string",
531+
},
532+
},
533+
"required": [
534+
"name",
535+
"age",
536+
],
537+
"type": "object",
538+
}
539+
`;
540+
541+
exports[`Zod Utils > zodSchemaToJsonSchema > strict parameter > should produce strict JSON schema with strict: true 1`] = `
542+
{
543+
"$schema": "http://json-schema.org/draft-07/schema#",
544+
"additionalProperties": false,
545+
"properties": {
546+
"age": {
547+
"type": "number",
548+
},
549+
"name": {
550+
"type": "string",
551+
},
552+
},
553+
"required": [
554+
"name",
555+
"age",
556+
],
557+
"type": "object",
558+
}
559+
`;

agents/src/llm/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,14 @@ export function computeChatCtxDiff(oldCtx: ChatContext, newCtx: ChatContext): Di
323323
};
324324
}
325325

326-
export function toJsonSchema(schema: ToolInputSchema<any>, isOpenai: boolean = true): JSONSchema7 {
326+
export function toJsonSchema(
327+
schema: ToolInputSchema<any>,
328+
isOpenai: boolean = true,
329+
strict: boolean = false,
330+
): JSONSchema7 {
327331
if (isZodSchema(schema)) {
328-
return zodSchemaToJsonSchema(schema, isOpenai);
332+
return zodSchemaToJsonSchema(schema, isOpenai, strict);
329333
}
334+
330335
return schema as JSONSchema7;
331336
}

0 commit comments

Comments
 (0)