Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

249 lines
8.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { HyperbolicAudioProvider } from '../../src/providers/hyperbolic/audio';
import { calculateHyperbolicCost, HyperbolicProvider } from '../../src/providers/hyperbolic/chat';
import { HyperbolicImageProvider } from '../../src/providers/hyperbolic/image';
import { OpenAiChatCompletionProvider } from '../../src/providers/openai/chat';
import { mockProcessEnv } from '../util/utils';
vi.mock('../../src/logger', () => ({
default: {
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
}));
describe('HyperbolicProvider', () => {
let provider: HyperbolicProvider;
const mockApiKey = 'test-api-key';
beforeEach(() => {
vi.clearAllMocks();
mockProcessEnv({ HYPERBOLIC_API_KEY: mockApiKey });
});
afterEach(() => {
mockProcessEnv({ HYPERBOLIC_API_KEY: undefined });
});
describe('constructor', () => {
it('should create a provider with default config', () => {
provider = new HyperbolicProvider('deepseek-ai/DeepSeek-R1', {});
expect(provider.id()).toBe('hyperbolic:deepseek-ai/DeepSeek-R1');
expect(provider.toString()).toBe('[Hyperbolic Provider deepseek-ai/DeepSeek-R1]');
});
it('should use custom API key from config', () => {
const customApiKey = 'custom-key';
provider = new HyperbolicProvider('deepseek-ai/DeepSeek-R1', {
config: { config: { apiKey: customApiKey } },
});
expect(provider['originalConfig']?.apiKey).toBe(customApiKey);
});
});
describe('isReasoningModel', () => {
it('should identify reasoning models correctly', () => {
const reasoningProvider = new HyperbolicProvider('deepseek-ai/DeepSeek-R1', {});
expect(reasoningProvider['isReasoningModel']()).toBe(true);
const qwqProvider = new HyperbolicProvider('qwen/QwQ-32B', {});
expect(qwqProvider['isReasoningModel']()).toBe(true);
const regularProvider = new HyperbolicProvider('meta-llama/Llama-3.1-70B', {});
expect(regularProvider['isReasoningModel']()).toBe(false);
});
});
describe('callApi', () => {
it('should handle successful API response', async () => {
provider = new HyperbolicProvider('deepseek-ai/DeepSeek-R1', {});
const mockResponse = {
output: 'Test response',
tokenUsage: { prompt: 100, completion: 50 },
cached: false,
raw: JSON.stringify({
usage: {
prompt_tokens: 100,
completion_tokens: 50,
completion_tokens_details: {
reasoning_tokens: 30,
accepted_prediction_tokens: 10,
rejected_prediction_tokens: 5,
},
},
}),
};
vi.spyOn(OpenAiChatCompletionProvider.prototype, 'callApi').mockResolvedValue(mockResponse);
const result = await provider.callApi('Test prompt');
expect(result.output).toBe('Test response');
expect(result.tokenUsage.completionDetails).toEqual({
reasoning: 30,
acceptedPrediction: 10,
rejectedPrediction: 5,
});
expect(result.cost).toBeDefined();
});
it('should handle API errors', async () => {
provider = new HyperbolicProvider('deepseek-ai/DeepSeek-R1', {});
const mockError = { error: 'API Error' };
vi.spyOn(OpenAiChatCompletionProvider.prototype, 'callApi').mockResolvedValue(mockError);
const result = await provider.callApi('Test prompt');
expect(result).toEqual(mockError);
});
});
describe('calculateHyperbolicCost', () => {
it('should calculate cost for known models', () => {
const cost = calculateHyperbolicCost('deepseek-ai/DeepSeek-R1', {}, 1000, 500);
expect(cost).toBe((0.5 / 1e6) * 1000 + (2.18 / 1e6) * 500);
});
it('should handle model aliases', () => {
const cost = calculateHyperbolicCost('DeepSeek-R1', {}, 1000, 500);
expect(cost).toBe((0.5 / 1e6) * 1000 + (2.18 / 1e6) * 500);
});
it('should return undefined for unknown models', () => {
const cost = calculateHyperbolicCost('unknown-model', {}, 1000, 500);
expect(cost).toBeUndefined();
});
it('should return undefined if tokens are missing', () => {
const cost = calculateHyperbolicCost('deepseek-ai/DeepSeek-R1', {}, undefined, 500);
expect(cost).toBeUndefined();
});
it('should use custom cost from config', () => {
const customCost = 0.001;
const cost = calculateHyperbolicCost(
'deepseek-ai/DeepSeek-R1',
{ cost: customCost },
1000,
500,
);
expect(cost).toBe(customCost * 1000 + customCost * 500);
});
it('should use separate custom input and output costs from config', () => {
const cost = calculateHyperbolicCost(
'deepseek-ai/DeepSeek-R1',
{ inputCost: 0.001, outputCost: 0.003 },
1000,
500,
);
expect(cost).toBe(2.5);
});
it('should prefer separate custom costs over custom cost', () => {
const cost = calculateHyperbolicCost(
'deepseek-ai/DeepSeek-R1',
{ cost: 0.02, inputCost: 0.001, outputCost: 0.003 },
1000,
500,
);
expect(cost).toBe(2.5);
});
});
});
describe('HyperbolicImageProvider', () => {
let provider: HyperbolicImageProvider;
const mockApiKey = 'test-api-key';
beforeEach(() => {
vi.clearAllMocks();
mockProcessEnv({ HYPERBOLIC_API_KEY: mockApiKey });
});
afterEach(() => {
mockProcessEnv({ HYPERBOLIC_API_KEY: undefined });
});
describe('constructor', () => {
it('should create an image provider with default model', () => {
provider = new HyperbolicImageProvider('Flux.1-dev', {});
expect(provider.id()).toBe('hyperbolic:image:Flux.1-dev');
expect(provider.toString()).toBe('[Hyperbolic Image Provider Flux.1-dev]');
});
});
describe('getApiModelName', () => {
it('should resolve model aliases', () => {
provider = new HyperbolicImageProvider('flux-dev', {});
expect(provider['getApiModelName']()).toBe('Flux.1-dev');
provider = new HyperbolicImageProvider('sdxl', {});
expect(provider['getApiModelName']()).toBe('SDXL1.0-base');
});
it('should return original name for unknown models', () => {
provider = new HyperbolicImageProvider('custom-model', {});
expect(provider['getApiModelName']()).toBe('custom-model');
});
});
describe('calculateImageCost', () => {
it('should return correct cost for known models', () => {
provider = new HyperbolicImageProvider('Flux.1-dev', {});
expect(provider['calculateImageCost']()).toBe(0.025);
provider = new HyperbolicImageProvider('SDXL1.0-base', {});
expect(provider['calculateImageCost']()).toBe(0.01);
});
it('should return default cost for unknown models', () => {
provider = new HyperbolicImageProvider('unknown-model', {});
expect(provider['calculateImageCost']()).toBe(0.01);
});
});
});
describe('HyperbolicAudioProvider', () => {
let provider: HyperbolicAudioProvider;
const mockApiKey = 'test-api-key';
beforeEach(() => {
vi.clearAllMocks();
mockProcessEnv({ HYPERBOLIC_API_KEY: mockApiKey });
});
afterEach(() => {
mockProcessEnv({ HYPERBOLIC_API_KEY: undefined });
});
describe('constructor', () => {
it('should create an audio provider with default model', () => {
provider = new HyperbolicAudioProvider('Melo-TTS', {});
expect(provider.id()).toBe('hyperbolic:audio:Melo-TTS');
expect(provider.toString()).toBe('[Hyperbolic Audio Provider Melo-TTS]');
});
it('should use default model if none specified', () => {
provider = new HyperbolicAudioProvider('', {});
expect(provider.modelName).toBe('Melo-TTS');
});
});
describe('calculateAudioCost', () => {
it('should calculate cost based on text length', () => {
provider = new HyperbolicAudioProvider('Melo-TTS', {});
const textLength = 5000; // 5000 characters
expect(provider['calculateAudioCost'](textLength)).toBe(0.005); // $0.001 per 1000 chars
});
it('should handle partial thousands', () => {
provider = new HyperbolicAudioProvider('Melo-TTS', {});
const textLength = 1500; // 1.5 thousand characters
expect(provider['calculateAudioCost'](textLength)).toBe(0.0015);
});
});
});