Skip to content

Commit c29cfd0

Browse files
committed
feat: Add VercelAI Provider for AI SDK
1 parent ed8c332 commit c29cfd0

File tree

8 files changed

+494
-0
lines changed

8 files changed

+494
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "@launchdarkly/js-core",
33
"workspaces": [
4+
"packages/ai-providers/server-ai-vercel",
45
"packages/shared/common",
56
"packages/shared/sdk-client",
67
"packages/shared/sdk-server",
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { VercelProvider } from '../src/VercelProvider';
2+
3+
// Mock Vercel AI SDK
4+
jest.mock('ai', () => ({
5+
generateText: jest.fn(),
6+
}));
7+
8+
describe('VercelProvider', () => {
9+
let mockModel: any;
10+
let provider: VercelProvider;
11+
12+
beforeEach(() => {
13+
mockModel = { name: 'test-model' };
14+
provider = new VercelProvider(mockModel, {});
15+
});
16+
17+
describe('createAIMetrics', () => {
18+
it('creates metrics with success=true and token usage', () => {
19+
const mockResponse = {
20+
usage: {
21+
promptTokens: 50,
22+
completionTokens: 50,
23+
totalTokens: 100,
24+
},
25+
};
26+
27+
const result = VercelProvider.createAIMetrics(mockResponse);
28+
29+
expect(result).toEqual({
30+
success: true,
31+
usage: {
32+
total: 100,
33+
input: 50,
34+
output: 50,
35+
},
36+
});
37+
});
38+
39+
it('creates metrics with success=true and no usage when usage is missing', () => {
40+
const mockResponse = {};
41+
42+
const result = VercelProvider.createAIMetrics(mockResponse);
43+
44+
expect(result).toEqual({
45+
success: true,
46+
usage: undefined,
47+
});
48+
});
49+
50+
it('handles partial usage data', () => {
51+
const mockResponse = {
52+
usage: {
53+
promptTokens: 30,
54+
// completionTokens and totalTokens missing
55+
},
56+
};
57+
58+
const result = VercelProvider.createAIMetrics(mockResponse);
59+
60+
expect(result).toEqual({
61+
success: true,
62+
usage: {
63+
total: 0,
64+
input: 30,
65+
output: 0,
66+
},
67+
});
68+
});
69+
});
70+
71+
describe('invokeModel', () => {
72+
it('invokes Vercel AI generateText and returns response', async () => {
73+
const { generateText } = require('ai');
74+
const mockResponse = {
75+
text: 'Hello! How can I help you today?',
76+
usage: {
77+
promptTokens: 10,
78+
completionTokens: 15,
79+
totalTokens: 25,
80+
},
81+
};
82+
83+
(generateText as jest.Mock).mockResolvedValue(mockResponse);
84+
85+
const messages = [
86+
{ role: 'user' as const, content: 'Hello!' },
87+
];
88+
89+
const result = await provider.invokeModel(messages);
90+
91+
expect(generateText).toHaveBeenCalledWith({
92+
model: mockModel,
93+
messages: [{ role: 'user', content: 'Hello!' }],
94+
});
95+
96+
expect(result).toEqual({
97+
message: {
98+
role: 'assistant',
99+
content: 'Hello! How can I help you today?',
100+
},
101+
metrics: {
102+
success: true,
103+
usage: {
104+
total: 25,
105+
input: 10,
106+
output: 15,
107+
},
108+
},
109+
});
110+
});
111+
112+
it('handles response without usage data', async () => {
113+
const { generateText } = require('ai');
114+
const mockResponse = {
115+
text: 'Hello! How can I help you today?',
116+
};
117+
118+
(generateText as jest.Mock).mockResolvedValue(mockResponse);
119+
120+
const messages = [
121+
{ role: 'user' as const, content: 'Hello!' },
122+
];
123+
124+
const result = await provider.invokeModel(messages);
125+
126+
expect(result).toEqual({
127+
message: {
128+
role: 'assistant',
129+
content: 'Hello! How can I help you today?',
130+
},
131+
metrics: {
132+
success: true,
133+
usage: undefined,
134+
},
135+
});
136+
});
137+
});
138+
139+
describe('getModel', () => {
140+
it('returns the underlying Vercel AI model', () => {
141+
const model = provider.getModel();
142+
expect(model).toBe(mockModel);
143+
});
144+
});
145+
146+
describe('createVercelModel', () => {
147+
it('creates OpenAI model for openai provider', async () => {
148+
const mockAiConfig = {
149+
model: { name: 'gpt-4', parameters: {} },
150+
provider: { name: 'openai' },
151+
enabled: true,
152+
tracker: {} as any,
153+
toVercelAISDK: jest.fn(),
154+
};
155+
156+
// Mock the dynamic import
157+
jest.doMock('@ai-sdk/openai', () => ({
158+
openai: jest.fn().mockReturnValue(mockModel),
159+
}));
160+
161+
const result = await VercelProvider.createVercelModel(mockAiConfig);
162+
expect(result).toBe(mockModel);
163+
});
164+
165+
it('throws error for unsupported provider', async () => {
166+
const mockAiConfig = {
167+
model: { name: 'test-model', parameters: {} },
168+
provider: { name: 'unsupported' },
169+
enabled: true,
170+
tracker: {} as any,
171+
toVercelAISDK: jest.fn(),
172+
};
173+
174+
await expect(VercelProvider.createVercelModel(mockAiConfig)).rejects.toThrow(
175+
'Unsupported Vercel AI provider: unsupported'
176+
);
177+
});
178+
});
179+
180+
describe('create', () => {
181+
it('creates VercelProvider with correct model and parameters', async () => {
182+
const mockAiConfig = {
183+
model: {
184+
name: 'gpt-4',
185+
parameters: {
186+
temperature: 0.7,
187+
maxTokens: 1000,
188+
},
189+
},
190+
provider: { name: 'openai' },
191+
enabled: true,
192+
tracker: {} as any,
193+
toVercelAISDK: jest.fn(),
194+
};
195+
196+
// Mock the dynamic import
197+
jest.doMock('@ai-sdk/openai', () => ({
198+
openai: jest.fn().mockReturnValue(mockModel),
199+
}));
200+
201+
const result = await VercelProvider.create(mockAiConfig);
202+
203+
expect(result).toBeInstanceOf(VercelProvider);
204+
expect(result.getModel()).toBeDefined();
205+
});
206+
});
207+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
roots: ['<rootDir>'],
5+
testMatch: ['**/__tests__/**/*.test.ts'],
6+
collectCoverageFrom: [
7+
'src/**/*.ts',
8+
'!src/**/*.d.ts',
9+
'!src/**/*.test.ts',
10+
'!src/**/*.spec.ts',
11+
],
12+
coverageDirectory: 'coverage',
13+
coverageReporters: ['text', 'lcov', 'html'],
14+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"name": "@launchdarkly/server-sdk-ai-vercel",
3+
"version": "0.0.0",
4+
"description": "LaunchDarkly AI SDK Vercel Provider for Server-Side JavaScript",
5+
"homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/ai-providers/server-ai-vercel",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/launchdarkly/js-core.git"
9+
},
10+
"main": "dist/index.js",
11+
"types": "dist/index.d.ts",
12+
"type": "commonjs",
13+
"scripts": {
14+
"build": "npx tsc",
15+
"lint": "npx eslint . --ext .ts",
16+
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore",
17+
"lint:fix": "yarn run lint --fix",
18+
"check": "yarn prettier && yarn lint && yarn build && yarn test",
19+
"test": "jest"
20+
},
21+
"keywords": [
22+
"launchdarkly",
23+
"ai",
24+
"llm",
25+
"vercel"
26+
],
27+
"author": "LaunchDarkly",
28+
"license": "Apache-2.0",
29+
"dependencies": {
30+
"@ai-sdk/provider": "^2.0.0",
31+
"@launchdarkly/server-sdk-ai": "0.11.4",
32+
"ai": "^5.0.0"
33+
},
34+
"optionalDependencies": {
35+
"@ai-sdk/anthropic": "^2.0.0",
36+
"@ai-sdk/cohere": "^2.0.0",
37+
"@ai-sdk/google": "^2.0.0",
38+
"@ai-sdk/mistral": "^2.0.0",
39+
"@ai-sdk/openai": "^2.0.0"
40+
},
41+
"devDependencies": {
42+
"@ai-sdk/anthropic": "^2.0.0",
43+
"@ai-sdk/cohere": "^2.0.0",
44+
"@ai-sdk/google": "^2.0.0",
45+
"@ai-sdk/mistral": "^2.0.0",
46+
"@ai-sdk/openai": "^2.0.0",
47+
"@launchdarkly/js-server-sdk-common": "2.16.2",
48+
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
49+
"@types/jest": "^29.5.3",
50+
"@typescript-eslint/eslint-plugin": "^6.20.0",
51+
"@typescript-eslint/parser": "^6.20.0",
52+
"eslint": "^8.45.0",
53+
"eslint-config-airbnb-base": "^15.0.0",
54+
"eslint-config-airbnb-typescript": "^17.1.0",
55+
"eslint-config-prettier": "^8.8.0",
56+
"eslint-plugin-import": "^2.27.5",
57+
"eslint-plugin-jest": "^27.6.3",
58+
"eslint-plugin-prettier": "^5.0.0",
59+
"jest": "^29.6.1",
60+
"prettier": "^3.0.0",
61+
"ts-jest": "^29.1.1",
62+
"typescript": "5.1.6"
63+
},
64+
"peerDependencies": {
65+
"@launchdarkly/js-server-sdk-common": "2.x"
66+
}
67+
}

0 commit comments

Comments
 (0)