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

457 lines
20 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AzureModerationProvider } from '../../src/providers/azure/moderation';
import {
getDefaultProviders,
setDefaultCompletionProviders,
setDefaultEmbeddingProviders,
} from '../../src/providers/defaults';
import { AIStudioChatProvider } from '../../src/providers/google/ai.studio';
import { hasGoogleDefaultCredentials } from '../../src/providers/google/util';
import { VertexEmbeddingProvider } from '../../src/providers/google/vertex';
import {
DefaultEmbeddingProvider as MistralEmbeddingProvider,
DefaultGradingJsonProvider as MistralGradingJsonProvider,
DefaultGradingProvider as MistralGradingProvider,
DefaultSuggestionsProvider as MistralSuggestionsProvider,
DefaultSynthesizeProvider as MistralSynthesizeProvider,
} from '../../src/providers/mistral/defaults';
import {
clearCodexDefaultProvidersForTesting,
hasCodexDefaultCredentials,
} from '../../src/providers/openai/codexDefaults';
import {
DefaultModerationProvider,
DefaultEmbeddingProvider as OpenAiEmbeddingProvider,
DefaultGradingJsonProvider as OpenAiGradingJsonProvider,
DefaultGradingProvider as OpenAiGradingProvider,
DefaultSuggestionsProvider as OpenAiSuggestionsProvider,
} from '../../src/providers/openai/defaults';
import { providerRegistry } from '../../src/providers/providerRegistry';
import { mockProcessEnv } from '../util/utils';
import type { EnvOverrides } from '../../src/types/env';
import type { ApiProvider } from '../../src/types/index';
vi.mock('../../src/providers/google/util', async (importOriginal) => {
return {
...(await importOriginal()),
hasGoogleDefaultCredentials: vi.fn().mockResolvedValue(false),
};
});
vi.mock('../../src/providers/openai/codexDefaults', async (importOriginal) => {
return {
...(await importOriginal<typeof import('../../src/providers/openai/codexDefaults')>()),
hasCodexDefaultCredentials: vi.fn().mockReturnValue(false),
};
});
class MockProvider implements ApiProvider {
private providerId: string;
constructor(id: string) {
this.providerId = id;
}
id(): string {
return this.providerId;
}
async callApi() {
return {};
}
}
describe('Provider override tests', () => {
const originalEnv = { ...process.env };
beforeEach(() => {
mockProcessEnv({ ...originalEnv }, { clear: true });
setDefaultCompletionProviders(undefined as any);
setDefaultEmbeddingProviders(undefined as any);
vi.mocked(hasGoogleDefaultCredentials).mockResolvedValue(false);
vi.mocked(hasCodexDefaultCredentials).mockReturnValue(false);
clearCodexDefaultProvidersForTesting();
mockProcessEnv({ OPENAI_API_KEY: undefined });
mockProcessEnv({ ANTHROPIC_API_KEY: undefined });
mockProcessEnv({ MISTRAL_API_KEY: undefined });
mockProcessEnv({ XAI_API_KEY: undefined });
mockProcessEnv({ GEMINI_API_KEY: undefined });
mockProcessEnv({ GOOGLE_API_KEY: undefined });
mockProcessEnv({ PALM_API_KEY: undefined });
mockProcessEnv({ AZURE_OPENAI_API_KEY: undefined });
mockProcessEnv({ AZURE_API_KEY: undefined });
mockProcessEnv({ AZURE_DEPLOYMENT_NAME: undefined });
mockProcessEnv({ AZURE_OPENAI_DEPLOYMENT_NAME: undefined });
});
afterEach(async () => {
mockProcessEnv(originalEnv, { clear: true });
clearCodexDefaultProvidersForTesting();
await providerRegistry.shutdownAll();
vi.resetAllMocks();
});
it('should override all completion providers when setDefaultCompletionProviders is called', async () => {
const mockProvider = new MockProvider('test-completion-provider');
await setDefaultCompletionProviders(mockProvider);
const providers = await getDefaultProviders();
expect(providers.gradingJsonProvider.id()).toBe('test-completion-provider');
expect(providers.gradingProvider.id()).toBe('test-completion-provider');
expect(providers.suggestionsProvider.id()).toBe('test-completion-provider');
expect(providers.synthesizeProvider.id()).toBe('test-completion-provider');
expect(providers.embeddingProvider.id()).not.toBe('test-completion-provider');
});
it('should override embedding provider when setDefaultEmbeddingProviders is called', async () => {
const mockProvider = new MockProvider('test-embedding-provider');
await setDefaultEmbeddingProviders(mockProvider);
const providers = await getDefaultProviders();
expect(providers.embeddingProvider.id()).toBe('test-embedding-provider');
expect(providers.gradingJsonProvider.id()).not.toBe('test-embedding-provider');
expect(providers.gradingProvider.id()).not.toBe('test-embedding-provider');
expect(providers.suggestionsProvider.id()).not.toBe('test-embedding-provider');
expect(providers.synthesizeProvider.id()).not.toBe('test-embedding-provider');
});
it('should allow both completion and embedding provider overrides simultaneously', async () => {
const mockCompletionProvider = new MockProvider('test-completion-provider');
const mockEmbeddingProvider = new MockProvider('test-embedding-provider');
await setDefaultCompletionProviders(mockCompletionProvider);
await setDefaultEmbeddingProviders(mockEmbeddingProvider);
const providers = await getDefaultProviders();
expect(providers.gradingJsonProvider.id()).toBe('test-completion-provider');
expect(providers.gradingProvider.id()).toBe('test-completion-provider');
expect(providers.suggestionsProvider.id()).toBe('test-completion-provider');
expect(providers.synthesizeProvider.id()).toBe('test-completion-provider');
expect(providers.embeddingProvider.id()).toBe('test-embedding-provider');
});
it('should use AzureModerationProvider when AZURE_CONTENT_SAFETY_ENDPOINT is set', async () => {
mockProcessEnv({ AZURE_CONTENT_SAFETY_ENDPOINT: 'https://test-endpoint.com' });
mockProcessEnv({ AZURE_API_KEY: 'test-api-key' });
const providers = await getDefaultProviders();
expect(providers.moderationProvider).toBeInstanceOf(AzureModerationProvider);
expect((providers.moderationProvider as AzureModerationProvider).modelName).toBe(
'text-content-safety',
);
});
it('should use DefaultModerationProvider when AZURE_CONTENT_SAFETY_ENDPOINT is not set', async () => {
mockProcessEnv({ AZURE_CONTENT_SAFETY_ENDPOINT: undefined });
const providers = await getDefaultProviders();
expect(providers.moderationProvider).toBe(DefaultModerationProvider);
});
it('should use AzureModerationProvider when AZURE_CONTENT_SAFETY_ENDPOINT is provided via env overrides', async () => {
const envOverrides: EnvOverrides = {
AZURE_API_KEY: 'test-api-key',
AZURE_CONTENT_SAFETY_ENDPOINT: 'https://test-endpoint.com',
} as EnvOverrides;
const providers = await getDefaultProviders(envOverrides);
expect(providers.moderationProvider).toBeInstanceOf(AzureModerationProvider);
expect((providers.moderationProvider as AzureModerationProvider).modelName).toBe(
'text-content-safety',
);
});
it('should use Azure moderation provider with custom configuration', async () => {
const envOverrides: EnvOverrides = {
AZURE_API_KEY: 'test-api-key',
AZURE_CONTENT_SAFETY_ENDPOINT: 'https://test-endpoint.com',
AZURE_CONTENT_SAFETY_API_KEY: 'test-api-key',
AZURE_CONTENT_SAFETY_API_VERSION: '2024-01-01',
} as EnvOverrides;
const providers = await getDefaultProviders(envOverrides);
expect(providers.moderationProvider).toBeInstanceOf(AzureModerationProvider);
const moderationProvider = providers.moderationProvider as AzureModerationProvider;
expect(moderationProvider.modelName).toBe('text-content-safety');
expect(moderationProvider.endpoint).toBe('https://test-endpoint.com');
expect(moderationProvider.apiVersion).toBe('2024-01-01');
});
it('should use Mistral providers when MISTRAL_API_KEY is set', async () => {
mockProcessEnv({ MISTRAL_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.embeddingProvider).toBe(MistralEmbeddingProvider);
expect(providers.gradingJsonProvider).toBe(MistralGradingJsonProvider);
expect(providers.gradingProvider).toBe(MistralGradingProvider);
expect(providers.suggestionsProvider).toBe(MistralSuggestionsProvider);
expect(providers.synthesizeProvider).toBe(MistralSynthesizeProvider);
});
it('should use xAI providers when XAI_API_KEY is set', async () => {
mockProcessEnv({ XAI_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
// xAI has no public embeddings/moderation API, so we fall back to OpenAI for those.
expect(providers.embeddingProvider).toBe(OpenAiEmbeddingProvider);
expect(providers.gradingJsonProvider.id()).toBe('xai:grok-4.3');
expect(providers.gradingProvider.id()).toBe('xai:grok-4.3');
expect(providers.suggestionsProvider.id()).toBe('xai:grok-4.3');
expect(providers.synthesizeProvider.id()).toBe('xai:grok-4.3');
expect(providers.webSearchProvider?.id()).toBe('xai:responses:grok-4.3');
});
it('should use Codex SDK providers when ChatGPT/Codex credentials exist without API provider keys', async () => {
vi.mocked(hasCodexDefaultCredentials).mockReturnValue(true);
const providers = await getDefaultProviders();
expect(providers.gradingJsonProvider.id()).toBe('openai:codex-sdk');
expect(providers.gradingProvider.id()).toBe('openai:codex-sdk');
expect(providers.llmRubricProvider?.id()).toBe('openai:codex-sdk');
expect(providers.suggestionsProvider.id()).toBe('openai:codex-sdk');
expect(providers.synthesizeProvider.id()).toBe('openai:codex-sdk');
expect(providers.webSearchProvider?.id()).toBe('openai:codex-sdk');
expect(providers.webSearchProvider?.config?.web_search_mode).toBe('live');
expect(providers.embeddingProvider.id()).toBe('openai:text-embedding-3-large');
expect(providers.moderationProvider).toBe(DefaultModerationProvider);
});
it('should prefer OpenAI API defaults over Codex SDK defaults when OPENAI_API_KEY exists', async () => {
mockProcessEnv({ OPENAI_API_KEY: 'test-openai-key' });
vi.mocked(hasCodexDefaultCredentials).mockReturnValue(true);
const providers = await getDefaultProviders();
expect(providers.gradingJsonProvider).toBe(OpenAiGradingJsonProvider);
expect(providers.gradingProvider).toBe(OpenAiGradingProvider);
expect(providers.suggestionsProvider).toBe(OpenAiSuggestionsProvider);
expect(providers.synthesizeProvider).toBe(OpenAiGradingJsonProvider);
});
it('should prefer Mistral defaults over Codex SDK defaults when MISTRAL_API_KEY exists', async () => {
mockProcessEnv({ MISTRAL_API_KEY: 'test-mistral-key' });
vi.mocked(hasCodexDefaultCredentials).mockReturnValue(true);
const providers = await getDefaultProviders();
expect(providers.gradingJsonProvider).toBe(MistralGradingJsonProvider);
expect(providers.gradingProvider).toBe(MistralGradingProvider);
expect(providers.suggestionsProvider).toBe(MistralSuggestionsProvider);
expect(providers.synthesizeProvider).toBe(MistralSynthesizeProvider);
});
it('should prefer xAI defaults over Codex SDK defaults when XAI_API_KEY exists', async () => {
mockProcessEnv({ XAI_API_KEY: 'test-xai-key' });
vi.mocked(hasCodexDefaultCredentials).mockReturnValue(true);
const providers = await getDefaultProviders();
expect(providers.gradingProvider.id()).toBe('xai:grok-4.3');
expect(providers.embeddingProvider).toBe(OpenAiEmbeddingProvider);
});
it('should probe Google default credentials once per provider resolution', async () => {
vi.mocked(hasGoogleDefaultCredentials).mockResolvedValue(false);
await getDefaultProviders();
expect(hasGoogleDefaultCredentials).toHaveBeenCalledTimes(1);
});
it('should not probe Google default credentials when Azure is preferred', async () => {
mockProcessEnv({ AZURE_OPENAI_API_KEY: 'azure-key' });
mockProcessEnv({ AZURE_DEPLOYMENT_NAME: 'azure-chat' });
mockProcessEnv({ AZURE_OPENAI_DEPLOYMENT_NAME: 'azure-chat' });
await getDefaultProviders();
expect(hasGoogleDefaultCredentials).not.toHaveBeenCalled();
});
it('should use Mistral providers when provided via env overrides', async () => {
const envOverrides: EnvOverrides = {
MISTRAL_API_KEY: 'test-key',
} as EnvOverrides;
const providers = await getDefaultProviders(envOverrides);
expect(providers.embeddingProvider).toBe(MistralEmbeddingProvider);
expect(providers.gradingJsonProvider).toBe(MistralGradingJsonProvider);
expect(providers.gradingProvider).toBe(MistralGradingProvider);
expect(providers.suggestionsProvider).toBe(MistralSuggestionsProvider);
expect(providers.synthesizeProvider).toBe(MistralSynthesizeProvider);
});
it('should use xAI providers when provided via env overrides', async () => {
const envOverrides: EnvOverrides = {
XAI_API_KEY: 'test-key',
} as EnvOverrides;
const providers = await getDefaultProviders(envOverrides);
expect(providers.embeddingProvider).toBe(OpenAiEmbeddingProvider);
expect(providers.gradingJsonProvider.id()).toBe('xai:grok-4.3');
expect(providers.gradingProvider.id()).toBe('xai:grok-4.3');
expect(providers.suggestionsProvider.id()).toBe('xai:grok-4.3');
expect(providers.synthesizeProvider.id()).toBe('xai:grok-4.3');
expect(providers.webSearchProvider?.id()).toBe('xai:responses:grok-4.3');
});
it('should prefer Mistral defaults over xAI defaults when both keys exist', async () => {
mockProcessEnv({ MISTRAL_API_KEY: 'mistral-key' });
mockProcessEnv({ XAI_API_KEY: 'xai-key' });
const providers = await getDefaultProviders();
expect(providers.embeddingProvider).toBe(MistralEmbeddingProvider);
expect(providers.gradingProvider).toBe(MistralGradingProvider);
expect(providers.gradingProvider.id()).not.toBe('xai:grok-4.3');
});
it('should not use Mistral providers when OpenAI credentials exist', async () => {
mockProcessEnv({ MISTRAL_API_KEY: 'test-key' });
mockProcessEnv({ OPENAI_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.embeddingProvider).not.toBe(MistralEmbeddingProvider);
expect(providers.gradingJsonProvider).not.toBe(MistralGradingJsonProvider);
expect(providers.gradingProvider).not.toBe(MistralGradingProvider);
expect(providers.suggestionsProvider).not.toBe(MistralSuggestionsProvider);
expect(providers.synthesizeProvider).not.toBe(MistralSynthesizeProvider);
});
it('should not use Mistral providers when Anthropic credentials exist', async () => {
mockProcessEnv({ MISTRAL_API_KEY: 'test-key' });
mockProcessEnv({ ANTHROPIC_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.embeddingProvider).not.toBe(MistralEmbeddingProvider);
expect(providers.gradingJsonProvider).not.toBe(MistralGradingJsonProvider);
expect(providers.gradingProvider).not.toBe(MistralGradingProvider);
expect(providers.suggestionsProvider).not.toBe(MistralSuggestionsProvider);
expect(providers.synthesizeProvider).not.toBe(MistralSynthesizeProvider);
});
describe('Google AI Studio provider selection', () => {
it('should use Google AI Studio providers when GEMINI_API_KEY is set', async () => {
mockProcessEnv({ GEMINI_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.gradingProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.llmRubricProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.embeddingProvider).toBeInstanceOf(VertexEmbeddingProvider); // Falls back to Vertex
});
it('should use Google AI Studio providers when GOOGLE_API_KEY is set', async () => {
mockProcessEnv({ GOOGLE_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.gradingProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.llmRubricProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.embeddingProvider).toBeInstanceOf(VertexEmbeddingProvider); // Falls back to Vertex
});
it('should use Google AI Studio providers when PALM_API_KEY is set', async () => {
mockProcessEnv({ PALM_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.gradingProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.llmRubricProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.embeddingProvider).toBeInstanceOf(VertexEmbeddingProvider); // Falls back to Vertex
});
it('should use Google AI Studio providers when provided via env overrides', async () => {
const envOverrides: EnvOverrides = {
GEMINI_API_KEY: 'test-key',
} as EnvOverrides;
const providers = await getDefaultProviders(envOverrides);
expect(providers.gradingProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.llmRubricProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.embeddingProvider).toBeInstanceOf(VertexEmbeddingProvider); // Falls back to Vertex
});
it('should not use Google AI Studio providers when OpenAI credentials exist', async () => {
mockProcessEnv({ GEMINI_API_KEY: 'test-key' });
mockProcessEnv({ OPENAI_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.gradingProvider).not.toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).not.toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).not.toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).not.toBeInstanceOf(AIStudioChatProvider);
});
it('should not use Google AI Studio providers when Anthropic credentials exist', async () => {
mockProcessEnv({ GEMINI_API_KEY: 'test-key' });
mockProcessEnv({ ANTHROPIC_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.gradingProvider).not.toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).not.toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).not.toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).not.toBeInstanceOf(AIStudioChatProvider);
});
it('should prefer Google AI Studio over Vertex when both credentials are available', async () => {
mockProcessEnv({ GEMINI_API_KEY: 'test-key' });
// hasGoogleDefaultCredentials is mocked to return false, but in practice
// AI Studio should be preferred over Vertex in the provider selection order
const providers = await getDefaultProviders();
expect(providers.gradingProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).toBeInstanceOf(AIStudioChatProvider);
});
it('should prefer Google AI Studio over Mistral when both credentials are available', async () => {
mockProcessEnv({ GEMINI_API_KEY: 'test-key' });
mockProcessEnv({ MISTRAL_API_KEY: 'test-key' });
const providers = await getDefaultProviders();
expect(providers.gradingProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingJsonProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.suggestionsProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.synthesizeProvider).toBeInstanceOf(AIStudioChatProvider);
expect(providers.gradingProvider).not.toBe(MistralGradingProvider);
expect(providers.gradingJsonProvider).not.toBe(MistralGradingJsonProvider);
});
});
});