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

* adding beta header context-1m-2025-08-07 to claude sonnet 4 to increase contact window to 1M tokens * adding context-1m beta header to test cases * ci: Update Anthropic `getLLMConfig` tests and headers for model variations - Refactored test cases to ensure proper handling of model variations for 'claude-sonnet-4'. - Cleaned up unused mock implementations in tests for better clarity and performance. * refactor: regex in header retrieval for 'claude-sonnet-4' models * refactor: default tokens for 'claude-sonnet-4' to `1,000,000` * refactor: add token value retrieval and pattern matching to model tests --------- Co-authored-by: Dirk Petersen <no-reply@nowhere.com>
341 lines
12 KiB
JavaScript
341 lines
12 KiB
JavaScript
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
|
|
|
|
jest.mock('https-proxy-agent', () => ({
|
|
HttpsProxyAgent: jest.fn().mockImplementation((proxy) => ({ proxy })),
|
|
}));
|
|
|
|
describe('getLLMConfig', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('should create a basic configuration with default values', () => {
|
|
const result = getLLMConfig('test-api-key', { modelOptions: {} });
|
|
|
|
expect(result.llmConfig).toHaveProperty('apiKey', 'test-api-key');
|
|
expect(result.llmConfig).toHaveProperty('model', 'claude-3-5-sonnet-latest');
|
|
expect(result.llmConfig).toHaveProperty('stream', true);
|
|
expect(result.llmConfig).toHaveProperty('maxTokens');
|
|
});
|
|
|
|
it('should include proxy settings when provided', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {},
|
|
proxy: 'http://proxy:8080',
|
|
});
|
|
|
|
expect(result.llmConfig.clientOptions).toHaveProperty('fetchOptions');
|
|
expect(result.llmConfig.clientOptions.fetchOptions).toHaveProperty('dispatcher');
|
|
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher).toBeDefined();
|
|
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher.constructor.name).toBe(
|
|
'ProxyAgent',
|
|
);
|
|
});
|
|
|
|
it('should include reverse proxy URL when provided', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {},
|
|
reverseProxyUrl: 'http://reverse-proxy',
|
|
});
|
|
|
|
expect(result.llmConfig.clientOptions).toHaveProperty('baseURL', 'http://reverse-proxy');
|
|
expect(result.llmConfig).toHaveProperty('anthropicApiUrl', 'http://reverse-proxy');
|
|
});
|
|
|
|
it('should include topK and topP for non-Claude-3.7 models', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-opus',
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).toHaveProperty('topK', 10);
|
|
expect(result.llmConfig).toHaveProperty('topP', 0.9);
|
|
});
|
|
|
|
it('should include topK and topP for Claude-3.5 models', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-5-sonnet',
|
|
topK: 10,
|
|
topP: 0.9,
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).toHaveProperty('topK', 10);
|
|
expect(result.llmConfig).toHaveProperty('topP', 0.9);
|
|
});
|
|
|
|
it('should NOT include topK and topP for Claude-3-7 models with thinking enabled (hyphen notation)', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
topK: 10,
|
|
topP: 0.9,
|
|
thinking: true,
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).not.toHaveProperty('topK');
|
|
expect(result.llmConfig).not.toHaveProperty('topP');
|
|
expect(result.llmConfig).toHaveProperty('thinking');
|
|
expect(result.llmConfig.thinking).toHaveProperty('type', 'enabled');
|
|
// When thinking is enabled, it uses the default thinkingBudget of 2000
|
|
expect(result.llmConfig.thinking).toHaveProperty('budget_tokens', 2000);
|
|
});
|
|
|
|
it('should add "prompt-caching" and "context-1m" beta headers for claude-sonnet-4 model', () => {
|
|
const modelOptions = {
|
|
model: 'claude-sonnet-4-20250514',
|
|
promptCache: true,
|
|
};
|
|
const result = getLLMConfig('test-key', { modelOptions });
|
|
const clientOptions = result.llmConfig.clientOptions;
|
|
expect(clientOptions.defaultHeaders).toBeDefined();
|
|
expect(clientOptions.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(clientOptions.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31,context-1m-2025-08-07',
|
|
);
|
|
});
|
|
|
|
it('should add "prompt-caching" and "context-1m" beta headers for claude-sonnet-4 model formats', () => {
|
|
const modelVariations = [
|
|
'claude-sonnet-4-20250514',
|
|
'claude-sonnet-4-latest',
|
|
'anthropic/claude-sonnet-4-20250514',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const modelOptions = { model, promptCache: true };
|
|
const result = getLLMConfig('test-key', { modelOptions });
|
|
const clientOptions = result.llmConfig.clientOptions;
|
|
expect(clientOptions.defaultHeaders).toBeDefined();
|
|
expect(clientOptions.defaultHeaders).toHaveProperty('anthropic-beta');
|
|
expect(clientOptions.defaultHeaders['anthropic-beta']).toBe(
|
|
'prompt-caching-2024-07-31,context-1m-2025-08-07',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should NOT include topK and topP for Claude-3.7 models with thinking enabled (decimal notation)', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3.7-sonnet',
|
|
topK: 10,
|
|
topP: 0.9,
|
|
thinking: true,
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).not.toHaveProperty('topK');
|
|
expect(result.llmConfig).not.toHaveProperty('topP');
|
|
expect(result.llmConfig).toHaveProperty('thinking');
|
|
expect(result.llmConfig.thinking).toHaveProperty('type', 'enabled');
|
|
// When thinking is enabled, it uses the default thinkingBudget of 2000
|
|
expect(result.llmConfig.thinking).toHaveProperty('budget_tokens', 2000);
|
|
});
|
|
|
|
it('should handle custom maxOutputTokens', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-opus',
|
|
maxOutputTokens: 2048,
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).toHaveProperty('maxTokens', 2048);
|
|
});
|
|
|
|
it('should handle promptCache setting', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-5-sonnet',
|
|
promptCache: true,
|
|
},
|
|
});
|
|
|
|
// We're not checking specific header values since that depends on the actual helper function
|
|
// Just verifying that the promptCache setting is processed
|
|
expect(result.llmConfig).toBeDefined();
|
|
});
|
|
|
|
it('should include topK and topP for Claude-3.7 models when thinking is not enabled', () => {
|
|
// Test with thinking explicitly set to null/undefined
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
topK: 10,
|
|
topP: 0.9,
|
|
thinking: false,
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).toHaveProperty('topK', 10);
|
|
expect(result.llmConfig).toHaveProperty('topP', 0.9);
|
|
|
|
// Test with thinking explicitly set to false
|
|
const result2 = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
topK: 10,
|
|
topP: 0.9,
|
|
thinking: false,
|
|
},
|
|
});
|
|
|
|
expect(result2.llmConfig).toHaveProperty('topK', 10);
|
|
expect(result2.llmConfig).toHaveProperty('topP', 0.9);
|
|
|
|
// Test with decimal notation as well
|
|
const result3 = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3.7-sonnet',
|
|
topK: 10,
|
|
topP: 0.9,
|
|
thinking: false,
|
|
},
|
|
});
|
|
|
|
expect(result3.llmConfig).toHaveProperty('topK', 10);
|
|
expect(result3.llmConfig).toHaveProperty('topP', 0.9);
|
|
});
|
|
|
|
describe('Edge cases', () => {
|
|
it('should handle missing apiKey', () => {
|
|
const result = getLLMConfig(undefined, { modelOptions: {} });
|
|
expect(result.llmConfig).not.toHaveProperty('apiKey');
|
|
});
|
|
|
|
it('should handle empty modelOptions', () => {
|
|
expect(() => {
|
|
getLLMConfig('test-api-key', {});
|
|
}).toThrow("Cannot read properties of undefined (reading 'thinking')");
|
|
});
|
|
|
|
it('should handle no options parameter', () => {
|
|
expect(() => {
|
|
getLLMConfig('test-api-key');
|
|
}).toThrow("Cannot read properties of undefined (reading 'thinking')");
|
|
});
|
|
|
|
it('should handle temperature, stop sequences, and stream settings', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
temperature: 0.7,
|
|
stop: ['\n\n', 'END'],
|
|
stream: false,
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).toHaveProperty('temperature', 0.7);
|
|
expect(result.llmConfig).toHaveProperty('stopSequences', ['\n\n', 'END']);
|
|
expect(result.llmConfig).toHaveProperty('stream', false);
|
|
});
|
|
|
|
it('should handle maxOutputTokens when explicitly set to falsy value', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-opus',
|
|
maxOutputTokens: null,
|
|
},
|
|
});
|
|
|
|
// The actual anthropicSettings.maxOutputTokens.reset('claude-3-opus') returns 4096
|
|
expect(result.llmConfig).toHaveProperty('maxTokens', 4096);
|
|
});
|
|
|
|
it('should handle both proxy and reverseProxyUrl', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {},
|
|
proxy: 'http://proxy:8080',
|
|
reverseProxyUrl: 'https://reverse-proxy.com',
|
|
});
|
|
|
|
expect(result.llmConfig.clientOptions).toHaveProperty('fetchOptions');
|
|
expect(result.llmConfig.clientOptions.fetchOptions).toHaveProperty('dispatcher');
|
|
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher).toBeDefined();
|
|
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher.constructor.name).toBe(
|
|
'ProxyAgent',
|
|
);
|
|
expect(result.llmConfig.clientOptions).toHaveProperty('baseURL', 'https://reverse-proxy.com');
|
|
expect(result.llmConfig).toHaveProperty('anthropicApiUrl', 'https://reverse-proxy.com');
|
|
});
|
|
|
|
it('should handle prompt cache with supported model', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-5-sonnet',
|
|
promptCache: true,
|
|
},
|
|
});
|
|
|
|
// claude-3-5-sonnet supports prompt caching and should get the appropriate headers
|
|
expect(result.llmConfig.clientOptions.defaultHeaders).toEqual({
|
|
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15,prompt-caching-2024-07-31',
|
|
});
|
|
});
|
|
|
|
it('should handle thinking and thinkingBudget options', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
thinking: true,
|
|
thinkingBudget: 10000, // This exceeds the default max_tokens of 8192
|
|
},
|
|
});
|
|
|
|
// The function should add thinking configuration for claude-3-7 models
|
|
expect(result.llmConfig).toHaveProperty('thinking');
|
|
expect(result.llmConfig.thinking).toHaveProperty('type', 'enabled');
|
|
// With claude-3-7-sonnet, the max_tokens default is 8192
|
|
// Budget tokens gets adjusted to 90% of max_tokens (8192 * 0.9 = 7372) when it exceeds max_tokens
|
|
expect(result.llmConfig.thinking).toHaveProperty('budget_tokens', 7372);
|
|
|
|
// Test with budget_tokens within max_tokens limit
|
|
const result2 = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
model: 'claude-3-7-sonnet',
|
|
thinking: true,
|
|
thinkingBudget: 2000,
|
|
},
|
|
});
|
|
|
|
expect(result2.llmConfig.thinking).toHaveProperty('budget_tokens', 2000);
|
|
});
|
|
|
|
it('should remove system options from modelOptions', () => {
|
|
const modelOptions = {
|
|
model: 'claude-3-opus',
|
|
thinking: true,
|
|
promptCache: true,
|
|
thinkingBudget: 1000,
|
|
temperature: 0.5,
|
|
};
|
|
|
|
getLLMConfig('test-api-key', { modelOptions });
|
|
|
|
expect(modelOptions).not.toHaveProperty('thinking');
|
|
expect(modelOptions).not.toHaveProperty('promptCache');
|
|
expect(modelOptions).not.toHaveProperty('thinkingBudget');
|
|
expect(modelOptions).toHaveProperty('temperature', 0.5);
|
|
});
|
|
|
|
it('should handle all nullish values removal', () => {
|
|
const result = getLLMConfig('test-api-key', {
|
|
modelOptions: {
|
|
temperature: null,
|
|
topP: undefined,
|
|
topK: 0,
|
|
stop: [],
|
|
},
|
|
});
|
|
|
|
expect(result.llmConfig).not.toHaveProperty('temperature');
|
|
expect(result.llmConfig).not.toHaveProperty('topP');
|
|
expect(result.llmConfig).toHaveProperty('topK', 0);
|
|
expect(result.llmConfig).toHaveProperty('stopSequences', []);
|
|
});
|
|
});
|
|
});
|