Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-stars-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vantige-ai/typescript-sdk": patch
---

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.
46 changes: 41 additions & 5 deletions examples/ai-sdk-tools-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { z } from 'zod';

/**
* Example showing how to create AI SDK tools from available knowledge bases
*
* This example demonstrates how to create tools compatible with different versions
* of Vercel's AI SDK:
* - AI SDK v4: Uses 'parameters' property for input schema
* - AI SDK v5+: Uses 'inputSchema' property (MCP-aligned)
*
* You can specify the version as the third parameter to any tool creation function.
*/
async function exampleAISDKToolsUsage() {
// Initialize the Vantige client
Expand Down Expand Up @@ -33,7 +40,8 @@ async function exampleAISDKToolsUsage() {
console.log('\n=== Creating Simple AI SDK Tools ===');
const simpleTools = createSimpleKnowledgeBaseTools(
availableResponse.knowledgeBases,
client
client,
'v5' // Default to v5, but you can specify 'v4' for legacy support
);

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

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

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

// ===== EXAMPLE 4: Testing a Tool =====
// ===== EXAMPLE 4: AI SDK Version Comparison =====
console.log('\n=== AI SDK Version Comparison ===');

// Create tools for v4 (legacy)
const v4Tools = createSimpleKnowledgeBaseTools(
availableResponse.knowledgeBases.slice(0, 1), // Just one for demo
client,
'v4'
);

// Create tools for v5 (current)
const v5Tools = createSimpleKnowledgeBaseTools(
availableResponse.knowledgeBases.slice(0, 1), // Just one for demo
client,
'v5'
);

const firstToolKey = Object.keys(v4Tools)[0];
if (firstToolKey) {
console.log(`\nTool "${firstToolKey}" structure comparison:`);
console.log('v4 tool has "parameters":', 'parameters' in v4Tools[firstToolKey]);
console.log('v4 tool has "inputSchema":', 'inputSchema' in v4Tools[firstToolKey]);
console.log('v5 tool has "parameters":', 'parameters' in v5Tools[firstToolKey]);
console.log('v5 tool has "inputSchema":', 'inputSchema' in v5Tools[firstToolKey]);
}

// ===== EXAMPLE 5: Testing a Tool =====
if (availableResponse.knowledgeBases.length > 0) {
console.log('\n=== Testing a Knowledge Base Tool ===');
const firstKB = availableResponse.knowledgeBases[0];
Expand Down Expand Up @@ -111,7 +147,7 @@ async function exampleAISDKToolsUsage() {
}
}

// ===== EXAMPLE 5: Integration with Vercel AI SDK =====
// ===== EXAMPLE 6: Integration with Vercel AI SDK =====
console.log('\n=== Vercel AI SDK Integration Example ===');

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

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

// ===== EXAMPLE 6: Dynamic Tool Loading =====
// ===== EXAMPLE 7: Dynamic Tool Loading =====
console.log('\n=== Dynamic Tool Loading Example ===');

// Function to dynamically load tools based on external scope
Expand Down
4 changes: 3 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ module.exports = {
'html',
'json-summary'
],
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts']
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
testTimeout: 5000, // 5 second timeout for all tests
maxWorkers: '50%' // Use half the available CPU cores for parallel test execution
};
92 changes: 66 additions & 26 deletions src/client/__tests__/http-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ describe('VantigeHttpClient', () => {
let mockAxiosInstance: any;

beforeEach(() => {
// Mock setTimeout to make retry delays instant
jest.spyOn(global, 'setTimeout').mockImplementation((fn) => {
fn();
return {} as any;
});

mockAuth = {
getAuthHeaders: jest.fn().mockReturnValue({
'Authorization': 'Bearer test-token',
Expand All @@ -39,23 +45,24 @@ describe('VantigeHttpClient', () => {

httpClient = new VantigeHttpClient({
baseUrl: 'https://api.vantige.ai',
timeout: 30000,
retries: 3,
timeout: 1000, // Much shorter timeout for tests
retries: 2, // Fewer retries for faster tests
auth: mockAuth,
debug: false,
});
});

afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

describe('Constructor', () => {
it('should create HTTP client with correct configuration', () => {
expect(httpClient).toBeInstanceOf(VantigeHttpClient);
expect(mockedAxios.create).toHaveBeenCalledWith({
baseURL: 'https://api.vantige.ai',
timeout: 30000,
timeout: 1000,
headers: {
'Authorization': 'Bearer test-token',
'Content-Type': 'application/json',
Expand All @@ -70,6 +77,13 @@ describe('VantigeHttpClient', () => {
});

describe('Error Handling', () => {
it('should pass through existing VantigeSDKError without wrapping', async () => {
const existing = new VantigeSDKError('Existing', VantigeErrorCode.NETWORK_ERROR);
mockAxiosInstance.get = jest.fn().mockRejectedValue(existing);

await expect(httpClient.get('/test')).rejects.toBe(existing);
});

it('should handle network timeout errors', async () => {
const timeoutError = new Error('timeout of 30000ms exceeded') as AxiosError;
timeoutError.code = 'ECONNABORTED';
Expand All @@ -78,7 +92,7 @@ describe('VantigeHttpClient', () => {
mockAxiosInstance.get = jest.fn().mockRejectedValue(timeoutError);

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});

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

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
}, 10000);
});
});

describe('Retry Logic', () => {
it('should throw after exhausting retries', async () => {
const networkError = new Error('Network Error') as AxiosError;
networkError.isAxiosError = true;
networkError.response = undefined;

mockAxiosInstance.get = jest.fn()
.mockRejectedValueOnce(networkError)
.mockRejectedValueOnce(networkError)
.mockRejectedValueOnce(networkError)
.mockRejectedValueOnce(networkError);

await expect(httpClient.get('/test')).rejects.toBeInstanceOf(VantigeSDKError);
expect(mockAxiosInstance.get).toHaveBeenCalled();
});

it('should retry on network errors', async () => {
const networkError = new Error('Network Error') as AxiosError;
networkError.isAxiosError = true;
Expand All @@ -227,7 +256,7 @@ describe('VantigeHttpClient', () => {
const result = await httpClient.get('/test');
expect(result).toEqual(successResponse);
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(3);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
}, 10000);
});

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

await expect(httpClient.get('/test')).rejects.toThrow(VantigeSDKError);
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
}, 10000);
});

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

// Mock setTimeout to avoid actual delays in tests
jest.spyOn(global, 'setTimeout').mockImplementation((fn) => {
fn();
return {} as any;
});

const result = await httpClient.get('/test');
expect(result).toEqual(successResponse);
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(2);

jest.restoreAllMocks();
}, 10000);
});
});

describe('HTTP Methods', () => {
it('should use interceptor success handler to return response unmodified', () => {
const handlers: any = {};
mockAxiosInstance.interceptors.response.use = jest.fn((success: any, fail: any) => {
handlers.success = success;
handlers.fail = fail;
});

// Recreate client to register interceptor with captured handlers
httpClient = new VantigeHttpClient({
baseUrl: 'https://api.vantige.ai',
timeout: 1000,
retries: 2,
auth: mockAuth,
});

const resp = { data: { ok: true } } as any;
const result = handlers.success(resp);
expect(result).toBe(resp);
});
it('should make GET requests', async () => {
const mockResponse = { success: true };
mockAxiosInstance.get = jest.fn().mockResolvedValue({ data: mockResponse });
Expand Down
Loading