Skip to content

Commit 99a6194

Browse files
committed
feat! : Support invoke with structured output in VercelAI provider
1 parent 8d57904 commit 99a6194

File tree

3 files changed

+258
-4
lines changed

3 files changed

+258
-4
lines changed

packages/ai-providers/server-ai-vercel/__tests__/VercelProvider.test.ts

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { generateText } from 'ai';
1+
import { generateObject, generateText } from 'ai';
22

33
import { VercelProvider } from '../src/VercelProvider';
44

55
// Mock Vercel AI SDK
66
jest.mock('ai', () => ({
77
generateText: jest.fn(),
8+
generateObject: jest.fn(),
89
}));
910

1011
describe('VercelProvider', () => {
@@ -116,6 +117,76 @@ describe('VercelProvider', () => {
116117
});
117118
});
118119

120+
describe('convertToZodSchema', () => {
121+
it('converts simple object structure to Zod schema', () => {
122+
const responseStructure = {
123+
name: 'string',
124+
age: 0,
125+
isActive: true,
126+
};
127+
128+
const schema = VercelProvider.convertToZodSchema(responseStructure);
129+
130+
expect(schema).toBeDefined();
131+
expect(typeof schema.parse).toBe('function');
132+
});
133+
134+
it('converts nested object structure to Zod schema', () => {
135+
const responseStructure = {
136+
user: {
137+
name: 'string',
138+
age: 0,
139+
},
140+
settings: {
141+
theme: 'string',
142+
notifications: true,
143+
},
144+
};
145+
146+
const schema = VercelProvider.convertToZodSchema(responseStructure);
147+
148+
expect(schema).toBeDefined();
149+
expect(typeof schema.parse).toBe('function');
150+
});
151+
152+
it('converts array structure to Zod schema', () => {
153+
const responseStructure = {
154+
items: ['string'],
155+
numbers: [0],
156+
booleans: [true],
157+
};
158+
159+
const schema = VercelProvider.convertToZodSchema(responseStructure);
160+
161+
expect(schema).toBeDefined();
162+
expect(typeof schema.parse).toBe('function');
163+
});
164+
165+
it('handles empty array structure', () => {
166+
const responseStructure = {
167+
items: [],
168+
};
169+
170+
const schema = VercelProvider.convertToZodSchema(responseStructure);
171+
172+
expect(schema).toBeDefined();
173+
expect(typeof schema.parse).toBe('function');
174+
});
175+
176+
it('handles null and undefined values', () => {
177+
const responseStructure = {
178+
nullable: null,
179+
undefined,
180+
string: 'string',
181+
};
182+
183+
const schema = VercelProvider.convertToZodSchema(responseStructure);
184+
185+
expect(schema).toBeDefined();
186+
expect(typeof schema.parse).toBe('function');
187+
});
188+
});
189+
119190
describe('invokeModel', () => {
120191
it('invokes Vercel AI generateText and returns response', async () => {
121192
const mockResponse = {
@@ -178,6 +249,91 @@ describe('VercelProvider', () => {
178249
});
179250
});
180251

252+
describe('invokeStructuredModel', () => {
253+
it('invokes Vercel AI generateObject and returns structured response', async () => {
254+
const mockResponse = {
255+
object: {
256+
name: 'John Doe',
257+
age: 30,
258+
isActive: true,
259+
},
260+
usage: {
261+
promptTokens: 10,
262+
completionTokens: 15,
263+
totalTokens: 25,
264+
},
265+
};
266+
267+
(generateObject as jest.Mock).mockResolvedValue(mockResponse);
268+
269+
const messages = [{ role: 'user' as const, content: 'Generate user data' }];
270+
const responseStructure = {
271+
name: 'string',
272+
age: 0,
273+
isActive: true,
274+
};
275+
276+
const result = await provider.invokeStructuredModel(messages, responseStructure);
277+
278+
expect(generateObject).toHaveBeenCalledWith({
279+
model: mockModel,
280+
messages: [{ role: 'user', content: 'Generate user data' }],
281+
schema: expect.any(Object), // Zod schema
282+
});
283+
284+
expect(result).toEqual({
285+
data: {
286+
name: 'John Doe',
287+
age: 30,
288+
isActive: true,
289+
},
290+
rawResponse: JSON.stringify({
291+
name: 'John Doe',
292+
age: 30,
293+
isActive: true,
294+
}),
295+
metrics: {
296+
success: true,
297+
usage: {
298+
total: 25,
299+
input: 10,
300+
output: 15,
301+
},
302+
},
303+
});
304+
});
305+
306+
it('handles structured response without usage data', async () => {
307+
const mockResponse = {
308+
object: {
309+
result: 'success',
310+
},
311+
};
312+
313+
(generateObject as jest.Mock).mockResolvedValue(mockResponse);
314+
315+
const messages = [{ role: 'user' as const, content: 'Generate result' }];
316+
const responseStructure = {
317+
result: 'string',
318+
};
319+
320+
const result = await provider.invokeStructuredModel(messages, responseStructure);
321+
322+
expect(result).toEqual({
323+
data: {
324+
result: 'success',
325+
},
326+
rawResponse: JSON.stringify({
327+
result: 'success',
328+
}),
329+
metrics: {
330+
success: true,
331+
usage: undefined,
332+
},
333+
});
334+
});
335+
});
336+
181337
describe('getModel', () => {
182338
it('returns the underlying Vercel AI model', () => {
183339
const model = provider.getModel();

packages/ai-providers/server-ai-vercel/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"jest": "^29.6.1",
4949
"prettier": "^3.0.0",
5050
"ts-jest": "^29.1.1",
51-
"typescript": "5.1.6"
51+
"typescript": "5.1.6",
52+
"zod": "^3.22.0"
5253
},
5354
"peerDependencies": {
5455
"@ai-sdk/anthropic": "^2.0.0",
@@ -57,7 +58,8 @@
5758
"@ai-sdk/mistral": "^2.0.0",
5859
"@ai-sdk/openai": "^2.0.0",
5960
"@launchdarkly/server-sdk-ai": "^0.12.2",
60-
"ai": "^4.0.0 || ^5.0.0"
61+
"ai": "^4.0.0 || ^5.0.0",
62+
"zod": "^3.22.0"
6163
},
6264
"peerDependenciesMeta": {
6365
"@ai-sdk/anthropic": {

packages/ai-providers/server-ai-vercel/src/VercelProvider.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { generateText, LanguageModel } from 'ai';
1+
import { generateObject, generateText, LanguageModel } from 'ai';
2+
import { z } from 'zod';
23

34
import { AIProvider } from '@launchdarkly/server-sdk-ai';
45
import type {
@@ -8,6 +9,7 @@ import type {
89
LDLogger,
910
LDMessage,
1011
LDTokenUsage,
12+
StructuredResponse,
1113
} from '@launchdarkly/server-sdk-ai';
1214

1315
/**
@@ -69,6 +71,34 @@ export class VercelProvider extends AIProvider {
6971
};
7072
}
7173

74+
/**
75+
* Invoke the Vercel AI model with structured output support.
76+
*/
77+
async invokeStructuredModel(
78+
messages: LDMessage[],
79+
responseStructure: Record<string, unknown>,
80+
): Promise<StructuredResponse> {
81+
// Convert responseStructure to Zod schema
82+
const schema = VercelProvider.convertToZodSchema(responseStructure);
83+
84+
// Call Vercel AI generateObject
85+
const result = await generateObject({
86+
model: this._model,
87+
messages,
88+
schema,
89+
...this._parameters,
90+
});
91+
92+
// Extract metrics including token usage and success status
93+
const metrics = VercelProvider.createAIMetrics(result);
94+
95+
return {
96+
data: result.object,
97+
rawResponse: JSON.stringify(result.object),
98+
metrics,
99+
};
100+
}
101+
72102
/**
73103
* Get the underlying Vercel AI model instance.
74104
*/
@@ -121,6 +151,72 @@ export class VercelProvider extends AIProvider {
121151
};
122152
}
123153

154+
/**
155+
* Convert a response structure object to a Zod schema.
156+
* This method recursively converts a generic object structure to a Zod schema
157+
* that can be used with Vercel AI SDK's generateObject function.
158+
*
159+
* @param responseStructure The structure object to convert
160+
* @returns A Zod schema representing the structure
161+
*/
162+
static convertToZodSchema(responseStructure: Record<string, unknown>): z.ZodSchema {
163+
const shape: Record<string, z.ZodSchema> = {};
164+
165+
Object.entries(responseStructure).forEach(([key, value]) => {
166+
// eslint-disable-next-line no-underscore-dangle
167+
shape[key] = VercelProvider._convertValueToZodSchema(value);
168+
});
169+
170+
return z.object(shape);
171+
}
172+
173+
/**
174+
* Convert a single value to a Zod schema.
175+
* This is a helper method for convertToZodSchema that handles different value types.
176+
*
177+
* @param value The value to convert
178+
* @returns A Zod schema for the value
179+
*/
180+
private static _convertValueToZodSchema(value: unknown): z.ZodSchema {
181+
if (value === null || value === undefined) {
182+
return z.any();
183+
}
184+
185+
if (typeof value === 'string') {
186+
return z.string();
187+
}
188+
189+
if (typeof value === 'number') {
190+
return z.number();
191+
}
192+
193+
if (typeof value === 'boolean') {
194+
return z.boolean();
195+
}
196+
197+
if (Array.isArray(value)) {
198+
if (value.length === 0) {
199+
return z.array(z.any());
200+
}
201+
// Use the first element to determine the array type
202+
// eslint-disable-next-line no-underscore-dangle
203+
const elementSchema = VercelProvider._convertValueToZodSchema(value[0]);
204+
return z.array(elementSchema);
205+
}
206+
207+
if (typeof value === 'object') {
208+
const shape: Record<string, z.ZodSchema> = {};
209+
Object.entries(value).forEach(([key, val]) => {
210+
// eslint-disable-next-line no-underscore-dangle
211+
shape[key] = VercelProvider._convertValueToZodSchema(val);
212+
});
213+
return z.object(shape);
214+
}
215+
216+
// Fallback for any other type
217+
return z.any();
218+
}
219+
124220
/**
125221
* Create a Vercel AI model from an AI configuration.
126222
* This method creates a Vercel AI model based on the provider configuration.

0 commit comments

Comments
 (0)