Files
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
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) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

523 lines
18 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { fetchWithCache } from '../../src/cache';
import { createLiteLLMProvider, LiteLLMProvider } from '../../src/providers/litellm';
import { mockProcessEnv } from '../util/utils';
vi.mock('../../src/cache', async (importOriginal) => {
return {
...(await importOriginal()),
fetchWithCache: vi.fn(),
};
});
vi.mock('../../src/logger', () => ({
default: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
const mockFetchWithCache = vi.mocked(fetchWithCache);
// Mock fetch for API call tests
const mockFetch = vi.fn();
global.fetch = mockFetch as any;
const originalOpenAiTemperature = process.env.OPENAI_TEMPERATURE;
describe('LiteLLM Provider', () => {
beforeEach(() => {
vi.clearAllMocks();
mockProcessEnv({ OPENAI_TEMPERATURE: undefined });
});
afterEach(() => {
vi.resetAllMocks();
mockFetch.mockReset();
vi.unstubAllEnvs();
if (originalOpenAiTemperature === undefined) {
mockProcessEnv({ OPENAI_TEMPERATURE: undefined });
} else {
mockProcessEnv({ OPENAI_TEMPERATURE: originalOpenAiTemperature });
}
});
describe('createLiteLLMProvider', () => {
it('should create a chat provider by default', () => {
const provider = createLiteLLMProvider('litellm:gpt-4', {});
expect(provider).toBeInstanceOf(LiteLLMProvider);
});
it('should create a chat provider when explicitly specified', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {});
expect(provider).toBeInstanceOf(LiteLLMProvider);
});
it('should create a completion provider', () => {
const provider = createLiteLLMProvider('litellm:completion:gpt-3.5-turbo-instruct', {});
// Check that it has the right methods and identity
expect(provider.id()).toBe('litellm:completion:gpt-3.5-turbo-instruct');
expect(provider.toString()).toContain('LiteLLM Provider completion');
});
it('should create an embedding provider', () => {
const provider = createLiteLLMProvider('litellm:embedding:text-embedding-3-small', {});
// Check that it has the right methods and identity
expect(provider.id()).toBe('litellm:embedding:text-embedding-3-small');
expect(provider.toString()).toContain('LiteLLM Provider embedding');
});
it('should support embeddings alias', () => {
const provider = createLiteLLMProvider('litellm:embeddings:text-embedding-3-small', {});
// Check that it has the right methods and identity
expect(provider.id()).toBe('litellm:embedding:text-embedding-3-small');
expect(provider.toString()).toContain('LiteLLM Provider embedding');
});
it('should use custom apiBaseUrl from config', () => {
const customUrl = 'https://custom.litellm.com';
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiBaseUrl: customUrl,
},
},
});
expect(provider.config.apiBaseUrl).toBe(customUrl);
});
it('should use default apiBaseUrl if not provided', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {});
expect(provider.config.apiBaseUrl).toBe('http://0.0.0.0:4000');
});
it('should set apiKeyRequired to false', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {});
expect(provider.config.apiKeyRequired).toBe(false);
});
it('should set apiKeyEnvar to LITELLM_API_KEY', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {});
expect(provider.config.apiKeyEnvar).toBe('LITELLM_API_KEY');
});
it('should work with LITELLM_API_KEY environment variable', () => {
const originalEnv = process.env.LITELLM_API_KEY;
mockProcessEnv({ LITELLM_API_KEY: 'test-litellm-key' });
try {
const provider = createLiteLLMProvider('litellm:gpt-4', {
config: {
config: {
apiBaseUrl: 'http://localhost:4000',
},
},
});
// Verify config is set correctly
expect(provider.config.apiKeyRequired).toBe(false);
expect(provider.config.apiKeyEnvar).toBe('LITELLM_API_KEY');
// The wrapped provider should have the API key
const wrappedProvider = (provider as any).provider;
expect(wrappedProvider.getApiKey()).toBe('test-litellm-key');
} finally {
if (originalEnv === undefined) {
mockProcessEnv({ LITELLM_API_KEY: undefined });
} else {
mockProcessEnv({ LITELLM_API_KEY: originalEnv });
}
}
});
it('should handle model names with colons', () => {
const provider = createLiteLLMProvider('litellm:chat:custom:model:v1', {});
expect(provider).toBeInstanceOf(LiteLLMProvider);
expect(provider.id()).toContain('custom:model:v1');
});
it('should pass through additional config options', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
temperature: 0.7,
max_tokens: 100,
},
},
});
expect(provider.config.temperature).toBe(0.7);
expect(provider.config.max_tokens).toBe(100);
});
describe('Provider Identity', () => {
it('should return LiteLLM identity for chat provider', () => {
const provider = createLiteLLMProvider('litellm:gpt-4', {});
expect(provider.id()).toBe('litellm:gpt-4');
expect(provider.toString()).toBe('[LiteLLM Provider gpt-4]');
expect(provider.toJSON).toBeDefined();
expect(provider.toJSON!()).toEqual({
provider: 'litellm',
model: 'gpt-4',
type: 'chat',
config: expect.any(Object),
});
});
it('should return LiteLLM identity for explicit chat provider', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {});
expect(provider.id()).toBe('litellm:gpt-4');
expect(provider.toString()).toBe('[LiteLLM Provider gpt-4]');
});
it('should return LiteLLM identity for completion provider', () => {
const provider = createLiteLLMProvider('litellm:completion:gpt-3.5-turbo-instruct', {});
expect(provider.id()).toBe('litellm:completion:gpt-3.5-turbo-instruct');
expect(provider.toString()).toBe('[LiteLLM Provider completion gpt-3.5-turbo-instruct]');
expect(provider.toJSON).toBeDefined();
expect(provider.toJSON!()).toEqual({
provider: 'litellm',
model: 'gpt-3.5-turbo-instruct',
type: 'completion',
config: expect.any(Object),
});
});
it('should return LiteLLM identity for embedding provider', () => {
const provider = createLiteLLMProvider('litellm:embedding:text-embedding-3-small', {});
expect(provider.id()).toBe('litellm:embedding:text-embedding-3-small');
expect(provider.toString()).toBe('[LiteLLM Provider embedding text-embedding-3-small]');
expect(provider.toJSON).toBeDefined();
expect(provider.toJSON!()).toEqual({
provider: 'litellm',
model: 'text-embedding-3-small',
type: 'embedding',
config: expect.any(Object),
});
});
});
describe('Config handling', () => {
it('should not override default apiBaseUrl with falsy values', () => {
const provider1 = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiBaseUrl: null as any,
},
},
});
expect(provider1.config.apiBaseUrl).toBe('http://0.0.0.0:4000');
const provider2 = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiBaseUrl: undefined,
},
},
});
expect(provider2.config.apiBaseUrl).toBe('http://0.0.0.0:4000');
const provider3 = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiBaseUrl: '',
},
},
});
expect(provider3.config.apiBaseUrl).toBe('');
});
it('should override default apiBaseUrl with truthy values', () => {
const customUrl = 'https://custom.litellm.com';
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiBaseUrl: customUrl,
},
},
});
expect(provider.config.apiBaseUrl).toBe(customUrl);
});
it('should use LITELLM_API_BASE from provider env when config.apiBaseUrl is not set', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {},
env: { LITELLM_API_BASE: 'http://my-litellm-server' },
},
});
expect(provider.config.apiBaseUrl).toBe('http://my-litellm-server');
});
it('should use LITELLM_API_BASE from context env when config and provider env are not set', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: { config: {} },
env: { LITELLM_API_BASE: 'http://context-litellm.example.com' },
});
expect(provider.config.apiBaseUrl).toBe('http://context-litellm.example.com');
});
it('should use LITELLM_API_BASE from process env when no config or options env set', () => {
vi.stubEnv('LITELLM_API_BASE', 'http://env-litellm.example.com');
const provider = createLiteLLMProvider('litellm:chat:gpt-4', { config: { config: {} } });
expect(provider.config.apiBaseUrl).toBe('http://env-litellm.example.com');
});
it('should prefer provider env over context env and process env', () => {
vi.stubEnv('LITELLM_API_BASE', 'http://process-env.example.com');
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {},
env: { LITELLM_API_BASE: 'http://provider-env-wins.example.com' },
},
env: { LITELLM_API_BASE: 'http://context-env.example.com' },
});
expect(provider.config.apiBaseUrl).toBe('http://provider-env-wins.example.com');
});
it('should prefer config.apiBaseUrl over provider env, context env, and process env', () => {
vi.stubEnv('LITELLM_API_BASE', 'http://env.example.com');
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: { apiBaseUrl: 'https://config-wins.com' },
env: { LITELLM_API_BASE: 'http://provider-env.example.com' },
},
env: { LITELLM_API_BASE: 'http://context-env.example.com' },
});
expect(provider.config.apiBaseUrl).toBe('https://config-wins.com');
});
});
});
describe('Embedding Provider Functionality', () => {
it('should have callEmbeddingApi method', () => {
const provider = createLiteLLMProvider('litellm:embedding:text-embedding-3-small', {});
expect(typeof provider.callEmbeddingApi).toBe('function');
});
it('should be recognized as a valid embedding provider', () => {
const provider = createLiteLLMProvider('litellm:embedding:text-embedding-3-small', {});
// Check that it has either callEmbeddingApi or callSimilarityApi (matching matcher provider validation)
const isValidEmbeddingProvider =
'callEmbeddingApi' in provider || 'callSimilarityApi' in provider;
expect(isValidEmbeddingProvider).toBe(true);
});
it('should not have callEmbeddingApi method for chat providers', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {});
expect('callEmbeddingApi' in provider).toBe(false);
});
it('should not have callEmbeddingApi method for completion providers', () => {
const provider = createLiteLLMProvider('litellm:completion:gpt-3.5-turbo-instruct', {});
expect('callEmbeddingApi' in provider).toBe(false);
});
it('should pass through custom configuration to embedding provider', () => {
const customConfig = {
apiBaseUrl: 'https://custom.litellm.com',
apiKey: 'custom-key',
temperature: 0.5,
};
const provider = createLiteLLMProvider('litellm:embedding:text-embedding-3-small', {
config: {
config: customConfig,
},
});
expect(provider.config).toMatchObject(customConfig);
});
it('should handle model names with colons for embedding providers', () => {
const provider = createLiteLLMProvider('litellm:embedding:custom:embedding:model:v1', {});
expect(provider.id()).toBe('litellm:embedding:custom:embedding:model:v1');
expect(typeof provider.callEmbeddingApi).toBe('function');
});
});
describe('Temperature zero handling (GitHub issue #7322)', () => {
it('should correctly pass temperature: 0 to the underlying provider config', () => {
// This test verifies the fix for GitHub issue #7322 where temperature: 0
// was not being sent to the API because of a falsy check
const provider = createLiteLLMProvider('litellm:chat:gpt-3.5-turbo', {
config: {
config: {
temperature: 0,
},
},
});
// Verify the config is correctly set with temperature: 0
expect(provider.config.temperature).toBe(0);
expect('temperature' in provider.config).toBe(true);
});
it('should correctly pass max_tokens: 0 to the underlying provider config when explicitly set', () => {
// While max_tokens: 0 is impractical, it should still be preserved if explicitly configured
const provider = createLiteLLMProvider('litellm:chat:gpt-3.5-turbo', {
config: {
config: {
max_tokens: 0,
},
},
});
// Verify the config is correctly set with max_tokens: 0
expect(provider.config.max_tokens).toBe(0);
expect('max_tokens' in provider.config).toBe(true);
});
});
describe('Temperature omission for proxy providers (GitHub issue #8044)', () => {
const mockResponse = {
data: {
choices: [{ message: { content: 'Test output' } }],
usage: { total_tokens: 10, prompt_tokens: 5, completion_tokens: 5 },
},
cached: false,
status: 200,
statusText: 'OK',
};
it('should omit temperature from request body when not configured', async () => {
mockFetchWithCache.mockResolvedValue(mockResponse);
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiKey: 'test-key',
},
},
});
await provider.callApi('Test prompt');
const call = mockFetchWithCache.mock.calls[0] as [string, { body: string }];
const body = JSON.parse(call[1].body);
expect(body.temperature).toBeUndefined();
expect('temperature' in body).toBe(false);
});
it('should send temperature: 0 when explicitly configured', async () => {
mockFetchWithCache.mockResolvedValue(mockResponse);
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiKey: 'test-key',
temperature: 0,
},
},
});
await provider.callApi('Test prompt');
const call = mockFetchWithCache.mock.calls[0] as [string, { body: string }];
const body = JSON.parse(call[1].body);
expect(body.temperature).toBe(0);
expect('temperature' in body).toBe(true);
});
it('should send temperature: 0.7 when explicitly configured', async () => {
mockFetchWithCache.mockResolvedValue(mockResponse);
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiKey: 'test-key',
temperature: 0.7,
},
},
});
await provider.callApi('Test prompt');
const call = mockFetchWithCache.mock.calls[0] as [string, { body: string }];
const body = JSON.parse(call[1].body);
expect(body.temperature).toBe(0.7);
expect('temperature' in body).toBe(true);
});
it('should use OPENAI_TEMPERATURE env var when set', async () => {
mockFetchWithCache.mockResolvedValue(mockResponse);
mockProcessEnv({ OPENAI_TEMPERATURE: '0.5' });
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiKey: 'test-key',
},
},
});
await provider.callApi('Test prompt');
const call = mockFetchWithCache.mock.calls[0] as [string, { body: string }];
const body = JSON.parse(call[1].body);
expect(body.temperature).toBe(0.5);
expect('temperature' in body).toBe(true);
});
it('should set omitDefaults to true by default', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {});
expect(provider.config.omitDefaults).toBe(true);
});
it('should allow user to override omitDefaults', () => {
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
omitDefaults: false,
},
},
});
expect(provider.config.omitDefaults).toBe(false);
});
it('should send default temperature: 0 when omitDefaults is false', async () => {
mockFetchWithCache.mockResolvedValue(mockResponse);
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiKey: 'test-key',
omitDefaults: false,
},
},
});
await provider.callApi('Test prompt');
const call = mockFetchWithCache.mock.calls[0] as [string, { body: string }];
const body = JSON.parse(call[1].body);
expect(body.temperature).toBe(0);
expect('temperature' in body).toBe(true);
expect(body.max_tokens).toBe(1024);
expect('max_tokens' in body).toBe(true);
});
it('should omit max_tokens from request body when not configured', async () => {
mockFetchWithCache.mockResolvedValue(mockResponse);
const provider = createLiteLLMProvider('litellm:chat:gpt-4', {
config: {
config: {
apiKey: 'test-key',
},
},
});
await provider.callApi('Test prompt');
const call = mockFetchWithCache.mock.calls[0] as [string, { body: string }];
const body = JSON.parse(call[1].body);
expect(body.max_tokens).toBeUndefined();
expect('max_tokens' in body).toBe(false);
});
});
});