Skip to content

Commit 5af03f6

Browse files
committed
✨ feat: support anthropic with vision
1 parent 0e69cb0 commit 5af03f6

File tree

8 files changed

+157
-14
lines changed

8 files changed

+157
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
},
7575
"dependencies": {
7676
"@ant-design/icons": "^5",
77-
"@anthropic-ai/sdk": "^0.14.1",
77+
"@anthropic-ai/sdk": "^0.16.0",
7878
"@auth/core": "latest",
7979
"@aws-sdk/client-bedrock-runtime": "^3.503.1",
8080
"@azure/openai": "^1.0.0-beta.11",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// @vitest-environment edge-runtime
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { POST as UniverseRoute } from '../[provider]/route';
5+
import { POST, preferredRegion, runtime } from './route';
6+
7+
// 模拟 '../[provider]/route'
8+
vi.mock('../[provider]/route', () => ({
9+
POST: vi.fn().mockResolvedValue('mocked response'),
10+
}));
11+
12+
describe('Configuration tests', () => {
13+
it('should have runtime set to "edge"', () => {
14+
expect(runtime).toBe('edge');
15+
});
16+
17+
it('should contain specific regions in preferredRegion', () => {
18+
expect(preferredRegion).not.contain(['hk1']);
19+
});
20+
});
21+
22+
describe('Google POST function tests', () => {
23+
it('should call UniverseRoute with correct parameters', async () => {
24+
const mockRequest = new Request('https://example.com', { method: 'POST' });
25+
await POST(mockRequest);
26+
expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, { params: { provider: 'google' } });
27+
});
28+
});

src/app/api/chat/anthropic/route.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { POST as UniverseRoute } from '../[provider]/route';
2+
3+
// due to the Chinese region does not support accessing Google
4+
// we need to use proxy to access it
5+
// refs: https://github.com/google/generative-ai-js/issues/29#issuecomment-1866246513
6+
// if (process.env.HTTP_PROXY_URL) {
7+
// const { setGlobalDispatcher, ProxyAgent } = require('undici');
8+
//
9+
// console.log(process.env.HTTP_PROXY_URL)
10+
// setGlobalDispatcher(new ProxyAgent({ uri: process.env.HTTP_PROXY_URL }));
11+
// }
12+
13+
// but undici only can be used in NodeJS
14+
// so if you want to use with proxy, you need comment the code below
15+
export const runtime = 'edge';
16+
17+
export const preferredRegion = [
18+
'bom1',
19+
'cle1',
20+
'cpt1',
21+
'gru1',
22+
'hnd1',
23+
'iad1',
24+
'icn1',
25+
'kix1',
26+
'pdx1',
27+
'sfo1',
28+
'sin1',
29+
'syd1',
30+
];
31+
32+
export const POST = async (req: Request) =>
33+
UniverseRoute(req, { params: { provider: 'anthropic' } });

src/config/modelProviders/anthropic.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,52 @@ import { ModelProviderCard } from '@/types/llm';
33
const Anthropic: ModelProviderCard = {
44
chatModels: [
55
{
6-
displayName: 'Claude Instant 1.2',
7-
id: 'claude-instant-1.2',
8-
tokens: 100_000,
6+
description:
7+
'Ideal balance of intelligence and speed for enterprise workloads. Maximum utility at a lower price, dependable, balanced for scaled deployments',
8+
displayName: 'Claude 3 Sonnet',
9+
id: 'claude-3-sonnet-20240229',
10+
maxOutput: 4096,
11+
tokens: 200_000,
12+
vision: true,
913
},
1014
{
11-
displayName: 'Claude 2.0',
12-
id: 'claude-2.0',
13-
tokens: 100_000,
15+
description:
16+
'Most powerful model for highly complex tasks. Top-level performance, intelligence, fluency, and understanding',
17+
displayName: 'Claude 3 Opus',
18+
id: 'claude-3-opus-20240229',
19+
maxOutput: 4096,
20+
tokens: 200_000,
21+
vision: true,
22+
},
23+
{
24+
description:
25+
'Fastest and most compact model for near-instant responsiveness. Quick and accurate targeted performance',
26+
displayName: 'Claude 3 Haiku',
27+
hidden: true,
28+
id: 'claude-3-haiku-20240229',
29+
maxOutput: 4096,
30+
tokens: 200_000,
31+
vision: true,
1432
},
1533
{
1634
displayName: 'Claude 2.1',
1735
id: 'claude-2.1',
36+
maxOutput: 4096,
1837
tokens: 200_000,
1938
},
39+
{
40+
displayName: 'Claude 2.0',
41+
id: 'claude-2.0',
42+
maxOutput: 4096,
43+
tokens: 100_000,
44+
},
45+
{
46+
displayName: 'Claude Instant 1.2',
47+
hidden: true,
48+
id: 'claude-instant-1.2',
49+
maxOutput: 4096,
50+
tokens: 100_000,
51+
},
2052
],
2153
id: 'anthropic',
2254
};

