0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { OpenAiChatCompletionProvider } from '../../src/providers/openai/chat';
|
|
import {
|
|
calculatePerplexityCost,
|
|
createPerplexityProvider,
|
|
PerplexityProvider,
|
|
} from '../../src/providers/perplexity';
|
|
|
|
describe('Perplexity Provider', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('createPerplexityProvider', () => {
|
|
it('should create a provider with default settings', () => {
|
|
const provider = createPerplexityProvider('perplexity:sonar');
|
|
|
|
expect(provider).toBeInstanceOf(PerplexityProvider);
|
|
});
|
|
|
|
it('should use sonar as the default model if none is specified', () => {
|
|
const provider = createPerplexityProvider('perplexity:');
|
|
|
|
expect(provider).toBeInstanceOf(PerplexityProvider);
|
|
// @ts-ignore - accessing private property for testing
|
|
expect(provider.modelName).toBe('sonar');
|
|
});
|
|
|
|
it('should handle specific Perplexity models', () => {
|
|
const models = [
|
|
'sonar-pro',
|
|
'sonar-reasoning',
|
|
'sonar-reasoning-pro',
|
|
'sonar-deep-research',
|
|
'r1-1776',
|
|
];
|
|
|
|
for (const model of models) {
|
|
const provider = createPerplexityProvider(`perplexity:${model}`);
|
|
|
|
expect(provider).toBeInstanceOf(PerplexityProvider);
|
|
// @ts-ignore - accessing private property for testing
|
|
expect(provider.modelName).toBe(model);
|
|
}
|
|
});
|
|
|
|
it('should pass through configuration options', () => {
|
|
const config = {
|
|
temperature: 0.7,
|
|
max_tokens: 1000,
|
|
search_domain_filter: ['example.com'],
|
|
search_recency_filter: 'week',
|
|
return_related_questions: true,
|
|
};
|
|
|
|
const provider = createPerplexityProvider('perplexity:sonar', {
|
|
config: { config },
|
|
});
|
|
|
|
expect(provider).toBeInstanceOf(PerplexityProvider);
|
|
// Verify config was passed through to constructor
|
|
// @ts-ignore - accessing private property for testing
|
|
expect(provider.config).toMatchObject(expect.objectContaining(config));
|
|
});
|
|
});
|
|
|
|
describe('PerplexityProvider', () => {
|
|
it('should initialize with the correct API base URL and key environment variable', () => {
|
|
const provider = new PerplexityProvider('sonar');
|
|
|
|
// @ts-ignore - accessing private properties for testing
|
|
expect(provider.config.apiBaseUrl).toBe('https://api.perplexity.ai');
|
|
// @ts-ignore - accessing private properties for testing
|
|
expect(provider.config.apiKeyEnvar).toBe('PERPLEXITY_API_KEY');
|
|
});
|
|
|
|
it('should set the correct usage tier', () => {
|
|
const tiers = ['high', 'medium', 'low'] as const;
|
|
|
|
for (const tier of tiers) {
|
|
const provider = new PerplexityProvider('sonar', {
|
|
config: {
|
|
usage_tier: tier,
|
|
},
|
|
});
|
|
|
|
// @ts-ignore - accessing private property for testing
|
|
expect(provider.usageTier).toBe(tier);
|
|
}
|
|
});
|
|
|
|
it('should default to "medium" usage tier if not specified', () => {
|
|
const provider = new PerplexityProvider('sonar');
|
|
|
|
// @ts-ignore - accessing private property for testing
|
|
expect(provider.usageTier).toBe('medium');
|
|
});
|
|
|
|
it('should have the correct id() method', () => {
|
|
const provider = new PerplexityProvider('sonar-pro');
|
|
expect(provider.id()).toBe('sonar-pro');
|
|
});
|
|
|
|
it('should have the correct toString() method', () => {
|
|
const provider = new PerplexityProvider('sonar');
|
|
expect(provider.toString()).toBe('[Perplexity Provider sonar]');
|
|
});
|
|
|
|
it('should have the correct toJSON() method', () => {
|
|
const provider = new PerplexityProvider('sonar-pro', {
|
|
config: {
|
|
temperature: 0.7,
|
|
max_tokens: 1000,
|
|
},
|
|
});
|
|
|
|
expect(provider.toJSON()).toEqual({
|
|
provider: 'perplexity',
|
|
model: 'sonar-pro',
|
|
config: expect.objectContaining({
|
|
temperature: 0.7,
|
|
max_tokens: 1000,
|
|
apiKey: undefined,
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('should override callApi to calculate costs correctly', async () => {
|
|
// Mock the parent class callApi method
|
|
vi.spyOn(OpenAiChatCompletionProvider.prototype, 'callApi').mockResolvedValueOnce({
|
|
output: 'Test output',
|
|
tokenUsage: {
|
|
total: 20,
|
|
prompt: 10,
|
|
completion: 10,
|
|
},
|
|
});
|
|
|
|
const provider = new PerplexityProvider('sonar-pro');
|
|
const result = await provider.callApi('Test prompt');
|
|
|
|
// Verify the response has our custom cost calculation
|
|
expect(result.cost).toBeDefined();
|
|
expect(result.cost).toBe(0.00018); // (10/1M * $3) + (10/1M * $15) = $0.00018
|
|
});
|
|
|
|
it('should handle cached responses correctly', async () => {
|
|
// Mock the parent class callApi method with a cached response
|
|
vi.spyOn(OpenAiChatCompletionProvider.prototype, 'callApi').mockResolvedValueOnce({
|
|
output: 'Cached output',
|
|
tokenUsage: {
|
|
total: 20,
|
|
cached: 20,
|
|
},
|
|
cached: true,
|
|
});
|
|
|
|
const provider = new PerplexityProvider('sonar');
|
|
const result = await provider.callApi('Test prompt');
|
|
|
|
// Verify cached response is returned unchanged
|
|
expect(result.cached).toBe(true);
|
|
expect(result.tokenUsage?.cached).toBe(20);
|
|
// Cost should not be calculated for cached responses
|
|
expect(result.cost).toBeUndefined();
|
|
});
|
|
|
|
it('should still calculate cost for fresh responses with cached input tokens', async () => {
|
|
vi.spyOn(OpenAiChatCompletionProvider.prototype, 'callApi').mockResolvedValueOnce({
|
|
output: 'Fresh output',
|
|
tokenUsage: {
|
|
total: 20,
|
|
prompt: 10,
|
|
completion: 10,
|
|
cached: 5,
|
|
},
|
|
cached: false,
|
|
});
|
|
|
|
const provider = new PerplexityProvider('sonar-pro');
|
|
const result = await provider.callApi('Test prompt');
|
|
|
|
expect(result.cost).toBe(0.00018);
|
|
});
|
|
|
|
it('should pass through error responses', async () => {
|
|
// Mock the parent class callApi method with an error
|
|
vi.spyOn(OpenAiChatCompletionProvider.prototype, 'callApi').mockResolvedValueOnce({
|
|
error: 'API error',
|
|
});
|
|
|
|
const provider = new PerplexityProvider('sonar');
|
|
const result = await provider.callApi('Test prompt');
|
|
|
|
// Verify error is passed through
|
|
expect(result.error).toBe('API error');
|
|
expect(result.cost).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('calculatePerplexityCost', () => {
|
|
it('should return 0 if no tokens are provided', () => {
|
|
expect(calculatePerplexityCost('sonar')).toBe(0);
|
|
expect(calculatePerplexityCost('sonar', 0, 0)).toBe(0);
|
|
expect(calculatePerplexityCost('sonar', undefined, undefined)).toBe(0);
|
|
});
|
|
|
|
it('should calculate costs for sonar model', () => {
|
|
// sonar: $1 per million input tokens, $1 per million output tokens
|
|
expect(calculatePerplexityCost('sonar', 1000000, 1000000)).toBe(2);
|
|
expect(calculatePerplexityCost('sonar', 500000, 500000)).toBe(1);
|
|
expect(calculatePerplexityCost('sonar', 2000000, 0)).toBe(2);
|
|
expect(calculatePerplexityCost('sonar', 0, 3000000)).toBe(3);
|
|
});
|
|
|
|
it('should calculate costs for sonar-pro model', () => {
|
|
// sonar-pro: $3 per million input tokens, $15 per million output tokens
|
|
expect(calculatePerplexityCost('sonar-pro', 1000000, 1000000)).toBe(18);
|
|
expect(calculatePerplexityCost('sonar-pro', 500000, 500000)).toBe(9);
|
|
expect(calculatePerplexityCost('sonar-pro', 2000000, 0)).toBe(6);
|
|
expect(calculatePerplexityCost('sonar-pro', 0, 2000000)).toBe(30);
|
|
});
|
|
|
|
it('should calculate costs for sonar-reasoning model', () => {
|
|
// sonar-reasoning: $1 per million input tokens, $5 per million output tokens
|
|
expect(calculatePerplexityCost('sonar-reasoning', 1000000, 1000000)).toBe(6);
|
|
expect(calculatePerplexityCost('sonar-reasoning', 500000, 500000)).toBe(3);
|
|
expect(calculatePerplexityCost('sonar-reasoning', 2000000, 0)).toBe(2);
|
|
expect(calculatePerplexityCost('sonar-reasoning', 0, 2000000)).toBe(10);
|
|
});
|
|
|
|
it('should calculate costs for sonar-reasoning-pro model', () => {
|
|
// sonar-reasoning-pro: $2 per million input tokens, $8 per million output tokens
|
|
expect(calculatePerplexityCost('sonar-reasoning-pro', 1000000, 1000000)).toBe(10);
|
|
expect(calculatePerplexityCost('sonar-reasoning-pro', 500000, 500000)).toBe(5);
|
|
expect(calculatePerplexityCost('sonar-reasoning-pro', 2000000, 0)).toBe(4);
|
|
expect(calculatePerplexityCost('sonar-reasoning-pro', 0, 2000000)).toBe(16);
|
|
});
|
|
|
|
it('should calculate costs for sonar-deep-research model', () => {
|
|
// sonar-deep-research: $2 per million input tokens, $8 per million output tokens
|
|
expect(calculatePerplexityCost('sonar-deep-research', 1000000, 1000000)).toBe(10);
|
|
expect(calculatePerplexityCost('sonar-deep-research', 500000, 500000)).toBe(5);
|
|
expect(calculatePerplexityCost('sonar-deep-research', 2000000, 0)).toBe(4);
|
|
expect(calculatePerplexityCost('sonar-deep-research', 0, 2000000)).toBe(16);
|
|
});
|
|
|
|
it('should calculate costs for r1-1776 model', () => {
|
|
// r1-1776: $2 per million input tokens, $8 per million output tokens
|
|
expect(calculatePerplexityCost('r1-1776', 1000000, 1000000)).toBe(10);
|
|
expect(calculatePerplexityCost('r1-1776', 500000, 500000)).toBe(5);
|
|
expect(calculatePerplexityCost('r1-1776', 2000000, 0)).toBe(4);
|
|
expect(calculatePerplexityCost('r1-1776', 0, 2000000)).toBe(16);
|
|
});
|
|
|
|
it('should handle unknown models by defaulting to sonar pricing', () => {
|
|
expect(calculatePerplexityCost('unknown-model', 1000000, 1000000)).toBe(2);
|
|
expect(calculatePerplexityCost('custom-model', 500000, 500000)).toBe(1);
|
|
});
|
|
|
|
it('should handle case insensitivity in model names', () => {
|
|
expect(calculatePerplexityCost('SONAR-PRO', 1000000, 1000000)).toBe(18);
|
|
expect(calculatePerplexityCost('Sonar-Reasoning', 1000000, 1000000)).toBe(6);
|
|
expect(calculatePerplexityCost('sonar-DEEP-research', 1000000, 1000000)).toBe(10);
|
|
});
|
|
|
|
it('should handle different usage tiers', () => {
|
|
// Test one model with different tiers (the tier doesn't affect the token price calculation)
|
|
const model = 'sonar-pro';
|
|
const inputTokens = 1000000;
|
|
const outputTokens = 1000000;
|
|
|
|
// All tiers should calculate the same token costs
|
|
expect(calculatePerplexityCost(model, inputTokens, outputTokens, 'high')).toBe(18);
|
|
expect(calculatePerplexityCost(model, inputTokens, outputTokens, 'medium')).toBe(18);
|
|
expect(calculatePerplexityCost(model, inputTokens, outputTokens, 'low')).toBe(18);
|
|
});
|
|
});
|
|
});
|