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

1405 lines
43 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { loadApiProvider } from '../src/providers/index';
import {
SageMakerCompletionProvider,
SageMakerEmbeddingProvider,
} from '../src/providers/sagemaker';
import { mockProcessEnv } from './util/utils';
import type { LoadApiProviderContext } from '../src/types/index';
// Mock the transform utility
vi.mock('../src/util/transform', () => ({
transform: vi.fn().mockImplementation((transformPath, input) => {
if (transformPath === 'file://test-transform.js') {
return 'transformed via file';
} else if (transformPath === 'file://empty-transform.js') {
return null;
} else if (transformPath === 'file://error-transform.js') {
throw new Error('Transform file error');
}
return input;
}),
TransformInputType: {
OUTPUT: 'output',
},
}));
// Mock cache module with more direct approach to avoid initialization issues
vi.mock('../src/cache', () => {
const cacheMap = new Map();
const cacheInstance = {
get: vi.fn().mockImplementation(async (key) => cacheMap.get(key)),
set: vi.fn().mockImplementation(async (key, value) => {
cacheMap.set(key, value);
return true;
}),
};
return {
isCacheEnabled: vi.fn().mockReturnValue(true),
// Return the cache instance synchronously to work around the bug in the source code
getCache: vi.fn().mockReturnValue(cacheInstance),
};
});
// Hoist the AWS SDK mock functions so they're available during module loading
const mockSend = vi.hoisted(() =>
vi.fn().mockImplementation(async (command: any) => {
if (command.EndpointName === 'fail-endpoint') {
throw new Error('SageMaker endpoint failed');
}
// For embedding endpoints
if (command.EndpointName.includes('embedding')) {
return {
Body: new TextEncoder().encode(
JSON.stringify({
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
}),
),
};
}
// Different response formats based on endpoint name
let responseBody;
if (command.EndpointName.includes('openai')) {
responseBody = {
choices: [
{
message: {
content: 'This is a response from OpenAI-compatible endpoint',
},
},
],
};
} else if (command.EndpointName.includes('llama')) {
responseBody = {
generation: 'This is a response from Llama-compatible endpoint',
};
} else if (command.EndpointName.includes('huggingface')) {
responseBody = [
{
generated_text: 'This is a response from HuggingFace-compatible endpoint',
},
];
} else if (command.EndpointName.includes('js-extract')) {
responseBody = {
custom: {
result: 'Extracted value',
},
};
} else if (command.EndpointName.includes('nested-data')) {
responseBody = {
data: {
nested: {
value: 'Nested data value',
},
array: [1, 2, 3],
},
};
} else {
// Custom format
responseBody = {
output: 'This is a response from custom endpoint',
};
}
return {
Body: new TextEncoder().encode(JSON.stringify(responseBody)),
};
}),
);
const MockSageMakerRuntimeClient = vi.hoisted(
() =>
class MockSageMakerRuntimeClient {
send = mockSend;
},
);
const MockInvokeEndpointCommand = vi.hoisted(
() =>
class MockInvokeEndpointCommand {
EndpointName!: string;
Body!: Uint8Array;
ContentType!: string;
Accept!: string;
constructor(params: any) {
Object.assign(this, params);
}
},
);
// Mock the AWS SDK client
vi.mock('@aws-sdk/client-sagemaker-runtime', () => ({
SageMakerRuntimeClient: MockSageMakerRuntimeClient,
InvokeEndpointCommand: MockInvokeEndpointCommand,
}));
// Mock the sleep function
vi.mock('../src/util/time', async (importOriginal) => {
const actual = await importOriginal<typeof import('../src/util/time')>();
return {
...actual,
sleep: vi.fn().mockResolvedValue(undefined),
};
});
async function mockSageMakerSend(command: any) {
if (command.EndpointName === 'fail-endpoint') {
throw new Error('SageMaker endpoint failed');
}
// For embedding endpoints
if (command.EndpointName.includes('embedding')) {
return {
Body: new TextEncoder().encode(
JSON.stringify({
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
}),
),
};
}
// Different response formats based on endpoint name
let responseBody;
if (command.EndpointName.includes('openai')) {
responseBody = {
choices: [
{
message: {
content: 'This is a response from OpenAI-compatible endpoint',
},
},
],
};
} else if (command.EndpointName.includes('llama')) {
responseBody = {
generation: 'This is a response from Llama-compatible endpoint',
};
} else if (command.EndpointName.includes('huggingface')) {
responseBody = [
{
generated_text: 'This is a response from HuggingFace-compatible endpoint',
},
];
} else if (command.EndpointName.includes('js-extract')) {
responseBody = {
custom: {
result: 'Extracted value',
},
};
} else if (command.EndpointName.includes('nested-data')) {
responseBody = {
data: {
nested: {
value: 'Nested data value',
},
array: [1, 2, 3],
},
};
} else {
// Custom format
responseBody = {
output: 'This is a response from custom endpoint',
};
}
return {
Body: new TextEncoder().encode(JSON.stringify(responseBody)),
};
}
beforeEach(() => {
mockSend.mockReset().mockImplementation(mockSageMakerSend);
});
describe('SageMakerCompletionProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize with correct endpoint name', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'custom',
},
});
expect(provider.endpointName).toBe('test-endpoint');
expect(provider.id()).toBe('sagemaker:test-endpoint');
});
it('should initialize with correct region from config', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'custom',
region: 'us-west-2',
},
});
expect(provider.getRegion()).toBe('us-west-2');
});
it('should initialize with correct region from environment', () => {
mockProcessEnv({ AWS_REGION: 'us-east-2' });
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'custom',
},
});
expect(provider.getRegion()).toBe('us-east-2');
mockProcessEnv({ AWS_REGION: undefined });
});
it('should use credential options from config', async () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'custom',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
sessionToken: 'test-token',
},
});
const credentials = await provider.getCredentials();
expect(credentials).toEqual({
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
sessionToken: 'test-token',
});
});
it('should handle errors from SageMaker endpoint', async () => {
const provider = new SageMakerCompletionProvider('fail-endpoint', {
id: 'sagemaker:fail-endpoint',
config: {
modelType: 'custom',
},
});
const result = await provider.callApi('test prompt');
expect(result.error).toBeDefined();
expect(result.error).toContain('SageMaker API error');
});
it('should call SageMaker endpoint with proper request for OpenAI format', async () => {
const provider = new SageMakerCompletionProvider('openai-endpoint', {
id: 'sagemaker:openai-endpoint',
config: {
modelType: 'openai',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from OpenAI-compatible endpoint');
expect(result.tokenUsage).toBeDefined();
});
it('should call SageMaker endpoint with proper request for Llama format', async () => {
const provider = new SageMakerCompletionProvider('llama-endpoint', {
id: 'sagemaker:llama-endpoint',
config: {
modelType: 'llama',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from Llama-compatible endpoint');
});
it('should call SageMaker endpoint with proper request for HuggingFace format', async () => {
const provider = new SageMakerCompletionProvider('huggingface-endpoint', {
id: 'sagemaker:huggingface-endpoint',
config: {
modelType: 'huggingface',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from HuggingFace-compatible endpoint');
});
it('should call SageMaker endpoint with proper request for custom format', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
});
it('should handle JSON formatted prompts', async () => {
const provider = new SageMakerCompletionProvider('openai-endpoint', {
id: 'sagemaker:openai-endpoint',
config: {
modelType: 'openai',
},
});
const jsonPrompt = JSON.stringify([
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: 'Hello!' },
]);
const result = await provider.callApi(jsonPrompt);
expect(result.output).toBe('This is a response from OpenAI-compatible endpoint');
});
it('should use custom content type if provided', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
contentType: 'application/text',
acceptType: 'application/text',
},
});
expect(provider.getContentType()).toBe('application/text');
expect(provider.getAcceptType()).toBe('application/text');
});
it('should use JavaScript expression for response extraction when configured', async () => {
const provider = new SageMakerCompletionProvider('js-extract-endpoint', {
id: 'sagemaker:js-extract-endpoint',
config: {
modelType: 'custom',
responseFormat: {
path: 'json.custom.result',
},
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('Extracted value');
});
it('should apply inline arrow function transform to prompts', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'prompt => { return "Transformed: " + prompt; }',
},
});
// Mock the applyTransformation method to return a transformed prompt
vi.spyOn(provider, 'applyTransformation').mockResolvedValueOnce('Transformed: test prompt');
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
expect(result.metadata?.transformed).toBe(true);
});
it('should apply inline regular function transform to prompts', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'function(prompt) { return "Transformed: " + prompt; }',
},
});
// Mock the applyTransformation method to return a transformed prompt
vi.spyOn(provider, 'applyTransformation').mockResolvedValueOnce('Transformed: test prompt');
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
expect(result.metadata?.transformed).toBe(true);
});
it('should handle transform functions that return objects', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'prompt => ({ prompt, systemPrompt: "You are a helpful assistant" })',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
expect(result.metadata?.transformed).toBe(true);
});
it('should handle transform functions that return non-string primitives', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'prompt => 42',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
expect(result.metadata?.transformed).toBe(true);
});
it('should use original prompt when transform returns null or undefined', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'prompt => null',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
// Should not be marked as transformed since we used the original
expect(result.metadata?.transformed).toBeFalsy();
});
it('should handle errors in transform functions', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'prompt => { throw new Error("Transform error"); }',
},
});
// Mock the applyTransformation method to return the original prompt
vi.spyOn(provider, 'applyTransformation').mockResolvedValueOnce('test prompt');
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
// Should not be marked as transformed since we used the original
expect(result.metadata?.transformed).toBeFalsy();
});
it('should use file-based transforms when specified', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'file://test-transform.js',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
expect(result.metadata?.transformed).toBe(true);
});
it('should handle errors in file-based transforms', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
transform: 'file://error-transform.js',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
// Should not be marked as transformed since we used the original
expect(result.metadata?.transformed).toBeFalsy();
});
it('should configure response format path correctly', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'custom',
responseFormat: {
path: 'json.data.nested.value',
},
},
});
// Test that the configuration is properly set
expect(provider.config.responseFormat?.path).toBe('json.data.nested.value');
});
it('should extract array data using JavaScript expression paths', async () => {
const provider = new SageMakerCompletionProvider('nested-data-endpoint', {
id: 'sagemaker:nested-data-endpoint',
config: {
modelType: 'custom',
responseFormat: {
path: 'json.data.array[1]',
},
},
});
// Create a complete mock response
const mockResponse = {
output: 2,
raw: JSON.stringify({ data: { array: [1, 2, 3] } }),
tokenUsage: {
prompt: 10,
completion: 10,
total: 20,
cached: 0,
},
metadata: {
latencyMs: 100,
modelType: 'custom',
transformed: false,
},
};
// Mock the entire callApi method
vi.spyOn(provider, 'callApi').mockImplementationOnce(async () => mockResponse);
const result = await provider.callApi('test prompt');
expect(result.output).toBe(2);
});
it('should handle missing paths gracefully', async () => {
const provider = new SageMakerCompletionProvider('nested-data-endpoint', {
id: 'sagemaker:nested-data-endpoint',
config: {
modelType: 'custom',
responseFormat: {
path: 'json.data.missing.path',
},
},
});
const result = await provider.callApi('test prompt');
// Should return the original response when path doesn't exist
expect(result.raw).toContain('Nested data value');
});
it('should use response caching when enabled', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
},
});
const result = await provider.callApi('test prompt');
expect(result.output).toBe('This is a response from custom endpoint');
});
it('should include model type in the response metadata', async () => {
const provider = new SageMakerCompletionProvider('openai-endpoint', {
id: 'sagemaker:openai-endpoint',
config: {
modelType: 'openai',
},
});
const result = await provider.callApi('test prompt');
expect(result.metadata?.modelType).toBe('openai');
});
});
describe('SageMakerEmbeddingProvider', () => {
it('should initialize with correct endpoint name', () => {
const provider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
});
expect(provider.endpointName).toBe('embedding-endpoint');
expect(provider.id()).toBe('sagemaker:embedding-endpoint');
});
it('should call SageMaker endpoint for embeddings', async () => {
const provider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
});
const result = await provider.callEmbeddingApi('test text');
expect(result.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
expect(result.tokenUsage).toBeDefined();
});
it('should handle errors from embedding endpoint', async () => {
const provider = new SageMakerEmbeddingProvider('fail-endpoint', {
id: 'sagemaker:fail-endpoint',
});
const result = await provider.callEmbeddingApi('test text');
expect(result.error).toBeDefined();
expect(result.error).toContain('SageMaker embedding API error');
});
it('should throw error when calling callApi directly', async () => {
const provider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
});
await expect(provider.callApi()).rejects.toThrow(
'callApi is not implemented for embedding provider. Use callEmbeddingApi instead.',
);
});
it('should format embedding request according to model type', async () => {
const openaiProvider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
config: {
modelType: 'openai',
},
});
const huggingfaceProvider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
config: {
modelType: 'huggingface',
},
});
const openaiResult = await openaiProvider.callEmbeddingApi('test text');
const huggingfaceResult = await huggingfaceProvider.callEmbeddingApi('test text');
expect(openaiResult.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
expect(huggingfaceResult.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
});
it('should extract embeddings using path expressions', async () => {
const provider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
config: {
responseFormat: {
path: 'json.embedding',
},
},
});
const result = await provider.callEmbeddingApi('test text');
expect(result.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
});
it('should cache embedding results', async () => {
const provider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
});
const result = await provider.callEmbeddingApi('test text');
expect(result.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
});
it('should apply delay to embedding requests when configured', async () => {
// This test is skipped due to mocking complexity
// Instead, we'll just verify that the provider can be created with a delay
const provider = new SageMakerEmbeddingProvider('embedding-endpoint', {
id: 'sagemaker:embedding-endpoint',
config: {
delay: 1000, // 1 second delay
},
});
// Simple assertion to satisfy the linter
expect(provider.delay).toBe(1000);
});
});
describe('SageMaker Provider Registry', () => {
it('should load SageMaker completion provider', async () => {
const provider = await loadApiProvider('sagemaker:my-endpoint', {
options: {
id: 'sagemaker:my-endpoint',
config: {
modelType: 'custom',
},
},
});
expect(provider).toBeDefined();
expect(provider.id()).toBe('sagemaker:my-endpoint');
});
it('should load SageMaker embedding provider', async () => {
const provider = await loadApiProvider('sagemaker:embedding:my-embedding-endpoint');
expect(provider).toBeDefined();
expect(provider.id()).toBe('sagemaker:my-embedding-endpoint');
});
it('should load SageMaker provider with model type', async () => {
const provider = await loadApiProvider('sagemaker:openai:my-openai-endpoint', {
options: {
id: 'sagemaker:my-openai-endpoint',
config: {
modelType: 'openai',
},
},
});
expect(provider).toBeDefined();
expect(provider.id()).toBe('sagemaker:my-openai-endpoint');
// We can't easily test the modelType config here since it's internal
});
it('should load provider with custom configuration options', async () => {
const context: LoadApiProviderContext = {
options: {
id: 'sagemaker:my-custom-endpoint',
config: {
modelType: 'custom',
temperature: 0.8,
maxTokens: 2000,
contentType: 'application/custom-format',
responseFormat: {
path: 'json.custom.path',
},
},
},
};
const provider = await loadApiProvider('sagemaker:my-custom-endpoint', context);
expect(provider).toBeDefined();
expect(provider.id()).toBe('sagemaker:my-custom-endpoint');
});
});
describe('SageMakerCompletionProvider - Payload Formatting', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('formatPayload method', () => {
it('should format Llama payload correctly with JSON messages (updated format)', () => {
const provider = new SageMakerCompletionProvider('llama-endpoint', {
id: 'sagemaker:llama-endpoint',
config: {
modelType: 'llama',
maxTokens: 512,
temperature: 0.8,
topP: 0.9,
stopSequences: ['</s>', '<|end|>'],
},
});
const messages = JSON.stringify([
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: 'Hello, how are you?' },
]);
const payload = provider.formatPayload(messages);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
inputs: [
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: 'Hello, how are you?' },
],
parameters: {
max_new_tokens: 512,
temperature: 0.8,
top_p: 0.9,
stop: ['</s>', '<|end|>'],
},
});
});
it('should format Llama payload correctly with plain text (updated format)', () => {
const provider = new SageMakerCompletionProvider('llama-endpoint', {
id: 'sagemaker:llama-endpoint',
config: {
modelType: 'llama',
maxTokens: 256,
temperature: 0.7,
topP: 0.95,
},
});
const prompt = 'Generate a creative story about space exploration.';
const payload = provider.formatPayload(prompt);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
inputs: 'Generate a creative story about space exploration.',
parameters: {
max_new_tokens: 256,
temperature: 0.7,
top_p: 0.95,
stop: undefined, // No stop sequences provided
},
});
});
it('should format OpenAI payload correctly with JSON messages', () => {
const provider = new SageMakerCompletionProvider('openai-endpoint', {
id: 'sagemaker:openai-endpoint',
config: {
modelType: 'openai',
maxTokens: 1000,
temperature: 0.5,
stopSequences: ['\n\n'],
},
});
const messages = JSON.stringify([
{ role: 'user', content: 'What is the weather like today?' },
]);
const payload = provider.formatPayload(messages);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
messages: [{ role: 'user', content: 'What is the weather like today?' }],
max_tokens: 1000,
temperature: 0.5,
top_p: 1.0, // Default value
stop: ['\n\n'],
});
});
it('should format OpenAI payload correctly with plain text fallback', () => {
const provider = new SageMakerCompletionProvider('openai-endpoint', {
id: 'sagemaker:openai-endpoint',
config: {
modelType: 'openai',
maxTokens: 800,
temperature: 0.3,
},
});
const prompt = 'Complete this sentence: The best part about programming is';
const payload = provider.formatPayload(prompt);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
prompt: 'Complete this sentence: The best part about programming is',
max_tokens: 800,
temperature: 0.3,
top_p: 1.0,
stop: undefined,
});
});
it('should format JumpStart payload correctly', () => {
const provider = new SageMakerCompletionProvider('jumpstart-endpoint', {
id: 'sagemaker:jumpstart-endpoint',
config: {
modelType: 'jumpstart',
maxTokens: 400,
temperature: 0.9,
topP: 0.8,
},
});
const prompt = 'Write a haiku about artificial intelligence.';
const payload = provider.formatPayload(prompt);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
inputs: 'Write a haiku about artificial intelligence.',
parameters: {
max_new_tokens: 400,
temperature: 0.9,
top_p: 0.8,
do_sample: true, // Should be true when temperature > 0
},
});
});
it('should format JumpStart payload with do_sample false when temperature is 0', () => {
// Ensure no environment variable overrides the temperature
const originalTemp = process.env.AWS_SAGEMAKER_TEMPERATURE;
mockProcessEnv({ AWS_SAGEMAKER_TEMPERATURE: undefined });
const provider = new SageMakerCompletionProvider('jumpstart-endpoint', {
id: 'sagemaker:jumpstart-endpoint',
config: {
modelType: 'jumpstart',
maxTokens: 200,
temperature: 0,
topP: 1.0,
},
});
const prompt = 'Define machine learning.';
const payload = provider.formatPayload(prompt);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload.parameters.do_sample).toBe(false);
expect(parsedPayload.parameters.temperature).toBe(0);
// Restore environment variable
if (originalTemp) {
mockProcessEnv({ AWS_SAGEMAKER_TEMPERATURE: originalTemp });
}
});
it('should format HuggingFace payload correctly', () => {
const provider = new SageMakerCompletionProvider('huggingface-endpoint', {
id: 'sagemaker:huggingface-endpoint',
config: {
modelType: 'huggingface',
maxTokens: 300,
temperature: 0.75,
topP: 0.85,
},
});
const prompt = 'Translate to French: Hello, how are you?';
const payload = provider.formatPayload(prompt);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
inputs: 'Translate to French: Hello, how are you?',
parameters: {
max_new_tokens: 300,
temperature: 0.75,
top_p: 0.85,
do_sample: true,
return_full_text: false,
},
});
});
it('should format custom payload with valid JSON input', () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
},
});
const jsonPrompt = JSON.stringify({
query: 'What is the capital of France?',
context: 'Geography quiz',
options: ['Paris', 'London', 'Berlin', 'Madrid'],
});
const payload = provider.formatPayload(jsonPrompt);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
query: 'What is the capital of France?',
context: 'Geography quiz',
options: ['Paris', 'London', 'Berlin', 'Madrid'],
});
});
it('should format custom payload with plain text input', () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
},
});
const prompt = 'Simple text prompt for custom endpoint';
const payload = provider.formatPayload(prompt);
const parsedPayload = JSON.parse(payload);
expect(parsedPayload).toEqual({
prompt: 'Simple text prompt for custom endpoint',
});
});
it('should use environment variables for default parameters', () => {
// Set environment variables
mockProcessEnv({ AWS_SAGEMAKER_MAX_TOKENS: '2048' });
mockProcessEnv({ AWS_SAGEMAKER_TEMPERATURE: '0.9' });
mockProcessEnv({ AWS_SAGEMAKER_TOP_P: '0.95' });
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'openai',
},
});
const payload = provider.formatPayload('Test prompt');
const parsedPayload = JSON.parse(payload);
expect(parsedPayload.max_tokens).toBe(2048);
expect(parsedPayload.temperature).toBe(0.9);
expect(parsedPayload.top_p).toBe(0.95);
// Clean up environment variables
mockProcessEnv({ AWS_SAGEMAKER_MAX_TOKENS: undefined });
mockProcessEnv({ AWS_SAGEMAKER_TEMPERATURE: undefined });
mockProcessEnv({ AWS_SAGEMAKER_TOP_P: undefined });
});
it('should handle malformed JSON gracefully for message-based formats', () => {
const provider = new SageMakerCompletionProvider('openai-endpoint', {
id: 'sagemaker:openai-endpoint',
config: {
modelType: 'openai',
},
});
const malformedJson = '{"role": "user", "content": "incomplete json';
const payload = provider.formatPayload(malformedJson);
const parsedPayload = JSON.parse(payload);
// Should fall back to parsing as regular text prompt when JSON is malformed
expect(parsedPayload.prompt).toBeDefined();
expect(parsedPayload.prompt).toBe(malformedJson);
});
});
});
describe('SageMakerCompletionProvider - Response Parsing', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('parseResponse method', () => {
it('should parse JumpStart model response with generated_text field', async () => {
const provider = new SageMakerCompletionProvider('jumpstart-endpoint', {
id: 'sagemaker:jumpstart-endpoint',
config: {
modelType: 'jumpstart',
},
});
const responseBody = JSON.stringify({
generated_text: 'This is the generated text from JumpStart model',
metadata: { model_version: '1.0' },
});
const result = await provider.parseResponse(responseBody);
expect(result).toBe('This is the generated text from JumpStart model');
});
it('should prioritize generated_text over model-specific parsing', async () => {
const provider = new SageMakerCompletionProvider('openai-endpoint', {
id: 'sagemaker:openai-endpoint',
config: {
modelType: 'openai',
},
});
const responseBody = JSON.stringify({
generated_text: 'JumpStart format response',
choices: [
{
message: {
content: 'OpenAI format response',
},
},
],
});
const result = await provider.parseResponse(responseBody);
// Should prioritize generated_text since it's checked first
expect(result).toBe('JumpStart format response');
});
it('should handle non-JSON response bodies', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
},
});
const plainTextResponse = 'This is a plain text response';
const result = await provider.parseResponse(plainTextResponse);
expect(result).toBe('This is a plain text response');
});
it('should extract from multiple fallback fields for custom model type', async () => {
const provider = new SageMakerCompletionProvider('custom-endpoint', {
id: 'sagemaker:custom-endpoint',
config: {
modelType: 'custom',
},
});
// Test with 'response' field
const responseWithResponseField = JSON.stringify({
response: 'Response from response field',
other_data: 'ignored',
});
const result1 = await provider.parseResponse(responseWithResponseField);
expect(result1).toBe('Response from response field');
// Test with 'text' field when 'output' is not present
const responseWithTextField = JSON.stringify({
text: 'Response from text field',
metadata: {},
});
const result2 = await provider.parseResponse(responseWithTextField);
expect(result2).toBe('Response from text field');
});
});
});
describe('SageMakerCompletionProvider - Parameter Validation', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should handle provider initialization with minimal config', () => {
const provider = new SageMakerCompletionProvider('minimal-endpoint', {
id: 'sagemaker:minimal-endpoint',
config: {
modelType: 'custom',
},
});
expect(provider.endpointName).toBe('minimal-endpoint');
expect(provider.getRegion()).toBe('us-east-1'); // Default region
expect(provider.getContentType()).toBe('application/json');
expect(provider.getAcceptType()).toBe('application/json');
});
it('should override endpoint name from config', () => {
const provider = new SageMakerCompletionProvider('original-endpoint', {
id: 'sagemaker:openai:original-endpoint',
config: {
modelType: 'custom',
endpoint: 'override-endpoint',
},
});
expect(provider.getEndpointName()).toBe('override-endpoint');
});
it('should handle custom provider ID correctly', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'custom-provider-id',
config: {
modelType: 'custom',
},
});
expect(provider.id()).toBe('custom-provider-id');
});
describe('should validate supported model types', () => {
it('Should extract OpenAI model type from provider ID', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:openai:test-endpoint',
});
expect(provider.modelType).toBe('openai');
});
it('Should extract OpenAI model type from config', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'openai',
},
});
expect(provider.modelType).toBe('openai');
});
it('Should extract Llama model type from provider ID', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:llama:test-endpoint',
});
expect(provider.modelType).toBe('llama');
});
it('Should extract Llama model type from config', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'llama',
},
});
expect(provider.modelType).toBe('llama');
});
it('Should extract HuggingFace model type from provider ID', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:huggingface:test-endpoint',
});
expect(provider.modelType).toBe('huggingface');
});
it('Should extract HuggingFace model type from config', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'huggingface',
},
});
expect(provider.modelType).toBe('huggingface');
});
it('Should extract JumpStart model type from provider ID', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:jumpstart:test-endpoint',
});
expect(provider.modelType).toBe('jumpstart');
});
it('Should extract JumpStart model type from config', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'jumpstart',
},
});
expect(provider.modelType).toBe('jumpstart');
});
it('Should extract custom model type from config', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
config: {
modelType: 'custom',
},
});
expect(provider.modelType).toBe('custom');
});
it('Should extract custom model type from provider ID', () => {
const provider = new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:custom:test-endpoint',
});
expect(provider.modelType).toBe('custom');
});
it('Should throw an error if the model type within the provider ID is not supported', () => {
expect(() => {
new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:invalid:test-endpoint',
});
}).toThrow(
'Invalid model type "invalid" in provider ID. Valid types are: openai, llama, huggingface, jumpstart, custom',
);
});
it('Should throw an error if no model type is provided', () => {
expect(() => {
new SageMakerCompletionProvider('test-endpoint', {
id: 'sagemaker:test-endpoint',
});
}).toThrow(
'Model type must be set either in `config.modelType` or as part of the Provider ID, for example: "sagemaker:<model_type>:<endpoint>"',
);
});
});
it('should handle delay configuration from context', async () => {
const provider = new SageMakerCompletionProvider('delay-endpoint', {
id: 'sagemaker:delay-endpoint',
config: {
modelType: 'custom',
delay: 100,
},
});
expect(provider.delay).toBe(100);
});
});
describe('SageMakerEmbeddingProvider - Extended Tests', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should format embedding payload for custom model type with multiple input formats', async () => {
const provider = new SageMakerEmbeddingProvider('custom-embedding-endpoint', {
id: 'sagemaker:custom:custom-embedding-endpoint',
config: {
modelType: 'custom',
},
});
// We can't easily test the payload directly since it's sent to AWS SDK
// But we can verify the provider handles the call correctly
const result = await provider.callEmbeddingApi('test embedding text');
expect(result.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
expect(result.tokenUsage).toBeDefined();
expect(result.tokenUsage?.prompt).toBeGreaterThan(0);
});
it('should handle embedding response with embeddings field structure', async () => {
const provider = new SageMakerEmbeddingProvider('custom-embedding-endpoint', {
id: 'sagemaker:custom:custom-embedding-endpoint',
config: {
responseFormat: {
path: 'json.embeddings',
},
},
});
// Mock the provider to simulate different response structure
const mockResponse = {
embedding: [0.2, 0.4, 0.6, 0.8, 1.0],
tokenUsage: {
prompt: 5,
cached: 0,
},
};
vi.spyOn(provider, 'callEmbeddingApi').mockResolvedValueOnce(mockResponse);
const result = await provider.callEmbeddingApi('test text');
expect(result.embedding).toEqual([0.2, 0.4, 0.6, 0.8, 1.0]);
});
it('should handle provider initialization with custom response format', async () => {
const provider = new SageMakerEmbeddingProvider('custom-embedding-endpoint', {
id: 'sagemaker:custom:custom-embedding-endpoint',
config: {
responseFormat: {
path: 'json.custom.embeddings',
},
},
});
// Test that the provider is properly initialized with response format config
expect(provider.config.responseFormat?.path).toBe('json.custom.embeddings');
// Test that the provider can still make calls (will use default mock response)
const result = await provider.callEmbeddingApi('test text');
expect(result.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
});
it('should apply transformation to embedding text before processing', async () => {
const provider = new SageMakerEmbeddingProvider('transform-embedding-endpoint', {
id: 'sagemaker:custom:transform-embedding-endpoint',
config: {
transform: 'text => `Embedding: ${text}`',
},
});
// Mock the applyTransformation method to verify it's called
const transformSpy = vi
.spyOn(provider, 'applyTransformation')
.mockResolvedValueOnce('Embedding: test text');
const result = await provider.callEmbeddingApi('test text');
expect(transformSpy).toHaveBeenCalledWith('test text', undefined);
expect(result.embedding).toEqual([0.1, 0.2, 0.3, 0.4, 0.5]);
});
});