Skip to content

Commit e00ebfe

Browse files
authored
fix: add google json mode support + additionalProperties (#5464)
* fix: add json mode support + additionalProperties * copilot PR comments applied
1 parent f0712f9 commit e00ebfe

File tree

4 files changed

+550
-1
lines changed

4 files changed

+550
-1
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
import { toGoogle } from "../../llm-mapper/transform/providers/openai/request/toGoogle";
3+
4+
describe("toGoogle response_format transformation", () => {
5+
it("should convert response_format with json_schema to Google's responseMimeType and responseSchema", () => {
6+
const openAIRequest = {
7+
model: "gemini-2.5-flash",
8+
messages: [
9+
{
10+
role: "user" as const,
11+
content: "Generate a JSON object with title and bullets fields",
12+
},
13+
],
14+
response_format: {
15+
type: "json_schema" as const,
16+
json_schema: {
17+
name: "article",
18+
schema: {
19+
type: "object",
20+
properties: {
21+
title: { type: "string" },
22+
bullets: {
23+
type: "array",
24+
items: { type: "string" },
25+
},
26+
},
27+
required: ["title", "bullets"],
28+
},
29+
},
30+
},
31+
};
32+
33+
const result = toGoogle(openAIRequest);
34+
35+
// Should have generationConfig with responseMimeType and responseSchema
36+
expect(result.generationConfig).toBeDefined();
37+
expect(result.generationConfig?.responseMimeType).toBe("application/json");
38+
expect(result.generationConfig?.responseSchema).toBeDefined();
39+
expect(result.generationConfig?.responseSchema).toEqual({
40+
type: "object",
41+
properties: {
42+
title: { type: "string" },
43+
bullets: {
44+
type: "array",
45+
items: { type: "string" },
46+
},
47+
},
48+
required: ["title", "bullets"],
49+
});
50+
});
51+
52+
it("should convert response_format with type json_object to responseMimeType application/json", () => {
53+
const openAIRequest = {
54+
model: "gemini-2.5-flash",
55+
messages: [
56+
{
57+
role: "user" as const,
58+
content: "Return a JSON object",
59+
},
60+
],
61+
response_format: {
62+
type: "json_object" as const,
63+
},
64+
};
65+
66+
const result = toGoogle(openAIRequest);
67+
68+
expect(result.generationConfig).toBeDefined();
69+
expect(result.generationConfig?.responseMimeType).toBe("application/json");
70+
// No responseSchema since json_object doesn't provide one
71+
expect(result.generationConfig?.responseSchema).toBeUndefined();
72+
});
73+
74+
it("should handle response_format with type text (default behavior)", () => {
75+
const openAIRequest = {
76+
model: "gemini-2.5-flash",
77+
messages: [
78+
{
79+
role: "user" as const,
80+
content: "Just return plain text",
81+
},
82+
],
83+
response_format: {
84+
type: "text" as const,
85+
},
86+
};
87+
88+
const result = toGoogle(openAIRequest);
89+
90+
// Should NOT set responseMimeType for text (default behavior)
91+
expect(result.generationConfig?.responseMimeType).toBeUndefined();
92+
});
93+
94+
it("should strip additionalProperties from json_schema when converting", () => {
95+
const openAIRequest = {
96+
model: "gemini-2.5-flash",
97+
messages: [
98+
{
99+
role: "user" as const,
100+
content: "Generate structured output",
101+
},
102+
],
103+
response_format: {
104+
type: "json_schema" as const,
105+
json_schema: {
106+
name: "result",
107+
strict: true,
108+
schema: {
109+
type: "object",
110+
properties: {
111+
name: { type: "string" },
112+
nested: {
113+
type: "object",
114+
properties: {
115+
value: { type: "number" },
116+
},
117+
additionalProperties: false,
118+
},
119+
},
120+
required: ["name"],
121+
additionalProperties: false,
122+
},
123+
},
124+
},
125+
};
126+
127+
const result = toGoogle(openAIRequest);
128+
129+
expect(result.generationConfig?.responseSchema).toBeDefined();
130+
// additionalProperties should be stripped
131+
expect(result.generationConfig?.responseSchema?.additionalProperties).toBeUndefined();
132+
expect(result.generationConfig?.responseSchema?.properties?.nested?.additionalProperties).toBeUndefined();
133+
});
134+
135+
it("should handle Vercel AI SDK generateObject style request", () => {
136+
// This simulates what Vercel AI SDK sends for generateObject
137+
const openAIRequest = {
138+
model: "gemini-2.5-flash",
139+
messages: [
140+
{
141+
role: "system" as const,
142+
content: "You are a helpful assistant that generates structured data.",
143+
},
144+
{
145+
role: "user" as const,
146+
content: "Generate an article about AI",
147+
},
148+
],
149+
response_format: {
150+
type: "json_schema" as const,
151+
json_schema: {
152+
name: "article",
153+
schema: {
154+
type: "object",
155+
properties: {
156+
title: { type: "string", description: "The article title" },
157+
content: { type: "string", description: "The article content" },
158+
tags: {
159+
type: "array",
160+
items: { type: "string" },
161+
description: "Tags for the article",
162+
},
163+
},
164+
required: ["title", "content", "tags"],
165+
additionalProperties: false,
166+
},
167+
strict: true,
168+
},
169+
},
170+
};
171+
172+
const result = toGoogle(openAIRequest);
173+
174+
// Should properly convert to Google format
175+
expect(result.generationConfig).toBeDefined();
176+
expect(result.generationConfig?.responseMimeType).toBe("application/json");
177+
expect(result.generationConfig?.responseSchema).toBeDefined();
178+
179+
// Schema should be present without additionalProperties
180+
expect(result.generationConfig?.responseSchema?.type).toBe("object");
181+
expect(result.generationConfig?.responseSchema?.properties?.title).toEqual({
182+
type: "string",
183+
description: "The article title",
184+
});
185+
expect(result.generationConfig?.responseSchema?.additionalProperties).toBeUndefined();
186+
});
187+
});

0 commit comments

Comments
 (0)