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

273 lines
10 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { fetchWithCache } from '../../../src/cache';
import {
BedrockMantleChatProvider,
createBedrockMantleChatProvider,
getBedrockMantleChatBaseUrl,
} from '../../../src/providers/bedrock/mantleChat';
import { OpenAiChatCompletionProvider } from '../../../src/providers/openai/chat';
import { HttpRateLimitError } from '../../../src/util/fetch/errors';
import { mockProcessEnv } from '../../util/utils';
vi.mock('../../../src/cache', async (importOriginal) => ({
...(await importOriginal<typeof import('../../../src/cache')>()),
fetchWithCache: vi.fn(),
}));
describe('bedrock mantle Chat Completions provider', () => {
let restoreEnv: (() => void) | undefined;
beforeEach(() => {
vi.mocked(fetchWithCache).mockReset();
});
afterEach(() => {
restoreEnv?.();
restoreEnv = undefined;
vi.resetAllMocks();
});
describe('getBedrockMantleChatBaseUrl', () => {
it('builds the bare /v1 mantle endpoint for ordinary mantle chat models', () => {
expect(getBedrockMantleChatBaseUrl('us-east-1')).toBe(
'https://bedrock-mantle.us-east-1.api.aws/v1',
);
expect(getBedrockMantleChatBaseUrl('us-west-2')).toBe(
'https://bedrock-mantle.us-west-2.api.aws/v1',
);
});
it('builds the /openai/v1 mantle endpoint for xAI chat models', () => {
expect(getBedrockMantleChatBaseUrl('us-west-2', 'xai.grok-4.3')).toBe(
'https://bedrock-mantle.us-west-2.api.aws/openai/v1',
);
});
it('keeps open-weight gpt-oss chat models on the bare /v1 mantle endpoint', () => {
expect(getBedrockMantleChatBaseUrl('us-east-1', 'openai.gpt-oss-20b-1:0')).toBe(
'https://bedrock-mantle.us-east-1.api.aws/v1',
);
});
it('routes Gemma 4 chat models through the /openai/v1 mantle endpoint', () => {
expect(getBedrockMantleChatBaseUrl('us-east-1', 'google.gemma-4-31b')).toBe(
'https://bedrock-mantle.us-east-1.api.aws/openai/v1',
);
});
it('rejects a malformed region', () => {
expect(() => getBedrockMantleChatBaseUrl('evil.com/x')).toThrow(/Invalid AWS region/);
});
});
describe('createBedrockMantleChatProvider', () => {
it('throws a helpful error when no Bedrock API key is configured', () => {
restoreEnv = mockProcessEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined });
expect(() => createBedrockMantleChatProvider('zai.glm-4.6', {})).toThrow(
/AWS_BEARER_TOKEN_BEDROCK/,
);
});
it('targets the mantle /v1 endpoint for the configured region with config.apiKey', () => {
restoreEnv = mockProcessEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined });
const provider = createBedrockMantleChatProvider('deepseek.v3.1', {
config: { region: 'us-west-2', apiKey: 'bedrock-key' },
});
expect(provider).toBeInstanceOf(OpenAiChatCompletionProvider);
expect((provider.config as any).apiBaseUrl).toBe(
'https://bedrock-mantle.us-west-2.api.aws/v1',
);
expect((provider.config as any).apiKey).toBe('bedrock-key');
});
it('falls back to AWS_BEARER_TOKEN_BEDROCK and the default region', () => {
restoreEnv = mockProcessEnv({
AWS_BEARER_TOKEN_BEDROCK: 'env-bedrock-key',
AWS_BEDROCK_REGION: undefined,
AWS_REGION: undefined,
AWS_DEFAULT_REGION: undefined,
});
const provider = createBedrockMantleChatProvider('google.gemma-4-31b', {});
expect((provider.config as any).apiBaseUrl).toBe(
'https://bedrock-mantle.us-east-1.api.aws/openai/v1',
);
expect((provider.config as any).apiKey).toBe('env-bedrock-key');
});
it('defaults Grok mantle chat to its launch region', () => {
restoreEnv = mockProcessEnv({
AWS_BEARER_TOKEN_BEDROCK: 'env-bedrock-key',
AWS_BEDROCK_REGION: undefined,
AWS_REGION: undefined,
AWS_DEFAULT_REGION: undefined,
});
const provider = createBedrockMantleChatProvider('xai.grok-4.3', {});
expect((provider.config as any).apiBaseUrl).toBe(
'https://bedrock-mantle.us-west-2.api.aws/openai/v1',
);
});
it('omits the inherited temperature default when Grok temperature is not configured', async () => {
restoreEnv = mockProcessEnv({
AWS_BEARER_TOKEN_BEDROCK: 'env-bedrock-key',
OPENAI_TEMPERATURE: undefined,
});
const provider = createBedrockMantleChatProvider('xai.grok-4.3', {
config: { omitDefaults: false },
});
const { body } = await (provider as any).getOpenAiBody('hello');
expect((provider.config as any).omitDefaults).toBe(true);
expect(body.temperature).toBeUndefined();
});
it('treats an unresolved {{env.*}} apiKey template as missing', () => {
restoreEnv = mockProcessEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined });
expect(() =>
createBedrockMantleChatProvider('zai.glm-4.6', {
config: { apiKey: '{{env.AWS_BEARER_TOKEN_BEDROCK}}' },
}),
).toThrow(/AWS_BEARER_TOKEN_BEDROCK/);
});
it('rejects frontier OpenAI models on the mantle chat route', () => {
expect(() =>
createBedrockMantleChatProvider('openai.gpt-5.5', {
config: { apiKey: 'bedrock-key' },
}),
).toThrow(/Use the bare "bedrock:openai\.gpt-5\.5" id/);
});
it('pins the mantle endpoint even when OPENAI_API_HOST is set', () => {
restoreEnv = mockProcessEnv({
AWS_BEARER_TOKEN_BEDROCK: 'env-bedrock-key',
OPENAI_API_HOST: 'unrelated.example.com',
});
const provider = createBedrockMantleChatProvider('zai.glm-4.6', {
config: { region: 'us-west-2' },
});
// Base getApiUrl() would prefer OPENAI_API_HOST; the subclass must override that so the
// Bedrock bearer token is never sent to the wrong host.
expect(provider.getApiUrl()).toBe('https://bedrock-mantle.us-west-2.api.aws/v1');
});
it('sends the real model id and posts to <base>/chat/completions', async () => {
restoreEnv = mockProcessEnv({ AWS_BEARER_TOKEN_BEDROCK: 'env-bedrock-key' });
const provider = createBedrockMantleChatProvider('deepseek.v3.1', {
config: { region: 'us-west-2' },
}) as BedrockMantleChatProvider;
const { body } = await (provider as any).getOpenAiBody('hello');
expect(body.model).toBe('deepseek.v3.1');
expect(`${provider.getApiUrl()}/chat/completions`).toBe(
'https://bedrock-mantle.us-west-2.api.aws/v1/chat/completions',
);
});
it('forwards configured reasoning effort for Gemma 4 chat models', async () => {
const provider = createBedrockMantleChatProvider('google.gemma-4-31b', {
config: { apiKey: 'bedrock-key', reasoning_effort: 'high' },
});
const { body } = await (provider as any).getOpenAiBody('hello');
expect(body.reasoning_effort).toBe('high');
});
it('calls the mantle chat endpoint and tracks token usage', async () => {
vi.mocked(fetchWithCache).mockResolvedValue({
data: {
choices: [{ message: { content: 'Mantle output' }, finish_reason: 'stop' }],
usage: { total_tokens: 7, prompt_tokens: 4, completion_tokens: 3 },
},
cached: false,
status: 200,
statusText: 'OK',
});
const provider = createBedrockMantleChatProvider('deepseek.v3.1', {
config: { region: 'us-west-2', apiKey: 'bedrock-key' },
});
const result = await provider.callApi(JSON.stringify([{ role: 'user', content: 'hello' }]));
expect(fetchWithCache).toHaveBeenCalledWith(
'https://bedrock-mantle.us-west-2.api.aws/v1/chat/completions',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer bedrock-key' }),
}),
expect.any(Number),
'json',
true,
undefined,
);
expect(result.output).toBe('Mantle output');
expect(result.tokenUsage).toEqual({ total: 7, prompt: 4, completion: 3, numRequests: 1 });
});
it('returns HTTP errors from the mantle chat endpoint', async () => {
vi.mocked(fetchWithCache).mockResolvedValue({
data: { error: { message: 'server exploded' } },
cached: false,
status: 500,
statusText: 'Internal Server Error',
});
const provider = createBedrockMantleChatProvider('deepseek.v3.1', {
config: { apiKey: 'bedrock-key' },
});
const result = await provider.callApi('hello');
expect(result.error).toContain('API error: 500 Internal Server Error');
});
it('preserves structured rate limit errors from the mantle chat endpoint', async () => {
vi.mocked(fetchWithCache).mockRejectedValue(
new HttpRateLimitError({ status: 429, code: 'rate_limit_exceeded' }),
);
const provider = createBedrockMantleChatProvider('deepseek.v3.1', {
config: { apiKey: 'bedrock-key' },
});
const result = await provider.callApi('hello');
expect(result.error).toContain('Rate limit exceeded: HTTP 429 Too Many Requests');
});
it('surfaces mantle chat request timeout failures', async () => {
vi.mocked(fetchWithCache).mockRejectedValue(new Error('Request timed out'));
const provider = createBedrockMantleChatProvider('deepseek.v3.1', {
config: { apiKey: 'bedrock-key' },
});
const result = await provider.callApi('hello');
expect(result.error).toContain('API call error: Error: Request timed out');
});
it('uses the OpenAI-compatible chat endpoint and capabilities for Grok', async () => {
restoreEnv = mockProcessEnv({ AWS_BEARER_TOKEN_BEDROCK: 'env-bedrock-key' });
const provider = createBedrockMantleChatProvider('xai.grok-4.3', {
config: {
region: 'us-west-2',
omitDefaults: false,
reasoning_effort: 'high',
temperature: 0,
presence_penalty: 0.5,
frequency_penalty: 0.7,
stop: ['\n'],
},
}) as BedrockMantleChatProvider;
const { body } = await (provider as any).getOpenAiBody('hello');
expect((provider.config as any).omitDefaults).toBe(true);
expect(provider.getApiUrl()).toBe('https://bedrock-mantle.us-west-2.api.aws/openai/v1');
expect(body.model).toBe('xai.grok-4.3');
expect(body.reasoning_effort).toBe('high');
expect(body.temperature).toBe(0);
expect(body.presence_penalty).toBeUndefined();
expect(body.frequency_penalty).toBeUndefined();
expect(body.stop).toBeUndefined();
});
});
});