mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00

* fix: Add default random state parameter to OpenID auth request for providers that require it; ensure passport strategy uses it
* ⌚ refactor: debounce setUserContext to avoid race condition
* refactor: Update OpenID authentication to use randomState from openid-client
* chore: linting in presetSettings type definition
* chore: import order in ModelPanel
* refactor: remove `isLegacyOutput` property from AnthropicClient since only used where defined, add latest models to non-legacy patterns, and remove from client cleanup
* refactor: adjust grid layout in Parameters component for improved responsiveness
* refactor: adjust grid layout in ModelPanel for improved display of model parameters
* test: add cases for maxOutputTokens handling in Claude 4 Sonnet and Opus models
* ci: mock loadCustomConfig in server tests and refactor OpenID route for improved authentication handling
1037 lines
34 KiB
JavaScript
1037 lines
34 KiB
JavaScript
const { SplitStreamHandler } = require('@librechat/agents');
|
|
const { anthropicSettings } = require('librechat-data-provider');
|
|
const AnthropicClient = require('~/app/clients/AnthropicClient');
|
|
|
|
const HUMAN_PROMPT = '\n\nHuman:';
|
|
const AI_PROMPT = '\n\nAssistant:';
|
|
|
|
describe('AnthropicClient', () => {
|
|
let client;
|
|
const model = 'claude-2';
|
|
const parentMessageId = '1';
|
|
const messages = [
|
|
{ role: 'user', isCreatedByUser: true, text: 'Hello', messageId: parentMessageId },
|
|
{ role: 'assistant', isCreatedByUser: false, text: 'Hi', messageId: '2', parentMessageId },
|
|
{
|
|
role: 'user',
|
|
isCreatedByUser: true,
|
|
text: "What's up",
|
|
messageId: '3',
|
|
parentMessageId: '2',
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
const options = {
|
|
modelOptions: {
|
|
model,
|
|
temperature: anthropicSettings.temperature.default,
|
|
},
|
|
};
|
|
client = new AnthropicClient('test-api-key');
|
|
client.setOptions(options);
|
|
});
|
|
|
|
describe('setOptions', () => {
|
|
it('should set the options correctly', () => {
|
|
expect(client.apiKey).toBe('test-api-key');
|
|
expect(client.modelOptions.model).toBe(model);
|
|
expect(client.modelOptions.temperature).toBe(anthropicSettings.temperature.default);
|
|
});
|
|
|
|
it('should set legacy maxOutputTokens for non-Claude-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-2',
|
|
maxOutputTokens: anthropicSettings.maxOutputTokens.default,
|
|
},
|
|
});
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
});
|
|
it('should not set maxOutputTokens if not provided', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3',
|
|
},
|
|
});
|
|
expect(client.modelOptions.maxOutputTokens).toBeUndefined();
|
|
});
|
|
|
|
it('should not set legacy maxOutputTokens for Claude-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-opus-20240229',
|
|
maxOutputTokens: anthropicSettings.legacy.maxOutputTokens.default,
|
|
},
|
|
});
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getSaveOptions', () => {
|
|
it('should return the correct save options', () => {
|
|
const options = client.getSaveOptions();
|
|
expect(options).toHaveProperty('modelLabel');
|
|
expect(options).toHaveProperty('promptPrefix');
|
|
});
|
|
});
|
|
|
|
describe('buildMessages', () => {
|
|
it('should handle promptPrefix from options when promptPrefix argument is not provided', async () => {
|
|
client.options.promptPrefix = 'Test Prefix from options';
|
|
const result = await client.buildMessages(messages, parentMessageId);
|
|
const { prompt } = result;
|
|
expect(prompt).toContain('Test Prefix from options');
|
|
});
|
|
|
|
it('should build messages correctly for chat completion', async () => {
|
|
const result = await client.buildMessages(messages, '2');
|
|
expect(result).toHaveProperty('prompt');
|
|
expect(result.prompt).toContain(HUMAN_PROMPT);
|
|
expect(result.prompt).toContain('Hello');
|
|
expect(result.prompt).toContain(AI_PROMPT);
|
|
expect(result.prompt).toContain('Hi');
|
|
});
|
|
|
|
it('should group messages by the same author', async () => {
|
|
const groupedMessages = messages.map((m) => ({ ...m, isCreatedByUser: true, role: 'user' }));
|
|
const result = await client.buildMessages(groupedMessages, '3');
|
|
expect(result.context).toHaveLength(1);
|
|
|
|
// Check that HUMAN_PROMPT appears only once in the prompt
|
|
const matches = result.prompt.match(new RegExp(HUMAN_PROMPT, 'g'));
|
|
expect(matches).toHaveLength(1);
|
|
|
|
groupedMessages.push({
|
|
role: 'assistant',
|
|
isCreatedByUser: false,
|
|
text: 'I heard you the first time',
|
|
messageId: '4',
|
|
parentMessageId: '3',
|
|
});
|
|
|
|
const result2 = await client.buildMessages(groupedMessages, '4');
|
|
expect(result2.context).toHaveLength(2);
|
|
|
|
// Check that HUMAN_PROMPT appears only once in the prompt
|
|
const human_matches = result2.prompt.match(new RegExp(HUMAN_PROMPT, 'g'));
|
|
const ai_matches = result2.prompt.match(new RegExp(AI_PROMPT, 'g'));
|
|
expect(human_matches).toHaveLength(1);
|
|
expect(ai_matches).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle isEdited condition', async () => {
|
|
const editedMessages = [
|
|
{ role: 'user', isCreatedByUser: true, text: 'Hello', messageId: '1' },
|
|
{ role: 'assistant', isCreatedByUser: false, text: 'Hi', messageId: '2', parentMessageId },
|
|
];
|
|
|
|
const trimmedLabel = AI_PROMPT.trim();
|
|
const result = await client.buildMessages(editedMessages, '2');
|
|
expect(result.prompt.trim().endsWith(trimmedLabel)).toBeFalsy();
|
|
|
|
// Add a human message at the end to test the opposite
|
|
editedMessages.push({
|
|
role: 'user',
|
|
isCreatedByUser: true,
|
|
text: 'Hi again',
|
|
messageId: '3',
|
|
parentMessageId: '2',
|
|
});
|
|
const result2 = await client.buildMessages(editedMessages, '3');
|
|
expect(result2.prompt.trim().endsWith(trimmedLabel)).toBeTruthy();
|
|
});
|
|
|
|
it('should build messages correctly with a promptPrefix', async () => {
|
|
const promptPrefix = 'Test Prefix';
|
|
client.options.promptPrefix = promptPrefix;
|
|
const result = await client.buildMessages(messages, parentMessageId);
|
|
const { prompt } = result;
|
|
expect(prompt).toBeDefined();
|
|
expect(prompt).toContain(promptPrefix);
|
|
const textAfterPrefix = prompt.split(promptPrefix)[1];
|
|
expect(textAfterPrefix).toContain(AI_PROMPT);
|
|
|
|
const editedMessages = messages.slice(0, -1);
|
|
const result2 = await client.buildMessages(editedMessages, parentMessageId);
|
|
const textAfterPrefix2 = result2.prompt.split(promptPrefix)[1];
|
|
expect(textAfterPrefix2).toContain(AI_PROMPT);
|
|
});
|
|
|
|
it('should handle identityPrefix from options', async () => {
|
|
client.options.userLabel = 'John';
|
|
client.options.modelLabel = 'Claude-2';
|
|
const result = await client.buildMessages(messages, parentMessageId);
|
|
const { prompt } = result;
|
|
expect(prompt).toContain("Human's name: John");
|
|
expect(prompt).toContain('You are Claude-2');
|
|
});
|
|
});
|
|
|
|
describe('getClient', () => {
|
|
it('should set legacy maxOutputTokens for non-Claude-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-2',
|
|
maxOutputTokens: anthropicSettings.legacy.maxOutputTokens.default,
|
|
},
|
|
});
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
});
|
|
|
|
it('should not set legacy maxOutputTokens for Claude-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-opus-20240229',
|
|
maxOutputTokens: anthropicSettings.legacy.maxOutputTokens.default,
|
|
},
|
|
});
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
});
|
|
|
|
it('should add "max-tokens" & "prompt-caching" beta header for claude-3-5-sonnet model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'claude-3-5-sonnet-20241022',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient._options.defaultHeaders).toBeDefined();
|
|
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe(
|
|
'max-tokens-3-5-sonnet-2024-07-15,prompt-caching-2024-07-31',
|
|
);
|
|
});
|
|
|
|
it('should add "prompt-caching" beta header for claude-3-haiku model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'claude-3-haiku-2028',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient._options.defaultHeaders).toBeDefined();
|
|
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31',
|
|
);
|
|
});
|
|
|
|
it('should add "prompt-caching" beta header for claude-3-opus model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'claude-3-opus-2028',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient._options.defaultHeaders).toBeDefined();
|
|
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31',
|
|
);
|
|
});
|
|
|
|
describe('Claude 4 model headers', () => {
|
|
it('should add "prompt-caching" beta header for claude-sonnet-4 model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'claude-sonnet-4-20250514',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient._options.defaultHeaders).toBeDefined();
|
|
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31',
|
|
);
|
|
});
|
|
|
|
it('should add "prompt-caching" beta header for claude-opus-4 model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'claude-opus-4-20250514',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient._options.defaultHeaders).toBeDefined();
|
|
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31',
|
|
);
|
|
});
|
|
|
|
it('should add "prompt-caching" beta header for claude-4-sonnet model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'claude-4-sonnet-20250514',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient._options.defaultHeaders).toBeDefined();
|
|
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31',
|
|
);
|
|
});
|
|
|
|
it('should add "prompt-caching" beta header for claude-4-opus model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'claude-4-opus-20250514',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient._options.defaultHeaders).toBeDefined();
|
|
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should not add beta header for claude-3-5-sonnet-latest model', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const modelOptions = {
|
|
model: 'anthropic/claude-3-5-sonnet-latest',
|
|
};
|
|
client.setOptions({ modelOptions, promptCache: true });
|
|
const anthropicClient = client.getClient(modelOptions);
|
|
expect(anthropicClient.defaultHeaders).not.toHaveProperty('anthropic-beta');
|
|
});
|
|
|
|
it('should not add beta header for other models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-2',
|
|
},
|
|
});
|
|
const anthropicClient = client.getClient();
|
|
expect(anthropicClient.defaultHeaders).not.toHaveProperty('anthropic-beta');
|
|
});
|
|
});
|
|
|
|
describe('calculateCurrentTokenCount', () => {
|
|
let client;
|
|
|
|
beforeEach(() => {
|
|
client = new AnthropicClient('test-api-key');
|
|
});
|
|
|
|
it('should calculate correct token count when usage is provided', () => {
|
|
const tokenCountMap = {
|
|
msg1: 10,
|
|
msg2: 20,
|
|
currentMsg: 30,
|
|
};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = {
|
|
input_tokens: 70,
|
|
output_tokens: 50,
|
|
};
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(40); // 70 - (10 + 20) = 40
|
|
});
|
|
|
|
it('should return original estimate if calculation results in negative value', () => {
|
|
const tokenCountMap = {
|
|
msg1: 40,
|
|
msg2: 50,
|
|
currentMsg: 30,
|
|
};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = {
|
|
input_tokens: 80,
|
|
output_tokens: 50,
|
|
};
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(30); // Original estimate
|
|
});
|
|
|
|
it('should handle cache creation and read input tokens', () => {
|
|
const tokenCountMap = {
|
|
msg1: 10,
|
|
msg2: 20,
|
|
currentMsg: 30,
|
|
};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = {
|
|
input_tokens: 50,
|
|
cache_creation_input_tokens: 10,
|
|
cache_read_input_tokens: 20,
|
|
output_tokens: 40,
|
|
};
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(50); // (50 + 10 + 20) - (10 + 20) = 50
|
|
});
|
|
|
|
it('should handle missing usage properties', () => {
|
|
const tokenCountMap = {
|
|
msg1: 10,
|
|
msg2: 20,
|
|
currentMsg: 30,
|
|
};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = {
|
|
output_tokens: 40,
|
|
};
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(30); // Original estimate
|
|
});
|
|
|
|
it('should handle empty tokenCountMap', () => {
|
|
const tokenCountMap = {};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = {
|
|
input_tokens: 50,
|
|
output_tokens: 40,
|
|
};
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(50);
|
|
expect(Number.isNaN(result)).toBe(false);
|
|
});
|
|
|
|
it('should handle zero values in usage', () => {
|
|
const tokenCountMap = {
|
|
msg1: 10,
|
|
currentMsg: 20,
|
|
};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = {
|
|
input_tokens: 0,
|
|
cache_creation_input_tokens: 0,
|
|
cache_read_input_tokens: 0,
|
|
output_tokens: 0,
|
|
};
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(20); // Should return original estimate
|
|
expect(Number.isNaN(result)).toBe(false);
|
|
});
|
|
|
|
it('should handle undefined usage', () => {
|
|
const tokenCountMap = {
|
|
msg1: 10,
|
|
currentMsg: 20,
|
|
};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = undefined;
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(20); // Should return original estimate
|
|
expect(Number.isNaN(result)).toBe(false);
|
|
});
|
|
|
|
it('should handle non-numeric values in tokenCountMap', () => {
|
|
const tokenCountMap = {
|
|
msg1: 'ten',
|
|
currentMsg: 20,
|
|
};
|
|
const currentMessageId = 'currentMsg';
|
|
const usage = {
|
|
input_tokens: 30,
|
|
output_tokens: 10,
|
|
};
|
|
|
|
const result = client.calculateCurrentTokenCount({ tokenCountMap, currentMessageId, usage });
|
|
|
|
expect(result).toBe(30); // Should return 30 (input_tokens) - 0 (ignored 'ten') = 30
|
|
expect(Number.isNaN(result)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('maxOutputTokens handling for different models', () => {
|
|
it('should not cap maxOutputTokens for Claude 3.5 Sonnet models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 10;
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-5-sonnet',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
|
|
|
// Test with decimal notation
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3.5-sonnet',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
|
});
|
|
|
|
it('should not cap maxOutputTokens for Claude 3.7 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 2;
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
|
|
|
// Test with decimal notation
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3.7-sonnet',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
|
});
|
|
|
|
it('should not cap maxOutputTokens for Claude 4 Sonnet models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 10; // 40,960 tokens
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-sonnet-4-20250514',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
|
});
|
|
|
|
it('should not cap maxOutputTokens for Claude 4 Opus models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 6; // 24,576 tokens (under 32K limit)
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-opus-4-20250514',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
|
});
|
|
|
|
it('should cap maxOutputTokens for Claude 3.5 Haiku models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 2;
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-5-haiku',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
|
|
// Test with decimal notation
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3.5-haiku',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
});
|
|
|
|
it('should cap maxOutputTokens for Claude 3 Haiku and Opus models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 2;
|
|
|
|
// Test haiku
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-haiku',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
|
|
// Test opus
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-opus',
|
|
maxOutputTokens: highTokenValue,
|
|
},
|
|
});
|
|
|
|
expect(client.modelOptions.maxOutputTokens).toBe(
|
|
anthropicSettings.legacy.maxOutputTokens.default,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('topK/topP parameters for different models', () => {
|
|
beforeEach(() => {
|
|
// Mock the SplitStreamHandler
|
|
jest.spyOn(SplitStreamHandler.prototype, 'handle').mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('should include top_k and top_p parameters for non-claude-3.7 models', async () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
|
|
// Create a mock async generator function
|
|
async function* mockAsyncGenerator() {
|
|
yield { type: 'message_start', message: { usage: {} } };
|
|
yield { delta: { text: 'Test response' } };
|
|
yield { type: 'message_delta', usage: {} };
|
|
}
|
|
|
|
// Mock createResponse to return the async generator
|
|
jest.spyOn(client, 'createResponse').mockImplementation(() => {
|
|
return mockAsyncGenerator();
|
|
});
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-opus',
|
|
temperature: 0.7,
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
});
|
|
|
|
// Mock getClient to capture the request options
|
|
let capturedOptions = null;
|
|
jest.spyOn(client, 'getClient').mockImplementation((options) => {
|
|
capturedOptions = options;
|
|
return {};
|
|
});
|
|
|
|
const payload = [{ role: 'user', content: 'Test message' }];
|
|
await client.sendCompletion(payload, {});
|
|
|
|
// Check the options passed to getClient
|
|
expect(capturedOptions).toHaveProperty('top_k', 10);
|
|
expect(capturedOptions).toHaveProperty('top_p', 0.9);
|
|
});
|
|
|
|
it('should include top_k and top_p parameters for claude-3-5-sonnet models', async () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
|
|
// Create a mock async generator function
|
|
async function* mockAsyncGenerator() {
|
|
yield { type: 'message_start', message: { usage: {} } };
|
|
yield { delta: { text: 'Test response' } };
|
|
yield { type: 'message_delta', usage: {} };
|
|
}
|
|
|
|
// Mock createResponse to return the async generator
|
|
jest.spyOn(client, 'createResponse').mockImplementation(() => {
|
|
return mockAsyncGenerator();
|
|
});
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-5-sonnet',
|
|
temperature: 0.7,
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
});
|
|
|
|
// Mock getClient to capture the request options
|
|
let capturedOptions = null;
|
|
jest.spyOn(client, 'getClient').mockImplementation((options) => {
|
|
capturedOptions = options;
|
|
return {};
|
|
});
|
|
|
|
const payload = [{ role: 'user', content: 'Test message' }];
|
|
await client.sendCompletion(payload, {});
|
|
|
|
// Check the options passed to getClient
|
|
expect(capturedOptions).toHaveProperty('top_k', 10);
|
|
expect(capturedOptions).toHaveProperty('top_p', 0.9);
|
|
});
|
|
|
|
it('should not include top_k and top_p parameters for claude-3-7-sonnet models', async () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
|
|
// Create a mock async generator function
|
|
async function* mockAsyncGenerator() {
|
|
yield { type: 'message_start', message: { usage: {} } };
|
|
yield { delta: { text: 'Test response' } };
|
|
yield { type: 'message_delta', usage: {} };
|
|
}
|
|
|
|
// Mock createResponse to return the async generator
|
|
jest.spyOn(client, 'createResponse').mockImplementation(() => {
|
|
return mockAsyncGenerator();
|
|
});
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
temperature: 0.7,
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
});
|
|
|
|
// Mock getClient to capture the request options
|
|
let capturedOptions = null;
|
|
jest.spyOn(client, 'getClient').mockImplementation((options) => {
|
|
capturedOptions = options;
|
|
return {};
|
|
});
|
|
|
|
const payload = [{ role: 'user', content: 'Test message' }];
|
|
await client.sendCompletion(payload, {});
|
|
|
|
// Check the options passed to getClient
|
|
expect(capturedOptions).not.toHaveProperty('top_k');
|
|
expect(capturedOptions).not.toHaveProperty('top_p');
|
|
});
|
|
|
|
it('should not include top_k and top_p parameters for models with decimal notation (claude-3.7)', async () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
|
|
// Create a mock async generator function
|
|
async function* mockAsyncGenerator() {
|
|
yield { type: 'message_start', message: { usage: {} } };
|
|
yield { delta: { text: 'Test response' } };
|
|
yield { type: 'message_delta', usage: {} };
|
|
}
|
|
|
|
// Mock createResponse to return the async generator
|
|
jest.spyOn(client, 'createResponse').mockImplementation(() => {
|
|
return mockAsyncGenerator();
|
|
});
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3.7-sonnet',
|
|
temperature: 0.7,
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
});
|
|
|
|
// Mock getClient to capture the request options
|
|
let capturedOptions = null;
|
|
jest.spyOn(client, 'getClient').mockImplementation((options) => {
|
|
capturedOptions = options;
|
|
return {};
|
|
});
|
|
|
|
const payload = [{ role: 'user', content: 'Test message' }];
|
|
await client.sendCompletion(payload, {});
|
|
|
|
// Check the options passed to getClient
|
|
expect(capturedOptions).not.toHaveProperty('top_k');
|
|
expect(capturedOptions).not.toHaveProperty('top_p');
|
|
});
|
|
});
|
|
|
|
it('should include top_k and top_p parameters for Claude-3.7 models when thinking is explicitly disabled', async () => {
|
|
const client = new AnthropicClient('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
temperature: 0.7,
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
thinking: false,
|
|
});
|
|
|
|
async function* mockAsyncGenerator() {
|
|
yield { type: 'message_start', message: { usage: {} } };
|
|
yield { delta: { text: 'Test response' } };
|
|
yield { type: 'message_delta', usage: {} };
|
|
}
|
|
|
|
jest.spyOn(client, 'createResponse').mockImplementation(() => {
|
|
return mockAsyncGenerator();
|
|
});
|
|
|
|
let capturedOptions = null;
|
|
jest.spyOn(client, 'getClient').mockImplementation((options) => {
|
|
capturedOptions = options;
|
|
return {};
|
|
});
|
|
|
|
const payload = [{ role: 'user', content: 'Test message' }];
|
|
await client.sendCompletion(payload, {});
|
|
|
|
expect(capturedOptions).toHaveProperty('topK', 10);
|
|
expect(capturedOptions).toHaveProperty('topP', 0.9);
|
|
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3.7-sonnet',
|
|
temperature: 0.7,
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
thinking: false,
|
|
});
|
|
|
|
await client.sendCompletion(payload, {});
|
|
|
|
expect(capturedOptions).toHaveProperty('topK', 10);
|
|
expect(capturedOptions).toHaveProperty('topP', 0.9);
|
|
});
|
|
|
|
describe('isClaudeLatest', () => {
|
|
it('should set isClaudeLatest to true for claude-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3-sonnet-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(true);
|
|
});
|
|
|
|
it('should set isClaudeLatest to true for claude-3.5 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3.5-sonnet-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(true);
|
|
});
|
|
|
|
it('should set isClaudeLatest to true for claude-sonnet-4 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-sonnet-4-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(true);
|
|
});
|
|
|
|
it('should set isClaudeLatest to true for claude-opus-4 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-opus-4-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(true);
|
|
});
|
|
|
|
it('should set isClaudeLatest to true for claude-3.5-haiku models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-3.5-haiku-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(true);
|
|
});
|
|
|
|
it('should set isClaudeLatest to false for claude-2 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-2',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(false);
|
|
});
|
|
|
|
it('should set isClaudeLatest to false for claude-instant models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-instant',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(false);
|
|
});
|
|
|
|
it('should set isClaudeLatest to false for claude-sonnet-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-sonnet-3-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(false);
|
|
});
|
|
|
|
it('should set isClaudeLatest to false for claude-opus-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-opus-3-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(false);
|
|
});
|
|
|
|
it('should set isClaudeLatest to false for claude-haiku-3 models', () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-haiku-3-20240229',
|
|
},
|
|
});
|
|
expect(client.isClaudeLatest).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('configureReasoning', () => {
|
|
it('should enable thinking for claude-opus-4 and claude-sonnet-4 models', async () => {
|
|
const client = new AnthropicClient('test-api-key');
|
|
// Create a mock async generator function
|
|
async function* mockAsyncGenerator() {
|
|
yield { type: 'message_start', message: { usage: {} } };
|
|
yield { delta: { text: 'Test response' } };
|
|
yield { type: 'message_delta', usage: {} };
|
|
}
|
|
|
|
// Mock createResponse to return the async generator
|
|
jest.spyOn(client, 'createResponse').mockImplementation(() => {
|
|
return mockAsyncGenerator();
|
|
});
|
|
|
|
// Test claude-opus-4
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-opus-4-20250514',
|
|
},
|
|
thinking: true,
|
|
thinkingBudget: 2000,
|
|
});
|
|
|
|
let capturedOptions = null;
|
|
jest.spyOn(client, 'getClient').mockImplementation((options) => {
|
|
capturedOptions = options;
|
|
return {};
|
|
});
|
|
|
|
const payload = [{ role: 'user', content: 'Test message' }];
|
|
await client.sendCompletion(payload, {});
|
|
|
|
expect(capturedOptions).toHaveProperty('thinking');
|
|
expect(capturedOptions.thinking).toEqual({
|
|
type: 'enabled',
|
|
budget_tokens: 2000,
|
|
});
|
|
|
|
// Test claude-sonnet-4
|
|
client.setOptions({
|
|
modelOptions: {
|
|
model: 'claude-sonnet-4-20250514',
|
|
},
|
|
thinking: true,
|
|
thinkingBudget: 2000,
|
|
});
|
|
|
|
await client.sendCompletion(payload, {});
|
|
|
|
expect(capturedOptions).toHaveProperty('thinking');
|
|
expect(capturedOptions.thinking).toEqual({
|
|
type: 'enabled',
|
|
budget_tokens: 2000,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Claude Model Tests', () => {
|
|
it('should handle Claude 3 and 4 series models correctly', () => {
|
|
const client = new AnthropicClient('test-key');
|
|
// Claude 3 series models
|
|
const claude3Models = [
|
|
'claude-3-opus-20240229',
|
|
'claude-3-sonnet-20240229',
|
|
'claude-3-haiku-20240307',
|
|
'claude-3-5-sonnet-20240620',
|
|
'claude-3-5-haiku-20240620',
|
|
'claude-3.5-sonnet-20240620',
|
|
'claude-3.5-haiku-20240620',
|
|
'claude-3.7-sonnet-20240620',
|
|
'claude-3.7-haiku-20240620',
|
|
'anthropic/claude-3-opus-20240229',
|
|
'claude-3-opus-20240229/anthropic',
|
|
];
|
|
|
|
// Claude 4 series models
|
|
const claude4Models = [
|
|
'claude-sonnet-4-20250514',
|
|
'claude-opus-4-20250514',
|
|
'claude-4-sonnet-20250514',
|
|
'claude-4-opus-20250514',
|
|
'anthropic/claude-sonnet-4-20250514',
|
|
'claude-sonnet-4-20250514/anthropic',
|
|
];
|
|
|
|
// Test Claude 3 series
|
|
claude3Models.forEach((model) => {
|
|
client.setOptions({ modelOptions: { model } });
|
|
expect(
|
|
/claude-[3-9]/.test(client.modelOptions.model) ||
|
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(client.modelOptions.model),
|
|
).toBe(true);
|
|
});
|
|
|
|
// Test Claude 4 series
|
|
claude4Models.forEach((model) => {
|
|
client.setOptions({ modelOptions: { model } });
|
|
expect(
|
|
/claude-[3-9]/.test(client.modelOptions.model) ||
|
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(client.modelOptions.model),
|
|
).toBe(true);
|
|
});
|
|
|
|
// Test non-Claude 3/4 models
|
|
const nonClaudeModels = ['claude-2', 'claude-instant', 'gpt-4', 'gpt-3.5-turbo'];
|
|
|
|
nonClaudeModels.forEach((model) => {
|
|
client.setOptions({ modelOptions: { model } });
|
|
expect(
|
|
/claude-[3-9]/.test(client.modelOptions.model) ||
|
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(client.modelOptions.model),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
});
|