Skip to content

Commit

Permalink
✨ feat: add claude 3 to bedrock provider (lobehub#1551)
Browse files Browse the repository at this point in the history
* ✨ feat: new claude 3 models in bedrock

* ✨ support claude 3 params and stream handling

* ♻️ fix: remove useless condition check

* ✨ feat: add haiku to both bedrock and anthropic provider
  • Loading branch information
danielglh authored Mar 14, 2024
1 parent 0988466 commit 6e1fe33
Show file tree
Hide file tree
Showing 8 changed files with 395 additions and 79 deletions.
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

0 comments on commit 6e1fe33

Please sign in to comment.