mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🦾 feat: Claude-4 Support (#7509)
* refactor: Update AnthropicClient to support Claude model naming changes * Renamed `isClaude3` to `isClaudeLatest` to accommodate newer Claude models. * Updated logic to determine if the model is part of the Claude family. * Adjusted `useMessages` property to reflect the new model naming convention. * Cleaned up client properties during disposal to match the updated naming. * feat: Claude-4 Support * feat: Add Thinking and Prompt caching support for Claude 4 * chore: Update ANTHROPIC_MODELS in .env.example for latest model versions
This commit is contained in:
parent
28b76ce339
commit
a2f330e6ca
10 changed files with 483 additions and 34 deletions
|
|
@ -88,7 +88,7 @@ PROXY=
|
||||||
#============#
|
#============#
|
||||||
|
|
||||||
ANTHROPIC_API_KEY=user_provided
|
ANTHROPIC_API_KEY=user_provided
|
||||||
# ANTHROPIC_MODELS=claude-3-7-sonnet-latest,claude-3-7-sonnet-20250219,claude-3-5-haiku-20241022,claude-3-5-sonnet-20241022,claude-3-5-sonnet-latest,claude-3-5-sonnet-20240620,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307,claude-2.1,claude-2,claude-1.2,claude-1,claude-1-100k,claude-instant-1,claude-instant-1-100k
|
# ANTHROPIC_MODELS=claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
|
||||||
# ANTHROPIC_REVERSE_PROXY=
|
# ANTHROPIC_REVERSE_PROXY=
|
||||||
|
|
||||||
#============#
|
#============#
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class AnthropicClient extends BaseClient {
|
||||||
this.message_delta;
|
this.message_delta;
|
||||||
/** Whether the model is part of the Claude 3 Family
|
/** Whether the model is part of the Claude 3 Family
|
||||||
* @type {boolean} */
|
* @type {boolean} */
|
||||||
this.isClaude3;
|
this.isClaudeLatest;
|
||||||
/** Whether to use Messages API or Completions API
|
/** Whether to use Messages API or Completions API
|
||||||
* @type {boolean} */
|
* @type {boolean} */
|
||||||
this.useMessages;
|
this.useMessages;
|
||||||
|
|
@ -116,7 +116,8 @@ class AnthropicClient extends BaseClient {
|
||||||
);
|
);
|
||||||
|
|
||||||
const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic);
|
const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic);
|
||||||
this.isClaude3 = modelMatch.includes('claude-3');
|
this.isClaudeLatest =
|
||||||
|
/claude-[3-9]/.test(modelMatch) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch);
|
||||||
this.isLegacyOutput = !(
|
this.isLegacyOutput = !(
|
||||||
/claude-3[-.]5-sonnet/.test(modelMatch) || /claude-3[-.]7/.test(modelMatch)
|
/claude-3[-.]5-sonnet/.test(modelMatch) || /claude-3[-.]7/.test(modelMatch)
|
||||||
);
|
);
|
||||||
|
|
@ -130,7 +131,7 @@ class AnthropicClient extends BaseClient {
|
||||||
this.modelOptions.maxOutputTokens = legacy.maxOutputTokens.default;
|
this.modelOptions.maxOutputTokens = legacy.maxOutputTokens.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.useMessages = this.isClaude3 || !!this.options.attachments;
|
this.useMessages = this.isClaudeLatest || !!this.options.attachments;
|
||||||
|
|
||||||
this.defaultVisionModel = this.options.visionModel ?? 'claude-3-sonnet-20240229';
|
this.defaultVisionModel = this.options.visionModel ?? 'claude-3-sonnet-20240229';
|
||||||
this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments));
|
this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments));
|
||||||
|
|
@ -654,7 +655,10 @@ class AnthropicClient extends BaseClient {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.modelOptions.model.includes('claude-3')) {
|
if (
|
||||||
|
/claude-[3-9]/.test(this.modelOptions.model) ||
|
||||||
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(this.modelOptions.model)
|
||||||
|
) {
|
||||||
await buildMessagesPayload();
|
await buildMessagesPayload();
|
||||||
processTokens();
|
processTokens();
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ describe('AnthropicClient', () => {
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
isCreatedByUser: true,
|
isCreatedByUser: true,
|
||||||
text: 'What\'s up',
|
text: "What's up",
|
||||||
messageId: '3',
|
messageId: '3',
|
||||||
parentMessageId: '2',
|
parentMessageId: '2',
|
||||||
},
|
},
|
||||||
|
|
@ -170,7 +170,7 @@ describe('AnthropicClient', () => {
|
||||||
client.options.modelLabel = 'Claude-2';
|
client.options.modelLabel = 'Claude-2';
|
||||||
const result = await client.buildMessages(messages, parentMessageId);
|
const result = await client.buildMessages(messages, parentMessageId);
|
||||||
const { prompt } = result;
|
const { prompt } = result;
|
||||||
expect(prompt).toContain('Human\'s name: John');
|
expect(prompt).toContain("Human's name: John");
|
||||||
expect(prompt).toContain('You are Claude-2');
|
expect(prompt).toContain('You are Claude-2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -244,6 +244,64 @@ describe('AnthropicClient', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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', () => {
|
it('should not add beta header for claude-3-5-sonnet-latest model', () => {
|
||||||
const client = new AnthropicClient('test-api-key');
|
const client = new AnthropicClient('test-api-key');
|
||||||
const modelOptions = {
|
const modelOptions = {
|
||||||
|
|
@ -729,4 +787,223 @@ describe('AnthropicClient', () => {
|
||||||
expect(capturedOptions).toHaveProperty('topK', 10);
|
expect(capturedOptions).toHaveProperty('topK', 10);
|
||||||
expect(capturedOptions).toHaveProperty('topP', 0.9);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,8 @@ const tokenValues = Object.assign(
|
||||||
'claude-3-5-haiku': { prompt: 0.8, completion: 4 },
|
'claude-3-5-haiku': { prompt: 0.8, completion: 4 },
|
||||||
'claude-3.5-haiku': { prompt: 0.8, completion: 4 },
|
'claude-3.5-haiku': { prompt: 0.8, completion: 4 },
|
||||||
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
|
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
|
||||||
|
'claude-sonnet-4': { prompt: 3, completion: 15 },
|
||||||
|
'claude-opus-4': { prompt: 15, completion: 75 },
|
||||||
'claude-2.1': { prompt: 8, completion: 24 },
|
'claude-2.1': { prompt: 8, completion: 24 },
|
||||||
'claude-2': { prompt: 8, completion: 24 },
|
'claude-2': { prompt: 8, completion: 24 },
|
||||||
'claude-instant': { prompt: 0.8, completion: 2.4 },
|
'claude-instant': { prompt: 0.8, completion: 2.4 },
|
||||||
|
|
@ -162,6 +164,8 @@ const cacheTokenValues = {
|
||||||
'claude-3.5-haiku': { write: 1, read: 0.08 },
|
'claude-3.5-haiku': { write: 1, read: 0.08 },
|
||||||
'claude-3-5-haiku': { write: 1, read: 0.08 },
|
'claude-3-5-haiku': { write: 1, read: 0.08 },
|
||||||
'claude-3-haiku': { write: 0.3, read: 0.03 },
|
'claude-3-haiku': { write: 0.3, read: 0.03 },
|
||||||
|
'claude-sonnet-4': { write: 3.75, read: 0.3 },
|
||||||
|
'claude-opus-4': { write: 18.75, read: 1.5 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -664,3 +664,97 @@ describe('Grok Model Tests - Pricing', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Claude Model Tests', () => {
|
||||||
|
it('should return correct prompt and completion rates for Claude 4 models', () => {
|
||||||
|
expect(getMultiplier({ model: 'claude-sonnet-4', tokenType: 'prompt' })).toBe(
|
||||||
|
tokenValues['claude-sonnet-4'].prompt,
|
||||||
|
);
|
||||||
|
expect(getMultiplier({ model: 'claude-sonnet-4', tokenType: 'completion' })).toBe(
|
||||||
|
tokenValues['claude-sonnet-4'].completion,
|
||||||
|
);
|
||||||
|
expect(getMultiplier({ model: 'claude-opus-4', tokenType: 'prompt' })).toBe(
|
||||||
|
tokenValues['claude-opus-4'].prompt,
|
||||||
|
);
|
||||||
|
expect(getMultiplier({ model: 'claude-opus-4', tokenType: 'completion' })).toBe(
|
||||||
|
tokenValues['claude-opus-4'].completion,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Claude 4 model name variations with different prefixes and suffixes', () => {
|
||||||
|
const modelVariations = [
|
||||||
|
'claude-sonnet-4',
|
||||||
|
'claude-sonnet-4-20240229',
|
||||||
|
'claude-sonnet-4-latest',
|
||||||
|
'anthropic/claude-sonnet-4',
|
||||||
|
'claude-sonnet-4/anthropic',
|
||||||
|
'claude-sonnet-4-preview',
|
||||||
|
'claude-sonnet-4-20240229-preview',
|
||||||
|
'claude-opus-4',
|
||||||
|
'claude-opus-4-20240229',
|
||||||
|
'claude-opus-4-latest',
|
||||||
|
'anthropic/claude-opus-4',
|
||||||
|
'claude-opus-4/anthropic',
|
||||||
|
'claude-opus-4-preview',
|
||||||
|
'claude-opus-4-20240229-preview',
|
||||||
|
];
|
||||||
|
|
||||||
|
modelVariations.forEach((model) => {
|
||||||
|
const valueKey = getValueKey(model);
|
||||||
|
const isSonnet = model.includes('sonnet');
|
||||||
|
const expectedKey = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4';
|
||||||
|
|
||||||
|
expect(valueKey).toBe(expectedKey);
|
||||||
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues[expectedKey].prompt);
|
||||||
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
||||||
|
tokenValues[expectedKey].completion,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct cache rates for Claude 4 models', () => {
|
||||||
|
expect(getCacheMultiplier({ model: 'claude-sonnet-4', cacheType: 'write' })).toBe(
|
||||||
|
cacheTokenValues['claude-sonnet-4'].write,
|
||||||
|
);
|
||||||
|
expect(getCacheMultiplier({ model: 'claude-sonnet-4', cacheType: 'read' })).toBe(
|
||||||
|
cacheTokenValues['claude-sonnet-4'].read,
|
||||||
|
);
|
||||||
|
expect(getCacheMultiplier({ model: 'claude-opus-4', cacheType: 'write' })).toBe(
|
||||||
|
cacheTokenValues['claude-opus-4'].write,
|
||||||
|
);
|
||||||
|
expect(getCacheMultiplier({ model: 'claude-opus-4', cacheType: 'read' })).toBe(
|
||||||
|
cacheTokenValues['claude-opus-4'].read,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Claude 4 model cache rates with different prefixes and suffixes', () => {
|
||||||
|
const modelVariations = [
|
||||||
|
'claude-sonnet-4',
|
||||||
|
'claude-sonnet-4-20240229',
|
||||||
|
'claude-sonnet-4-latest',
|
||||||
|
'anthropic/claude-sonnet-4',
|
||||||
|
'claude-sonnet-4/anthropic',
|
||||||
|
'claude-sonnet-4-preview',
|
||||||
|
'claude-sonnet-4-20240229-preview',
|
||||||
|
'claude-opus-4',
|
||||||
|
'claude-opus-4-20240229',
|
||||||
|
'claude-opus-4-latest',
|
||||||
|
'anthropic/claude-opus-4',
|
||||||
|
'claude-opus-4/anthropic',
|
||||||
|
'claude-opus-4-preview',
|
||||||
|
'claude-opus-4-20240229-preview',
|
||||||
|
];
|
||||||
|
|
||||||
|
modelVariations.forEach((model) => {
|
||||||
|
const isSonnet = model.includes('sonnet');
|
||||||
|
const expectedKey = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4';
|
||||||
|
|
||||||
|
expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe(
|
||||||
|
cacheTokenValues[expectedKey].write,
|
||||||
|
);
|
||||||
|
expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe(
|
||||||
|
cacheTokenValues[expectedKey].read,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,17 @@ const FinalizationRegistry = global.FinalizationRegistry || null;
|
||||||
*/
|
*/
|
||||||
const clientRegistry = FinalizationRegistry
|
const clientRegistry = FinalizationRegistry
|
||||||
? new FinalizationRegistry((heldValue) => {
|
? new FinalizationRegistry((heldValue) => {
|
||||||
try {
|
try {
|
||||||
// This will run when the client is garbage collected
|
// This will run when the client is garbage collected
|
||||||
if (heldValue && heldValue.userId) {
|
if (heldValue && heldValue.userId) {
|
||||||
logger.debug(`[FinalizationRegistry] Cleaning up client for user ${heldValue.userId}`);
|
logger.debug(`[FinalizationRegistry] Cleaning up client for user ${heldValue.userId}`);
|
||||||
} else {
|
} else {
|
||||||
logger.debug('[FinalizationRegistry] Cleaning up client');
|
logger.debug('[FinalizationRegistry] Cleaning up client');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors
|
||||||
}
|
}
|
||||||
} catch (e) {
|
})
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -134,8 +134,8 @@ function disposeClient(client) {
|
||||||
if (client.message_delta) {
|
if (client.message_delta) {
|
||||||
client.message_delta = null;
|
client.message_delta = null;
|
||||||
}
|
}
|
||||||
if (client.isClaude3 !== undefined) {
|
if (client.isClaudeLatest !== undefined) {
|
||||||
client.isClaude3 = null;
|
client.isClaudeLatest = null;
|
||||||
}
|
}
|
||||||
if (client.useMessages !== undefined) {
|
if (client.useMessages !== undefined) {
|
||||||
client.useMessages = null;
|
client.useMessages = null;
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,14 @@ function checkPromptCacheSupport(modelName) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
return (
|
||||||
modelMatch === 'claude-3-7-sonnet' ||
|
/claude-3[-.]7/.test(modelMatch) ||
|
||||||
modelMatch === 'claude-3-5-sonnet' ||
|
/claude-3[-.]5-(?:sonnet|haiku)/.test(modelMatch) ||
|
||||||
modelMatch === 'claude-3-5-haiku' ||
|
/claude-3-(?:sonnet|haiku|opus)?/.test(modelMatch) ||
|
||||||
modelMatch === 'claude-3-haiku' ||
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch) ||
|
||||||
modelMatch === 'claude-3-opus' ||
|
/claude-[4-9]-(?:sonnet|opus|haiku)?/.test(modelMatch) ||
|
||||||
modelMatch === 'claude-3.7-sonnet' ||
|
/claude-4(?:-(?:sonnet|opus|haiku))?/.test(modelMatch)
|
||||||
modelMatch === 'claude-3.5-sonnet' ||
|
);
|
||||||
modelMatch === 'claude-3.5-haiku'
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -51,6 +45,14 @@ function getClaudeHeaders(model, supportsCacheControl) {
|
||||||
'anthropic-beta':
|
'anthropic-beta':
|
||||||
'token-efficient-tools-2025-02-19,output-128k-2025-02-19,prompt-caching-2024-07-31',
|
'token-efficient-tools-2025-02-19,output-128k-2025-02-19,prompt-caching-2024-07-31',
|
||||||
};
|
};
|
||||||
|
} else if (
|
||||||
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(model) ||
|
||||||
|
/claude-[4-9]-(?:sonnet|opus|haiku)?/.test(model) ||
|
||||||
|
/claude-4(?:-(?:sonnet|opus|haiku))?/.test(model)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'anthropic-beta': 'prompt-caching-2024-07-31',
|
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||||
|
|
@ -72,7 +74,8 @@ function configureReasoning(anthropicInput, extendedOptions = {}) {
|
||||||
if (
|
if (
|
||||||
extendedOptions.thinking &&
|
extendedOptions.thinking &&
|
||||||
updatedOptions?.model &&
|
updatedOptions?.model &&
|
||||||
/claude-3[-.]7/.test(updatedOptions.model)
|
(/claude-3[-.]7/.test(updatedOptions.model) ||
|
||||||
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(updatedOptions.model))
|
||||||
) {
|
) {
|
||||||
updatedOptions.thinking = {
|
updatedOptions.thinking = {
|
||||||
type: 'enabled',
|
type: 'enabled',
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,9 @@ const anthropicModels = {
|
||||||
'claude-3.7-sonnet': 200000,
|
'claude-3.7-sonnet': 200000,
|
||||||
'claude-3-5-sonnet-latest': 200000,
|
'claude-3-5-sonnet-latest': 200000,
|
||||||
'claude-3.5-sonnet-latest': 200000,
|
'claude-3.5-sonnet-latest': 200000,
|
||||||
|
'claude-sonnet-4': 200000,
|
||||||
|
'claude-opus-4': 200000,
|
||||||
|
'claude-4': 200000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const deepseekModels = {
|
const deepseekModels = {
|
||||||
|
|
@ -246,6 +249,8 @@ const anthropicMaxOutputs = {
|
||||||
'claude-3-haiku': 4096,
|
'claude-3-haiku': 4096,
|
||||||
'claude-3-sonnet': 4096,
|
'claude-3-sonnet': 4096,
|
||||||
'claude-3-opus': 4096,
|
'claude-3-opus': 4096,
|
||||||
|
'claude-opus-4': 32000,
|
||||||
|
'claude-sonnet-4': 64000,
|
||||||
'claude-3.5-sonnet': 8192,
|
'claude-3.5-sonnet': 8192,
|
||||||
'claude-3-5-sonnet': 8192,
|
'claude-3-5-sonnet': 8192,
|
||||||
'claude-3.7-sonnet': 128000,
|
'claude-3.7-sonnet': 128000,
|
||||||
|
|
|
||||||
|
|
@ -649,3 +649,58 @@ describe('Grok Model Tests - Tokens', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Claude Model Tests', () => {
|
||||||
|
it('should return correct context length for Claude 4 models', () => {
|
||||||
|
expect(getModelMaxTokens('claude-sonnet-4')).toBe(200000);
|
||||||
|
expect(getModelMaxTokens('claude-opus-4')).toBe(200000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Claude 4 model name variations with different prefixes and suffixes', () => {
|
||||||
|
const modelVariations = [
|
||||||
|
'claude-sonnet-4',
|
||||||
|
'claude-sonnet-4-20240229',
|
||||||
|
'claude-sonnet-4-latest',
|
||||||
|
'anthropic/claude-sonnet-4',
|
||||||
|
'claude-sonnet-4/anthropic',
|
||||||
|
'claude-sonnet-4-preview',
|
||||||
|
'claude-sonnet-4-20240229-preview',
|
||||||
|
'claude-opus-4',
|
||||||
|
'claude-opus-4-20240229',
|
||||||
|
'claude-opus-4-latest',
|
||||||
|
'anthropic/claude-opus-4',
|
||||||
|
'claude-opus-4/anthropic',
|
||||||
|
'claude-opus-4-preview',
|
||||||
|
'claude-opus-4-20240229-preview',
|
||||||
|
];
|
||||||
|
|
||||||
|
modelVariations.forEach((model) => {
|
||||||
|
expect(getModelMaxTokens(model)).toBe(200000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match model names correctly for Claude 4 models', () => {
|
||||||
|
const modelVariations = [
|
||||||
|
'claude-sonnet-4',
|
||||||
|
'claude-sonnet-4-20240229',
|
||||||
|
'claude-sonnet-4-latest',
|
||||||
|
'anthropic/claude-sonnet-4',
|
||||||
|
'claude-sonnet-4/anthropic',
|
||||||
|
'claude-sonnet-4-preview',
|
||||||
|
'claude-sonnet-4-20240229-preview',
|
||||||
|
'claude-opus-4',
|
||||||
|
'claude-opus-4-20240229',
|
||||||
|
'claude-opus-4-latest',
|
||||||
|
'anthropic/claude-opus-4',
|
||||||
|
'claude-opus-4/anthropic',
|
||||||
|
'claude-opus-4-preview',
|
||||||
|
'claude-opus-4-20240229-preview',
|
||||||
|
];
|
||||||
|
|
||||||
|
modelVariations.forEach((model) => {
|
||||||
|
const isSonnet = model.includes('sonnet');
|
||||||
|
const expectedModel = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4';
|
||||||
|
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe(expectedModel);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -735,6 +735,10 @@ const sharedOpenAIModels = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const sharedAnthropicModels = [
|
const sharedAnthropicModels = [
|
||||||
|
'claude-sonnet-4-20250514',
|
||||||
|
'claude-sonnet-4-latest',
|
||||||
|
'claude-opus-4-20250514',
|
||||||
|
'claude-opus-4-latest',
|
||||||
'claude-3-7-sonnet-latest',
|
'claude-3-7-sonnet-latest',
|
||||||
'claude-3-7-sonnet-20250219',
|
'claude-3-7-sonnet-20250219',
|
||||||
'claude-3-5-haiku-20241022',
|
'claude-3-5-haiku-20241022',
|
||||||
|
|
@ -904,6 +908,9 @@ export const visionModels = [
|
||||||
'llama-3.2-90b-vision',
|
'llama-3.2-90b-vision',
|
||||||
'llama-3-2-90b-vision',
|
'llama-3-2-90b-vision',
|
||||||
'llama-4',
|
'llama-4',
|
||||||
|
'claude-opus-4',
|
||||||
|
'claude-sonnet-4',
|
||||||
|
'claude-haiku-4',
|
||||||
];
|
];
|
||||||
export enum VisionModes {
|
export enum VisionModes {
|
||||||
generative = 'generative',
|
generative = 'generative',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue