Skip to content

Commit 0c06d6f

Browse files
authored
Merge pull request #11 from vantige-ai/enhancement/ai-sdk-v4-support
adding support for v4 ai sdk
2 parents a37e893 + f82523f commit 0c06d6f

File tree

7 files changed

+382
-113
lines changed

7 files changed

+382
-113
lines changed

.changeset/six-stars-eat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vantige-ai/typescript-sdk": patch
3+
---
4+
5+
Adding support for Vercel AI SDK v4. Users can now specify if they are using v4 of the AI SDK which will use the 'parameters' key instead of the newer 'inputSchema' key when creating tools.

examples/ai-sdk-tools-usage.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import { z } from 'zod';
66

77
/**
88
* Example showing how to create AI SDK tools from available knowledge bases
9+
*
10+
* This example demonstrates how to create tools compatible with different versions
11+
* of Vercel's AI SDK:
12+
* - AI SDK v4: Uses 'parameters' property for input schema
13+
* - AI SDK v5+: Uses 'inputSchema' property (MCP-aligned)
14+
*
15+
* You can specify the version as the third parameter to any tool creation function.
916
*/
1017
async function exampleAISDKToolsUsage() {
1118
// Initialize the Vantige client
@@ -33,7 +40,8 @@ async function exampleAISDKToolsUsage() {
3340
console.log('\n=== Creating Simple AI SDK Tools ===');
3441
const simpleTools = createSimpleKnowledgeBaseTools(
3542
availableResponse.knowledgeBases,
36-
client
43+
client,
44+
'v5' // Default to v5, but you can specify 'v4' for legacy support
3745
);
3846

3947
console.log('Simple tools created:', Object.keys(simpleTools));
@@ -60,7 +68,8 @@ async function exampleAISDKToolsUsage() {
6068
console.log('\n=== Creating Full-Featured AI SDK Tools ===');
6169
const fullTools = createKnowledgeBaseTools(
6270
availableResponse.knowledgeBases,
63-
client
71+
client,
72+
'v5' // Default to v5, but you can specify 'v4' for legacy support
6473
);
6574

6675
console.log('Full tools created:', Object.keys(fullTools));
@@ -72,14 +81,41 @@ async function exampleAISDKToolsUsage() {
7281
client,
7382
{
7483
simplified: true, // Use simple interface
84+
version: 'v5', // Specify AI SDK version ('v4' or 'v5')
7585
keyGenerator: (kb) => `search-${kb.name.toLowerCase().replace(/\s+/g, '-')}`,
7686
descriptionGenerator: (kb) => `Search the ${kb.name} knowledge base for relevant information. ${kb.description || ''}`,
7787
}
7888
);
7989

8090
console.log('Custom tools created:', Object.keys(customTools));
8191

82-
// ===== EXAMPLE 4: Testing a Tool =====
92+
// ===== EXAMPLE 4: AI SDK Version Comparison =====
93+
console.log('\n=== AI SDK Version Comparison ===');
94+
95+
// Create tools for v4 (legacy)
96+
const v4Tools = createSimpleKnowledgeBaseTools(
97+
availableResponse.knowledgeBases.slice(0, 1), // Just one for demo
98+
client,
99+
'v4'
100+
);
101+
102+
// Create tools for v5 (current)
103+
const v5Tools = createSimpleKnowledgeBaseTools(
104+
availableResponse.knowledgeBases.slice(0, 1), // Just one for demo
105+
client,
106+
'v5'
107+
);
108+
109+
const firstToolKey = Object.keys(v4Tools)[0];
110+
if (firstToolKey) {
111+
console.log(`\nTool "${firstToolKey}" structure comparison:`);
112+
console.log('v4 tool has "parameters":', 'parameters' in v4Tools[firstToolKey]);
113+
console.log('v4 tool has "inputSchema":', 'inputSchema' in v4Tools[firstToolKey]);
114+
console.log('v5 tool has "parameters":', 'parameters' in v5Tools[firstToolKey]);
115+
console.log('v5 tool has "inputSchema":', 'inputSchema' in v5Tools[firstToolKey]);
116+
}
117+
118+
// ===== EXAMPLE 5: Testing a Tool =====
83119
if (availableResponse.knowledgeBases.length > 0) {
84120
console.log('\n=== Testing a Knowledge Base Tool ===');
85121
const firstKB = availableResponse.knowledgeBases[0];
@@ -111,7 +147,7 @@ async function exampleAISDKToolsUsage() {
111147
}
112148
}
113149

114-
// ===== EXAMPLE 5: Integration with Vercel AI SDK =====
150+
// ===== EXAMPLE 6: Integration with Vercel AI SDK =====
115151
console.log('\n=== Vercel AI SDK Integration Example ===');
116152

117153
// This is how you would use it in a real Vercel AI SDK application
@@ -142,7 +178,7 @@ async function exampleAISDKToolsUsage() {
142178

143179
console.log('AI SDK configuration ready with tools:', Object.keys(aiSDKIntegration.tools));
144180

145-
// ===== EXAMPLE 6: Dynamic Tool Loading =====
181+
// ===== EXAMPLE 7: Dynamic Tool Loading =====
146182
console.log('\n=== Dynamic Tool Loading Example ===');
147183

148184
// Function to dynamically load tools based on external scope

jest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ module.exports = {
2323
'html',
2424
'json-summary'
2525
],
26-
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts']
26+
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
27+
testTimeout: 5000, // 5 second timeout for all tests
28+
maxWorkers: '50%' // Use half the available CPU cores for parallel test execution
2729
};

src/client/__tests__/http-client.test.ts

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ describe('VantigeHttpClient', () => {
1313
let mockAxiosInstance: any;
1414

1515
beforeEach(() => {
16+
// Mock setTimeout to make retry delays instant
17+
jest.spyOn(global, 'setTimeout').mockImplementation((fn) => {
18+
fn();
19+
return {} as any;
20+
});
21+
1622
mockAuth = {
1723
getAuthHeaders: jest.fn().mockReturnValue({
1824
'Authorization': 'Bearer test-token',
@@ -39,23 +45,24 @@ describe('VantigeHttpClient', () => {
3945

4046
httpClient = new VantigeHttpClient({
4147
baseUrl: 'https://api.vantige.ai',
42-
timeout: 30000,
43-
retries: 3,
48+
timeout: 1000, // Much shorter timeout for tests
49+
retries: 2, // Fewer retries for faster tests
4450
auth: mockAuth,
4551
debug: false,
4652
});
4753
});
4854

4955
afterEach(() => {
5056
jest.clearAllMocks();
57+
jest.restoreAllMocks();
5158
});
5259

5360
describe('Constructor', () => {
5461
it('should create HTTP client with correct configuration', () => {
5562
expect(httpClient).toBeInstanceOf(VantigeHttpClient);
5663
expect(mockedAxios.create).toHaveBeenCalledWith({
5764
baseURL: 'https://api.vantige.ai',
58-
timeout: 30000,
65+
timeout: 1000,
5966
headers: {
6067
'Authorization': 'Bearer test-token',
6168
'Content-Type': 'application/json',
@@ -70,6 +77,13 @@ describe('VantigeHttpClient', () => {
7077
});
7178

7279
describe('Error Handling', () => {
80+
it('should pass through existing VantigeSDKError without wrapping', async () => {
81+
const existing = new VantigeSDKError('Existing', VantigeErrorCode.NETWORK_ERROR);
82+
mockAxiosInstance.get = jest.fn().mockRejectedValue(existing);
83+
84+
await expect(httpClient.get('/test')).rejects.toBe(existing);
85+
});
86+
7387
it('should handle network timeout errors', async () => {
7488
const timeoutError = new Error('timeout of 30000ms exceeded') as AxiosError;
7589
timeoutError.code = 'ECONNABORTED';
@@ -78,7 +92,7 @@ describe('VantigeHttpClient', () => {
7892
mockAxiosInstance.get = jest.fn().mockRejectedValue(timeoutError);
7993

8094
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
81-
}, 10000);
95+
});
8296

8397
it('should handle network errors without response', async () => {
8498
const networkError = new Error('Network Error') as AxiosError;
@@ -88,7 +102,7 @@ describe('VantigeHttpClient', () => {
88102
mockAxiosInstance.get = jest.fn().mockRejectedValue(networkError);
89103

90104
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
91-
}, 10000);
105+
});
92106

93107
it('should handle 401 Unauthorized errors', async () => {
94108
const unauthorizedError = {
@@ -102,7 +116,7 @@ describe('VantigeHttpClient', () => {
102116
mockAxiosInstance.get = jest.fn().mockRejectedValue(unauthorizedError);
103117

104118
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
105-
}, 10000);
119+
});
106120

107121
it('should handle 403 Forbidden errors', async () => {
108122
const forbiddenError = {
@@ -116,7 +130,7 @@ describe('VantigeHttpClient', () => {
116130
mockAxiosInstance.get = jest.fn().mockRejectedValue(forbiddenError);
117131

118132
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
119-
}, 10000);
133+
});
120134

121135
it('should handle 404 Not Found errors', async () => {
122136
const notFoundError = {
@@ -130,7 +144,7 @@ describe('VantigeHttpClient', () => {
130144
mockAxiosInstance.get = jest.fn().mockRejectedValue(notFoundError);
131145

132146
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
133-
}, 10000);
147+
});
134148

135149
it('should handle 422 Validation errors', async () => {
136150
const validationError = {
@@ -144,7 +158,7 @@ describe('VantigeHttpClient', () => {
144158
mockAxiosInstance.get = jest.fn().mockRejectedValue(validationError);
145159

146160
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
147-
}, 10000);
161+
});
148162

149163
it('should handle 429 Rate Limit errors', async () => {
150164
const rateLimitError = {
@@ -158,7 +172,7 @@ describe('VantigeHttpClient', () => {
158172
mockAxiosInstance.get = jest.fn().mockRejectedValue(rateLimitError);
159173

160174
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
161-
}, 10000);
175+
});
162176

163177
it('should handle 500 Internal Server errors', async () => {
164178
const serverError = {
@@ -172,7 +186,7 @@ describe('VantigeHttpClient', () => {
172186
mockAxiosInstance.get = jest.fn().mockRejectedValue(serverError);
173187

174188
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
175-
}, 10000);
189+
});
176190

177191
it('should handle 503 Service Unavailable errors', async () => {
178192
const serviceUnavailableError = {
@@ -186,7 +200,7 @@ describe('VantigeHttpClient', () => {
186200
mockAxiosInstance.get = jest.fn().mockRejectedValue(serviceUnavailableError);
187201

188202
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
189-
}, 10000);
203+
});
190204

191205
it('should handle unknown HTTP errors', async () => {
192206
const unknownError = {
@@ -200,18 +214,33 @@ describe('VantigeHttpClient', () => {
200214
mockAxiosInstance.get = jest.fn().mockRejectedValue(unknownError);
201215

202216
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
203-
}, 10000);
217+
});
204218

205219
it('should handle non-axios errors', async () => {
206220
const genericError = new Error('Generic error');
207221

208222
mockAxiosInstance.get = jest.fn().mockRejectedValue(genericError);
209223

210224
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
211-
}, 10000);
225+
});
212226
});
213227

214228
describe('Retry Logic', () => {
229+
it('should throw after exhausting retries', async () => {
230+
const networkError = new Error('Network Error') as AxiosError;
231+
networkError.isAxiosError = true;
232+
networkError.response = undefined;
233+
234+
mockAxiosInstance.get = jest.fn()
235+
.mockRejectedValueOnce(networkError)
236+
.mockRejectedValueOnce(networkError)
237+
.mockRejectedValueOnce(networkError)
238+
.mockRejectedValueOnce(networkError);
239+
240+
await expect(httpClient.get('/test')).rejects.toBeInstanceOf(VantigeSDKError);
241+
expect(mockAxiosInstance.get).toHaveBeenCalled();
242+
});
243+
215244
it('should retry on network errors', async () => {
216245
const networkError = new Error('Network Error') as AxiosError;
217246
networkError.isAxiosError = true;
@@ -227,7 +256,7 @@ describe('VantigeHttpClient', () => {
227256
const result = await httpClient.get('/test');
228257
expect(result).toEqual(successResponse);
229258
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(3);
230-
}, 10000);
259+
});
231260

232261
it('should not retry on authentication errors', async () => {
233262
const authError = {
@@ -247,7 +276,7 @@ describe('VantigeHttpClient', () => {
247276

248277
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
249278
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
250-
}, 10000);
279+
});
251280

252281
it('should not retry on validation errors', async () => {
253282
const validationError = {
@@ -267,7 +296,7 @@ describe('VantigeHttpClient', () => {
267296

268297
await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
269298
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
270-
}, 10000);
299+
});
271300

272301
it('should retry on rate limit errors with delay', async () => {
273302
const rateLimitError = {
@@ -288,21 +317,32 @@ describe('VantigeHttpClient', () => {
288317
.mockRejectedValueOnce(rateLimitError)
289318
.mockResolvedValueOnce({ data: successResponse });
290319

291-
// Mock setTimeout to avoid actual delays in tests
292-
jest.spyOn(global, 'setTimeout').mockImplementation((fn) => {
293-
fn();
294-
return {} as any;
295-
});
296-
297320
const result = await httpClient.get('/test');
298321
expect(result).toEqual(successResponse);
299322
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(2);
300-
301-
jest.restoreAllMocks();
302-
}, 10000);
323+
});
303324
});
304325

305326
describe('HTTP Methods', () => {
327+
it('should use interceptor success handler to return response unmodified', () => {
328+
const handlers: any = {};
329+
mockAxiosInstance.interceptors.response.use = jest.fn((success: any, fail: any) => {
330+
handlers.success = success;
331+
handlers.fail = fail;
332+
});
333+
334+
// Recreate client to register interceptor with captured handlers
335+
httpClient = new VantigeHttpClient({
336+
baseUrl: 'https://api.vantige.ai',
337+
timeout: 1000,
338+
retries: 2,
339+
auth: mockAuth,
340+
});
341+
342+
const resp = { data: { ok: true } } as any;
343+
const result = handlers.success(resp);
344+
expect(result).toBe(resp);
345+
});
306346
it('should make GET requests', async () => {
307347
const mockResponse = { success: true };
308348
mockAxiosInstance.get = jest.fn().mockResolvedValue({ data: mockResponse });

0 commit comments

Comments
 (0)