Skip to content

✨ feat: add claude 3 to bedrock provider #1551

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"@ant-design/icons": "^5",
"@anthropic-ai/sdk": "^0.17.0",
"@auth/core": "latest",
"@aws-sdk/client-bedrock-runtime": "^3.503.1",
"@aws-sdk/client-bedrock-runtime": "^3.525.0",
"@azure/openai": "^1.0.0-beta.11",
"@cfworker/json-schema": "^1",
"@google/generative-ai": "^0.2.0",
Expand Down
3 changes: 1 addition & 2 deletions src/config/modelProviders/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ const Anthropic: ModelProviderCard = {
description:
'Fastest and most compact model for near-instant responsiveness. Quick and accurate targeted performance',
displayName: 'Claude 3 Haiku',
hidden: true,
id: 'claude-3-haiku-20240229',
id: 'claude-3-haiku-20240307',
maxOutput: 4096,
tokens: 200_000,
vision: true,
Expand Down
26 changes: 21 additions & 5 deletions src/config/modelProviders/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,34 @@ const Bedrock: ModelProviderCard = {
},
{
description:
'Claude Instant 1.2 v1.2,上下文大小等于 100k,一个更快更便宜但仍然非常能干的模型,可以处理包括随意对话在内的多种任务。',
displayName: 'Claude Instant 1.2',
id: 'anthropic.claude-instant-v1',
tokens: 100_000,
'Anthropic 推出的 Claude 3 Sonnet 模型在智能和速度之间取得理想的平衡,尤其是在处理企业工作负载方面。该模型提供最大的效用,同时价格低于竞争产品,并且其经过精心设计,是大规模部署人工智能的可信赖、高耐久性骨干模型。 Claude 3 Sonnet 可以处理图像和返回文本输出,并且提供 200K 上下文窗口。',
displayName: 'Claude 3 Sonnet',
id: 'anthropic.claude-3-sonnet-20240229-v1:0',
tokens: 200_000,
vision: true,
},
{
description:
'Claude 3 Haiku 是 Anthropic 最快速、最紧凑的模型,具有近乎即时的响应能力。该模型可以快速回答简单的查询和请求。客户将能够构建模仿人类交互的无缝人工智能体验。 Claude 3 Haiku 可以处理图像和返回文本输出,并且提供 200K 上下文窗口。',
displayName: 'Claude 3 Haiku',
id: 'anthropic.claude-3-haiku-20240307-v1:0',
tokens: 200_000,
vision: true,
},
{
description:
'Claude 2.1 v2.1,上下文大小等于 200kClaude 2 的更新版本,特性包括双倍的上下文窗口,以及在可靠性等方面的提升。',
'Claude 2.1 v2.1,上下文大小等于 200kClaude 2 的更新版本,采用双倍的上下文窗口,并在长文档和 RAG 上下文中提高可靠性、幻觉率和循证准确性。',
displayName: 'Claude 2.1',
id: 'anthropic.claude-v2:1',
tokens: 200_000,
},
{
description:
'Claude Instant 1.2 v1.2,上下文大小等于 100k。一种更快速、更实惠但仍然非常强大的模型,它可以处理一系列任务,包括随意对话、文本分析、摘要和文档问题回答。',
displayName: 'Claude Instant 1.2',
id: 'anthropic.claude-instant-v1',
tokens: 100_000,
},
{
description: 'Llama 2 Chat 13B v1,上下文大小为 4k,Llama 2 模型的对话用例优化变体。',
displayName: 'Llama 2 Chat 13B',
Expand Down
67 changes: 12 additions & 55 deletions src/libs/agent-runtime/anthropic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import { AgentRuntimeErrorType } from '../error';
import {
ChatCompetitionOptions,
ChatStreamPayload,
ModelProvider,
OpenAIChatMessage,
UserMessageContentPart,
ModelProvider
} from '../types';
import { AgentRuntimeError } from '../utils/createError';
import { debugStream } from '../utils/debugStream';
import { desensitizeUrl } from '../utils/desensitizeUrl';
import { parseDataUri } from '../utils/uriParser';
import { buildAnthropicMessages } from '../utils/anthropicHelpers';

const DEFAULT_BASE_URL = 'https://api.anthropic.com';

Expand All @@ -32,40 +30,22 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
this.baseURL = this.client.baseURL;
}

private buildAnthropicMessages = (
messages: OpenAIChatMessage[],
): Anthropic.Messages.MessageParam[] =>
messages.map((message) => this.convertToAnthropicMessage(message));

private convertToAnthropicMessage = (
message: OpenAIChatMessage,
): Anthropic.Messages.MessageParam => {
const content = message.content as string | UserMessageContentPart[];

return {
content:
typeof content === 'string' ? content : content.map((c) => this.convertToAnthropicBlock(c)),
role: message.role === 'function' || message.role === 'system' ? 'assistant' : message.role,
};
};

async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
const { messages, model, max_tokens, temperature, top_p } = payload;
const system_message = messages.find((m) => m.role === 'system');
const user_messages = messages.filter((m) => m.role !== 'system');

const requestParams: Anthropic.MessageCreateParams = {
max_tokens: max_tokens || 4096,
messages: this.buildAnthropicMessages(user_messages),
model: model,
stream: true,
system: system_message?.content as string,
temperature: temperature,
top_p: top_p,
};

try {
const response = await this.client.messages.create(requestParams);
const response = await this.client.messages.create({
max_tokens: max_tokens || 4096,
messages: buildAnthropicMessages(user_messages),
model: model,
stream: true,
system: system_message?.content as string,
temperature: temperature,
top_p: top_p,
});

const [prod, debug] = response.tee();

if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
Expand Down Expand Up @@ -105,29 +85,6 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
});
}
}

