Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@launchdarkly/js-core",
"workspaces": [
"packages/ai-providers/server-ai-langchain",
"packages/ai-providers/server-ai-vercel",
"packages/shared/common",
"packages/shared/sdk-client",
"packages/shared/sdk-server",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { VercelProvider } from '../src/VercelProvider';

// Mock Vercel AI SDK
jest.mock('ai', () => ({
generateText: jest.fn(),
}));

describe('VercelProvider', () => {
let mockModel: any;
let provider: VercelProvider;

beforeEach(() => {
mockModel = { name: 'test-model' };
provider = new VercelProvider(mockModel, {});
});

describe('createAIMetrics', () => {
it('creates metrics with success=true and token usage', () => {
const mockResponse = {
usage: {
promptTokens: 50,
completionTokens: 50,
totalTokens: 100,
},
};

const result = VercelProvider.createAIMetrics(mockResponse);

expect(result).toEqual({
success: true,
usage: {
total: 100,
input: 50,
output: 50,
},
});
});

it('creates metrics with success=true and no usage when usage is missing', () => {
const mockResponse = {};

const result = VercelProvider.createAIMetrics(mockResponse);

expect(result).toEqual({
success: true,
usage: undefined,
});
});

it('handles partial usage data', () => {
const mockResponse = {
usage: {
promptTokens: 30,
// completionTokens and totalTokens missing
},
};

const result = VercelProvider.createAIMetrics(mockResponse);

expect(result).toEqual({
success: true,
usage: {
total: 0,
input: 30,
output: 0,
},
});
});
});

describe('invokeModel', () => {
it('invokes Vercel AI generateText and returns response', async () => {
const { generateText } = require('ai');
const mockResponse = {
text: 'Hello! How can I help you today?',
usage: {
promptTokens: 10,
completionTokens: 15,
totalTokens: 25,
},
};

(generateText as jest.Mock).mockResolvedValue(mockResponse);

const messages = [
{ role: 'user' as const, content: 'Hello!' },
];

const result = await provider.invokeModel(messages);

expect(generateText).toHaveBeenCalledWith({
model: mockModel,
messages: [{ role: 'user', content: 'Hello!' }],
});

expect(result).toEqual({
message: {
role: 'assistant',
content: 'Hello! How can I help you today?',
},
metrics: {
success: true,
usage: {
total: 25,
input: 10,
output: 15,
},
},
});
});

it('handles response without usage data', async () => {
const { generateText } = require('ai');
const mockResponse = {
text: 'Hello! How can I help you today?',
};

(generateText as jest.Mock).mockResolvedValue(mockResponse);

const messages = [
{ role: 'user' as const, content: 'Hello!' },
];

const result = await provider.invokeModel(messages);

expect(result).toEqual({
message: {
role: 'assistant',
content: 'Hello! How can I help you today?',
},
metrics: {
success: true,
usage: undefined,
},
});
});
});

describe('getModel', () => {
it('returns the underlying Vercel AI model', () => {
const model = provider.getModel();
expect(model).toBe(mockModel);
});
});

describe('createVercelModel', () => {
it('creates OpenAI model for openai provider', async () => {
const mockAiConfig = {
model: { name: 'gpt-4', parameters: {} },
provider: { name: 'openai' },
enabled: true,
tracker: {} as any,
toVercelAISDK: jest.fn(),
};

// Mock the dynamic import
jest.doMock('@ai-sdk/openai', () => ({
openai: jest.fn().mockReturnValue(mockModel),
}));

const result = await VercelProvider.createVercelModel(mockAiConfig);
expect(result).toBe(mockModel);
});

it('throws error for unsupported provider', async () => {
const mockAiConfig = {
model: { name: 'test-model', parameters: {} },
provider: { name: 'unsupported' },
enabled: true,
tracker: {} as any,
toVercelAISDK: jest.fn(),
};

await expect(VercelProvider.createVercelModel(mockAiConfig)).rejects.toThrow(
'Unsupported Vercel AI provider: unsupported'
);
});
});

describe('create', () => {
it('creates VercelProvider with correct model and parameters', async () => {
const mockAiConfig = {
model: {
name: 'gpt-4',
parameters: {
temperature: 0.7,
maxTokens: 1000,
},
},
provider: { name: 'openai' },
enabled: true,
tracker: {} as any,
toVercelAISDK: jest.fn(),
};

// Mock the dynamic import
jest.doMock('@ai-sdk/openai', () => ({
openai: jest.fn().mockReturnValue(mockModel),
}));

const result = await VercelProvider.create(mockAiConfig);

expect(result).toBeInstanceOf(VercelProvider);
expect(result.getModel()).toBeDefined();
});
});
});
14 changes: 14 additions & 0 deletions packages/ai-providers/server-ai-vercel/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>'],
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
'!src/**/*.spec.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
};
67 changes: 67 additions & 0 deletions packages/ai-providers/server-ai-vercel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@launchdarkly/server-sdk-ai-vercel",
"version": "0.0.0",
"description": "LaunchDarkly AI SDK Vercel Provider for Server-Side JavaScript",
"homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/ai-providers/server-ai-vercel",
"repository": {
"type": "git",
"url": "https://github.com/launchdarkly/js-core.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "commonjs",
"scripts": {
"build": "npx tsc",
"lint": "npx eslint . --ext .ts",
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore",
"lint:fix": "yarn run lint --fix",
"check": "yarn prettier && yarn lint && yarn build && yarn test",
"test": "jest"
},
"keywords": [
"launchdarkly",
"ai",
"llm",
"vercel"
],
"author": "LaunchDarkly",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "^2.0.0",
"@launchdarkly/server-sdk-ai": "^0.12.0",
"ai": "^5.0.0"
},
"optionalDependencies": {
"@ai-sdk/anthropic": "^2.0.0",
"@ai-sdk/cohere": "^2.0.0",
"@ai-sdk/google": "^2.0.0",
"@ai-sdk/mistral": "^2.0.0",
"@ai-sdk/openai": "^2.0.0"
},
"devDependencies": {
"@ai-sdk/anthropic": "^2.0.0",
"@ai-sdk/cohere": "^2.0.0",
"@ai-sdk/google": "^2.0.0",
"@ai-sdk/mistral": "^2.0.0",
"@ai-sdk/openai": "^2.0.0",
"@launchdarkly/js-server-sdk-common": "2.16.2",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"eslint": "^8.45.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.1",
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"typescript": "5.1.6"
},
"peerDependencies": {
"@launchdarkly/js-server-sdk-common": "2.x"
}
}
Loading