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
288 lines
8.6 KiB
TypeScript
288 lines
8.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { fetchWithCache } from '../../../src/cache';
|
|
import { AzureCompletionProvider } from '../../../src/providers/azure/completion';
|
|
import { mockProcessEnv } from '../../util/utils';
|
|
|
|
vi.mock('../../../src/cache', async (importOriginal) => {
|
|
return {
|
|
...(await importOriginal()),
|
|
fetchWithCache: vi.fn(),
|
|
};
|
|
});
|
|
|
|
const setAuthHeaders = (
|
|
provider: AzureCompletionProvider,
|
|
headers: Record<string, string> = { 'api-key': 'test-key' },
|
|
) => {
|
|
(provider as any).authHeaders = headers;
|
|
(provider as any).initialized = true;
|
|
};
|
|
|
|
describe('AzureCompletionProvider', () => {
|
|
beforeEach(() => {
|
|
vi.spyOn(AzureCompletionProvider.prototype as any, 'ensureInitialized').mockResolvedValue(
|
|
undefined,
|
|
);
|
|
vi.spyOn(AzureCompletionProvider.prototype as any, 'getAuthHeaders').mockResolvedValue({
|
|
'api-key': 'test-key',
|
|
});
|
|
mockProcessEnv({ OPENAI_STOP: undefined });
|
|
mockProcessEnv({ AZURE_API_HOST: 'test.azure.com' });
|
|
mockProcessEnv({ AZURE_API_KEY: 'test-key' });
|
|
});
|
|
|
|
afterEach(() => {
|
|
mockProcessEnv({ AZURE_API_HOST: undefined });
|
|
mockProcessEnv({ AZURE_API_KEY: undefined });
|
|
vi.restoreAllMocks();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should handle basic completion with caching', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: 'hello' }],
|
|
usage: { total_tokens: 10, prompt_tokens: 5, completion_tokens: 5 },
|
|
},
|
|
cached: false,
|
|
} as any);
|
|
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: 'hello' }],
|
|
usage: { total_tokens: 10 },
|
|
},
|
|
cached: true,
|
|
} as any);
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: { apiHost: 'test.azure.com' },
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
const result1 = await provider.callApi('test prompt');
|
|
const result2 = await provider.callApi('test prompt');
|
|
|
|
expect(result1.output).toBe('hello');
|
|
expect(result2.output).toBe('hello');
|
|
expect(result1.tokenUsage).toEqual({ total: 10, prompt: 5, completion: 5 });
|
|
expect(result2.tokenUsage).toEqual({ cached: 10, total: 10 });
|
|
});
|
|
|
|
it('should pass custom headers from config to fetchWithCache', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: 'hello' }],
|
|
usage: { total_tokens: 10, prompt_tokens: 5, completion_tokens: 5 },
|
|
},
|
|
cached: false,
|
|
} as any);
|
|
|
|
const customHeaders = {
|
|
'X-Test-Header': 'test-value',
|
|
'Another-Header': 'another-value',
|
|
};
|
|
|
|
const provider = new AzureCompletionProvider('test-deployment', {
|
|
config: {
|
|
apiHost: 'test.azure.com',
|
|
apiKey: 'test-key',
|
|
headers: customHeaders,
|
|
},
|
|
});
|
|
|
|
setAuthHeaders(provider);
|
|
await provider.callApi('test prompt');
|
|
|
|
expect(fetchWithCache).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
'api-key': 'test-key',
|
|
'X-Test-Header': 'test-value',
|
|
'Another-Header': 'another-value',
|
|
}),
|
|
}),
|
|
expect.any(Number),
|
|
'json',
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it('should handle content filter response', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: null, finish_reason: 'content_filter' }],
|
|
usage: { total_tokens: 10, prompt_tokens: 5, completion_tokens: 5 },
|
|
},
|
|
cached: false,
|
|
} as any);
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: { apiHost: 'test.azure.com' },
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result.output).toBe(
|
|
"The generated content was filtered due to triggering Azure OpenAI Service's content filtering system.",
|
|
);
|
|
});
|
|
|
|
it('should handle API errors', async () => {
|
|
vi.mocked(fetchWithCache).mockRejectedValueOnce(new Error('API Error'));
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: { apiHost: 'test.azure.com' },
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result.error).toBe('API call error: Error: API Error');
|
|
});
|
|
|
|
it('should handle missing API host', async () => {
|
|
vi.mocked(fetchWithCache).mockImplementationOnce(function () {
|
|
throw new Error('Azure API host must be set.');
|
|
});
|
|
|
|
const provider = new AzureCompletionProvider('test', { config: {} });
|
|
setAuthHeaders(provider);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result.error).toBe('API call error: Error: Azure API host must be set.');
|
|
});
|
|
|
|
it('should handle empty response text', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: '', finish_reason: 'stop' }],
|
|
usage: { total_tokens: 10, prompt_tokens: 5, completion_tokens: 5 },
|
|
},
|
|
cached: false,
|
|
} as any);
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: { apiHost: 'test.azure.com' },
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result.output).toBe('');
|
|
});
|
|
|
|
it('should handle invalid OPENAI_STOP env var', async () => {
|
|
mockProcessEnv({ OPENAI_STOP: '{invalid json}' });
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: { apiHost: 'test.azure.com' },
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
await expect(provider.callApi('test')).rejects.toThrow(
|
|
/OPENAI_STOP is not a valid JSON string/,
|
|
);
|
|
|
|
mockProcessEnv({ OPENAI_STOP: undefined });
|
|
});
|
|
|
|
it('should handle missing output and finish_reason not content_filter', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: null, finish_reason: 'stop' }],
|
|
usage: { total_tokens: 7, prompt_tokens: 3, completion_tokens: 4 },
|
|
},
|
|
cached: false,
|
|
} as any);
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: { apiHost: 'test.azure.com' },
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result.output).toBe('');
|
|
expect(result.tokenUsage).toEqual({ total: 7, prompt: 3, completion: 4 });
|
|
});
|
|
|
|
it('should handle exception in response parsing gracefully', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {},
|
|
cached: false,
|
|
} as any);
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: { apiHost: 'test.azure.com' },
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result.error).toMatch(/API response error:/);
|
|
expect(result.tokenUsage).toEqual({
|
|
total: undefined,
|
|
prompt: undefined,
|
|
completion: undefined,
|
|
});
|
|
});
|
|
|
|
it('should pass passthrough config fields in body', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: 'foo' }],
|
|
usage: { total_tokens: 1, prompt_tokens: 1, completion_tokens: 0 },
|
|
},
|
|
cached: false,
|
|
} as any);
|
|
|
|
const provider = new AzureCompletionProvider('test', {
|
|
config: {
|
|
apiHost: 'test.azure.com',
|
|
passthrough: { logprobs: 3 },
|
|
},
|
|
});
|
|
setAuthHeaders(provider);
|
|
|
|
await provider.callApi('test prompt');
|
|
|
|
const actualCall = vi.mocked(fetchWithCache).mock.calls[0];
|
|
const body = JSON.parse(actualCall[1]?.body as string);
|
|
expect(body.logprobs).toBe(3);
|
|
});
|
|
|
|
it('should allow config.headers to override authHeaders', async () => {
|
|
vi.mocked(fetchWithCache).mockResolvedValueOnce({
|
|
data: {
|
|
choices: [{ text: 'override' }],
|
|
usage: { total_tokens: 2, prompt_tokens: 1, completion_tokens: 1 },
|
|
},
|
|
cached: false,
|
|
} as any);
|
|
|
|
const provider = new AzureCompletionProvider('test-deployment', {
|
|
config: {
|
|
apiHost: 'test.azure.com',
|
|
apiKey: 'test-key',
|
|
headers: { 'api-key': 'override-key', Extra: 'foo' },
|
|
},
|
|
});
|
|
|
|
setAuthHeaders(provider);
|
|
await provider.callApi('test prompt');
|
|
|
|
expect(fetchWithCache).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
'api-key': 'override-key',
|
|
Extra: 'foo',
|
|
}),
|
|
}),
|
|
expect.any(Number),
|
|
'json',
|
|
undefined,
|
|
);
|
|
});
|
|
});
|