private convertToAnthropicBlock(
content: UserMessageContentPart,
): Anthropic.ContentBlock | Anthropic.ImageBlockParam {
switch (content.type) {
case 'text': {
return content;
}

case 'image_url': {
const { mimeType, base64 } = parseDataUri(content.image_url.url);

return {
source: {
data: base64 as string,
media_type: mimeType as Anthropic.ImageBlockParam.Source['media_type'],
type: 'base64',
},
type: 'image',
};
}
}
}
}

export default LobeAnthropicAI;
217 changes: 217 additions & 0 deletions src/libs/agent-runtime/bedrock/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// @vitest-environment node
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
InvokeModelWithResponseStreamCommand,
} from '@aws-sdk/client-bedrock-runtime';
import * as debugStreamModule from '../utils/debugStream';
import { LobeBedrockAI } from './index';

const provider = 'bedrock';

// Mock the console.error to avoid polluting test output
vi.spyOn(console, 'error').mockImplementation(() => {});

vi.mock("@aws-sdk/client-bedrock-runtime", async (importOriginal) => {
const module = await importOriginal();
return {
...(module as any),
InvokeModelWithResponseStreamCommand: vi.fn()
}
})

let instance: LobeBedrockAI;

beforeEach(() => {
instance = new LobeBedrockAI({
region: 'us-west-2',
accessKeyId: 'test-access-key-id',
accessKeySecret: 'test-access-key-secret',
});

vi.spyOn(instance['client'], 'send').mockReturnValue(new ReadableStream() as any);
});

afterEach(() => {
vi.clearAllMocks();
});

describe('LobeBedrockAI', () => {
describe('init', () => {
it('should correctly initialize with AWS credentials', async () => {
const instance = new LobeBedrockAI({
region: 'us-west-2',
accessKeyId: 'test-access-key-id',
accessKeySecret: 'test-access-key-secret',
});
expect(instance).toBeInstanceOf(LobeBedrockAI);
});
});

describe('chat', () => {

describe('Claude model', () => {

it('should return a Response on successful API call', async () => {
const result = await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'anthropic.claude-v2:1',
temperature: 0,
});

// Assert
expect(result).toBeInstanceOf(Response);
});

it('should handle text messages correctly', async () => {
// Arrange
const mockStream = new ReadableStream({
start(controller) {
controller.enqueue('Hello, world!');
controller.close();
},
});
const mockResponse = Promise.resolve(mockStream);
(instance['client'].send as Mock).mockResolvedValue(mockResponse);

// Act
const result = await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'anthropic.claude-v2:1',
temperature: 0,
top_p: 1,
});

// Assert
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
accept: 'application/json',
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 4096,
messages: [{ content: 'Hello', role: 'user' }],
temperature: 0,
top_p: 1,
}),
contentType: 'application/json',
modelId: 'anthropic.claude-v2:1',
});
expect(result).toBeInstanceOf(Response);
});

it('should handle system prompt correctly', async () => {
// Arrange
const mockStream = new ReadableStream({
start(controller) {
controller.enqueue('Hello, world!');
controller.close();
},
});
const mockResponse = Promise.resolve(mockStream);
(instance['client'].send as Mock).mockResolvedValue(mockResponse);

// Act
const result = await instance.chat({
messages: [
{ content: 'You are an awesome greeter', role: 'system' },
{ content: 'Hello', role: 'user' },
],
model: 'anthropic.claude-v2:1',
temperature: 0,
top_p: 1,
});

// Assert
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
accept: 'application/json',
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 4096,
messages: [{ content: 'Hello', role: 'user' }],
system: 'You are an awesome greeter',
temperature: 0,
top_p: 1,
}),
contentType: 'application/json',
modelId: 'anthropic.claude-v2:1',
});
expect(result).toBeInstanceOf(Response);
});

it('should call Anthropic model with supported opions', async () => {
// Arrange
const mockStream = new ReadableStream({
start(controller) {
controller.enqueue('Hello, world!');
controller.close();
},
});
const mockResponse = Promise.resolve(mockStream);
(instance['client'].send as Mock).mockResolvedValue(mockResponse);

// Act
const result = await instance.chat({
max_tokens: 2048,
messages: [{ content: 'Hello', role: 'user' }],
model: 'anthropic.claude-v2:1',
temperature: 0.5,
top_p: 1,
});

// Assert
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
accept: 'application/json',
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 2048,
messages: [{ content: 'Hello', role: 'user' }],
temperature: 0.5,
top_p: 1,
}),
contentType: 'application/json',
modelId: 'anthropic.claude-v2:1',
});
expect(result).toBeInstanceOf(Response);
});

it('should call Anthropic model without unsupported opions', async () => {
// Arrange
const mockStream = new ReadableStream({
start(controller) {
controller.enqueue('Hello, world!');
controller.close();
},
});
const mockResponse = Promise.resolve(mockStream);
(instance['client'].send as Mock).mockResolvedValue(mockResponse);

// Act
const result = await instance.chat({
frequency_penalty: 0.5, // Unsupported option
max_tokens: 2048,
messages: [{ content: 'Hello', role: 'user' }],
model: 'anthropic.claude-v2:1',
presence_penalty: 0.5,
temperature: 0.5,
top_p: 1,
});

// Assert
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
accept: 'application/json',
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 2048,
messages: [{ content: 'Hello', role: 'user' }],
temperature: 0.5,
top_p: 1,
}),
contentType: 'application/json',
modelId: 'anthropic.claude-v2:1',
});
expect(result).toBeInstanceOf(Response);
});

});

});
});
Loading