src/libs/agent-runtime/anthropic/index.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1+
// sort-imports-ignore
2+
import '@anthropic-ai/sdk/shims/web';
13
import Anthropic from '@anthropic-ai/sdk';
24
import { AnthropicStream, StreamingTextResponse } from 'ai';
35
import { ClientOptions } from 'openai';
46

57
import { LobeRuntimeAI } from '../BaseAI';
68
import { AgentRuntimeErrorType } from '../error';
7-
import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
9+
import {
10+
ChatCompetitionOptions,
11+
ChatStreamPayload,
12+
ModelProvider,
13+
OpenAIChatMessage,
14+
UserMessageContentPart,
15+
} from '../types';
816
import { AgentRuntimeError } from '../utils/createError';
917
import { debugStream } from '../utils/debugStream';
1018
import { handleOpenAIError } from '../utils/handleOpenAIError';
19+
import { parseDataUri } from '../utils/uriParser';
1120

1221
export class LobeAnthropicAI implements LobeRuntimeAI {
1322
private client: Anthropic;
@@ -18,14 +27,31 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
1827
this.client = new Anthropic({ apiKey });
1928
}
2029

30+
private buildAnthropicMessages = (
31+
messages: OpenAIChatMessage[],
32+
): Anthropic.Messages.MessageParam[] =>
33+
messages.map((message) => this.convertToAnthropicMessage(message));
34+
35+
private convertToAnthropicMessage = (
36+
message: OpenAIChatMessage,
37+
): Anthropic.Messages.MessageParam => {
38+
const content = message.content as string | UserMessageContentPart[];
39+
40+
return {
41+
content:
42+
typeof content === 'string' ? content : content.map((c) => this.convertToAnthropicBlock(c)),
43+
role: message.role === 'function' || message.role === 'system' ? 'assistant' : message.role,
44+
};
45+
};
46+
2147
async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
2248
const { messages, model, max_tokens, temperature, top_p } = payload;
2349
const system_message = messages.find((m) => m.role === 'system');
2450
const user_messages = messages.filter((m) => m.role !== 'system');
2551

26-
const requestPramas: Anthropic.MessageCreateParams = {
52+
const requestParams: Anthropic.MessageCreateParams = {
2753
max_tokens: max_tokens || 1024,
28-
messages: user_messages as Anthropic.Messages.MessageParam[],
54+
messages: this.buildAnthropicMessages(user_messages),
2955
model: model,
3056
stream: true,
3157
system: system_message?.content as string,
@@ -34,7 +60,7 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
3460
};
3561

3662
try {
37-
const response = await this.client.messages.create(requestPramas);
63+
const response = await this.client.messages.create(requestParams);
3864
const [prod, debug] = response.tee();
3965

4066
if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
@@ -78,6 +104,29 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
78104
});
79105
}
80106
}
107+
108+
private convertToAnthropicBlock(
109+
content: UserMessageContentPart,
110+
): Anthropic.ContentBlock | Anthropic.ImageBlockParam {
111+
switch (content.type) {
112+
case 'text': {
113+
return content;
114+
}
115+
116+
case 'image_url': {
117+
const { mimeType, base64 } = parseDataUri(content.image_url.url);
118+
119+
return {
120+
source: {
121+
data: base64 as string,
122+
media_type: mimeType as Anthropic.ImageBlockParam.Source['media_type'],
123+
type: 'base64',
124+
},
125+
type: 'image',
126+
};
127+
}
128+
}
129+
}
81130
}
82131

83132
export default LobeAnthropicAI;

src/libs/agent-runtime/types/chat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ interface UserMessageContentPartText {
66
text: string;
77
type: 'text';
88
}
9-
interface UserMessageContentPartImage {
9+
10+
export interface UserMessageContentPartImage {
1011
image_url: {
1112
detail?: 'auto' | 'low' | 'high';
1213
url: string;

src/services/file.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ class FileService {
3434
}))();
3535

3636
// 压缩图片
37-
const fileType = 'image/webp';
38-
const base64String = compressImage({ img, type: fileType });
37+
const base64String = compressImage({ img, type: file.fileType });
3938
const binaryString = atob(base64String.split('base64,')[1]);
4039
const uint8Array = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
4140
file.data = uint8Array.buffer;

src/types/llm.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface ChatModelCard {
1919
* whether model is legacy (deprecated but not removed yet)
2020
*/
2121
legacy?: boolean;
22+
maxOutput?: number;
2223
tokens?: number;
2324
/**
2425
* whether model supports vision

0 commit comments

Comments
 (0)