@prysm-ai/llm 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/base.d.ts +17 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +26 -0
- package/dist/base.js.map +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +12 -0
- package/dist/constants.js.map +1 -0
- package/dist/content.d.ts +18 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +18 -0
- package/dist/content.js.map +1 -0
- package/dist/factory/createModel.d.ts +38 -0
- package/dist/factory/createModel.d.ts.map +1 -0
- package/dist/factory/createModel.js +44 -0
- package/dist/factory/createModel.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/provider/anthropic.d.ts +45 -0
- package/dist/provider/anthropic.d.ts.map +1 -0
- package/dist/provider/anthropic.js +290 -0
- package/dist/provider/anthropic.js.map +1 -0
- package/dist/provider/openai.d.ts +84 -0
- package/dist/provider/openai.d.ts.map +1 -0
- package/dist/provider/openai.js +234 -0
- package/dist/provider/openai.js.map +1 -0
- package/dist/provider/proxy.d.ts +53 -0
- package/dist/provider/proxy.d.ts.map +1 -0
- package/dist/provider/proxy.js +113 -0
- package/dist/provider/proxy.js.map +1 -0
- package/dist/router.d.ts +21 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +86 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
- package/src/base.ts +43 -0
- package/src/constants.ts +13 -0
- package/src/content.ts +36 -0
- package/src/factory/createModel.ts +64 -0
- package/src/index.ts +9 -0
- package/src/provider/anthropic.ts +359 -0
- package/src/provider/openai.ts +280 -0
- package/src/provider/proxy.ts +171 -0
- package/src/router.ts +113 -0
- package/src/types.ts +77 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +14 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { MessageContent } from './content';
|
|
3
|
+
export declare const ChatMessageRoleSchema: z.ZodEnum<["system", "user", "assistant", "tool"]>;
|
|
4
|
+
export type ChatMessageRole = z.infer<typeof ChatMessageRoleSchema>;
|
|
5
|
+
export interface ChatMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
role: ChatMessageRole;
|
|
8
|
+
content: MessageContent;
|
|
9
|
+
name?: string;
|
|
10
|
+
toolCallId?: string;
|
|
11
|
+
toolCalls?: ToolCall[];
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface ToolCall {
|
|
15
|
+
id: string;
|
|
16
|
+
type: 'function';
|
|
17
|
+
function: {
|
|
18
|
+
name: string;
|
|
19
|
+
arguments: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface LLMResponse {
|
|
23
|
+
id: string;
|
|
24
|
+
model: string;
|
|
25
|
+
role: 'assistant';
|
|
26
|
+
content: string;
|
|
27
|
+
toolCalls?: ToolCall[];
|
|
28
|
+
finishReason: 'stop' | 'length' | 'content_filter' | 'tool_calls' | 'function_call';
|
|
29
|
+
usage: {
|
|
30
|
+
promptTokens: number;
|
|
31
|
+
completionTokens: number;
|
|
32
|
+
totalTokens: number;
|
|
33
|
+
};
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface EmbedResponse {
|
|
37
|
+
embeddings: number[][];
|
|
38
|
+
model: string;
|
|
39
|
+
usage?: {
|
|
40
|
+
promptTokens: number;
|
|
41
|
+
totalTokens: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface LLMRequestOptions {
|
|
45
|
+
model: string;
|
|
46
|
+
temperature?: number;
|
|
47
|
+
maxTokens?: number;
|
|
48
|
+
topP?: number;
|
|
49
|
+
tools?: unknown[];
|
|
50
|
+
toolChoice?: unknown;
|
|
51
|
+
responseFormat?: {
|
|
52
|
+
type: 'text' | 'json_object';
|
|
53
|
+
};
|
|
54
|
+
stop?: string[];
|
|
55
|
+
stream?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface EmbedRequest {
|
|
58
|
+
model: string;
|
|
59
|
+
input: string | string[];
|
|
60
|
+
}
|
|
61
|
+
export type LLMProvider = 'openai' | 'anthropic' | 'local';
|
|
62
|
+
export interface ModelInfo {
|
|
63
|
+
provider: LLMProvider;
|
|
64
|
+
model: string;
|
|
65
|
+
displayName: string;
|
|
66
|
+
contextWindow: number;
|
|
67
|
+
supportsTools: boolean;
|
|
68
|
+
supportsVision: boolean;
|
|
69
|
+
maxOutputTokens?: number;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,eAAO,MAAM,qBAAqB,oDAAkD,CAAC;AACrF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,QAAQ,GAAG,gBAAgB,GAAG,YAAY,GAAG,eAAe,CAAC;IACpF,KAAK,EAAE;QACL,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAC;IAClD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,WAAW,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prysm-ai/llm",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "LLM abstraction layer for prysm-ai",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/prysm-ai/llm.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"prysm-ai",
|
|
20
|
+
"llm",
|
|
21
|
+
"ai",
|
|
22
|
+
"agent"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"openai": "^4.0.0",
|
|
27
|
+
"zod": "^3.23.0",
|
|
28
|
+
"@prysm-ai/core": "0.3.0",
|
|
29
|
+
"@prysm-ai/utils": "0.3.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@vitest/ui": "^2.0.0",
|
|
33
|
+
"typescript": "^5.4.0",
|
|
34
|
+
"vitest": "^2.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"build:prod": "tsc --sourceMap false --declarationMap false",
|
|
39
|
+
"build:watch": "tsc --watch",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"clean": "rm -rf dist",
|
|
42
|
+
"test": "vitest",
|
|
43
|
+
"test:ui": "vitest --ui",
|
|
44
|
+
"pub": "pnpm build:prod && npm publish --access public"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/base.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ChatMessage, LLMResponse, LLMRequestOptions, ModelInfo, EmbedRequest, EmbedResponse, LLMProvider } from './types';
|
|
2
|
+
import type { StreamingOptions } from './provider/openai';
|
|
3
|
+
|
|
4
|
+
export abstract class BaseLLMProvider {
|
|
5
|
+
abstract readonly provider: LLMProvider;
|
|
6
|
+
abstract readonly defaultModel: string;
|
|
7
|
+
|
|
8
|
+
protected apiKey!: string | undefined;
|
|
9
|
+
protected baseUrl!: string | undefined;
|
|
10
|
+
|
|
11
|
+
constructor(apiKey?: string, baseUrl?: string) {
|
|
12
|
+
this.apiKey = apiKey;
|
|
13
|
+
this.baseUrl = baseUrl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
abstract chat(messages: ChatMessage[], options?: LLMRequestOptions): Promise<LLMResponse>;
|
|
17
|
+
|
|
18
|
+
abstract chatStream(messages: ChatMessage[], options: StreamingOptions): Promise<void>;
|
|
19
|
+
|
|
20
|
+
abstract listModels(): Promise<ModelInfo[]>;
|
|
21
|
+
|
|
22
|
+
abstract validateKey(): Promise<boolean>;
|
|
23
|
+
|
|
24
|
+
async embed(_request: EmbedRequest): Promise<EmbedResponse> {
|
|
25
|
+
throw new Error('Embed not supported by this provider');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getProvider(): LLMProvider {
|
|
29
|
+
return this.provider;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected applyRequestOptions(requestBody: Record<string, unknown>, options: LLMRequestOptions): void {
|
|
33
|
+
if (options.temperature !== undefined) {
|
|
34
|
+
requestBody.temperature = options.temperature;
|
|
35
|
+
}
|
|
36
|
+
if (options.maxTokens !== undefined) {
|
|
37
|
+
requestBody.max_tokens = options.maxTokens;
|
|
38
|
+
}
|
|
39
|
+
if (options.stop !== undefined) {
|
|
40
|
+
requestBody.stop = options.stop;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const MessageRole = {
|
|
2
|
+
System: 'system',
|
|
3
|
+
User: 'user',
|
|
4
|
+
Assistant: 'assistant',
|
|
5
|
+
Tool: 'tool',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type MessageRole = (typeof MessageRole)[keyof typeof MessageRole];
|
|
9
|
+
|
|
10
|
+
export enum LLMProviderType {
|
|
11
|
+
OpenAI = 'openai',
|
|
12
|
+
Anthropic = 'anthropic',
|
|
13
|
+
}
|
package/src/content.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Message content types for multimodal support
|
|
2
|
+
|
|
3
|
+
export type MessageContent = string | MessagePart[];
|
|
4
|
+
|
|
5
|
+
export interface TextPart {
|
|
6
|
+
type: 'text';
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ImagePart {
|
|
11
|
+
type: 'image_url';
|
|
12
|
+
image_url: {
|
|
13
|
+
url: string;
|
|
14
|
+
detail?: 'low' | 'high' | 'auto';
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type MessagePart = TextPart | ImagePart;
|
|
19
|
+
|
|
20
|
+
export function isTextPart(part: MessagePart): part is TextPart {
|
|
21
|
+
return part.type === 'text';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isImagePart(part: MessagePart): part is ImagePart {
|
|
25
|
+
return part.type === 'image_url';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hasImages(content: MessageContent): boolean {
|
|
29
|
+
if (typeof content === 'string') return false;
|
|
30
|
+
return content.some(part => part.type === 'image_url');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getTextContent(content: MessageContent): string {
|
|
34
|
+
if (typeof content === 'string') return content;
|
|
35
|
+
return content.filter(isTextPart).map(p => p.text).join('\n');
|
|
36
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { OpenAIProvider, OpenAIProviderConfig } from '../provider/openai';
|
|
2
|
+
import { AnthropicProvider, AnthropicProviderConfig } from '../provider/anthropic';
|
|
3
|
+
import { BaseLLMProvider } from '../base';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* createModel 配置选项
|
|
7
|
+
*/
|
|
8
|
+
export interface CreateModelOptions {
|
|
9
|
+
/** 接口类型: openai | anthropic */
|
|
10
|
+
type: 'openai' | 'anthropic';
|
|
11
|
+
/** 模型接口地址 */
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
/** API 密钥 */
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/** 模型名称 */
|
|
16
|
+
model: string;
|
|
17
|
+
/** 可选: API 路径 */
|
|
18
|
+
apiPath?: string;
|
|
19
|
+
/** 可选: 超时时间(毫秒) */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** 可选: 默认 temperature */
|
|
22
|
+
temperature?: number;
|
|
23
|
+
/** 可选: 默认 maxTokens */
|
|
24
|
+
maxTokens?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 创建模型实例工厂函数
|
|
29
|
+
* 根据 type 创建对应的 Provider 实例,返回可直接使用的模型
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const model = createModel({
|
|
33
|
+
* type: 'openai',
|
|
34
|
+
* baseUrl: 'https://api.moonshot.cn/v1',
|
|
35
|
+
* apiKey: process.env.MOONSHOT_API_KEY,
|
|
36
|
+
* model: 'kimi-k2.6',
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* const response = await model.chat(messages);
|
|
40
|
+
*/
|
|
41
|
+
export function createModel(options: CreateModelOptions): BaseLLMProvider {
|
|
42
|
+
const { type, baseUrl, apiKey, apiPath, timeoutMs } = options;
|
|
43
|
+
|
|
44
|
+
switch (type) {
|
|
45
|
+
case 'openai': {
|
|
46
|
+
const config: OpenAIProviderConfig = { apiKey };
|
|
47
|
+
if (baseUrl) config.baseUrl = baseUrl;
|
|
48
|
+
if (apiPath) config.apiPath = apiPath;
|
|
49
|
+
if (timeoutMs) config.timeoutMs = timeoutMs;
|
|
50
|
+
return new OpenAIProvider(config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case 'anthropic': {
|
|
54
|
+
const config: AnthropicProviderConfig = { apiKey };
|
|
55
|
+
if (baseUrl) config.baseUrl = baseUrl;
|
|
56
|
+
if (apiPath) config.apiPath = apiPath;
|
|
57
|
+
if (timeoutMs) config.timeoutMs = timeoutMs;
|
|
58
|
+
return new AnthropicProvider(config);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
throw new Error(`Unsupported provider type: ${type}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './base';
|
|
3
|
+
export * from './provider/openai';
|
|
4
|
+
export * from './provider/anthropic';
|
|
5
|
+
export * from './provider/proxy';
|
|
6
|
+
export * from './router';
|
|
7
|
+
export * from './content';
|
|
8
|
+
export * from './constants';
|
|
9
|
+
export * from './factory/createModel';
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import type { ChatMessage, LLMResponse, LLMRequestOptions, ModelInfo } from '../types';
|
|
2
|
+
import type { MessageContent, MessagePart } from '../content';
|
|
3
|
+
import { BaseLLMProvider } from '../base';
|
|
4
|
+
import { isTextPart } from '../content';
|
|
5
|
+
import { MessageRole } from '../constants';
|
|
6
|
+
|
|
7
|
+
export interface AnthropicStreamingOptions extends Omit<LLMRequestOptions, 'stream'> {
|
|
8
|
+
stream: true;
|
|
9
|
+
onChunk?: (chunk: string) => void;
|
|
10
|
+
onComplete?: (fullContent: string) => void;
|
|
11
|
+
onError?: (error: Error) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AnthropicProviderConfig {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
apiPath?: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AnthropicImageBlock {
|
|
22
|
+
type: 'image';
|
|
23
|
+
source: {
|
|
24
|
+
type: 'base64' | 'url';
|
|
25
|
+
media_type: string;
|
|
26
|
+
data: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface AnthropicContentBlock {
|
|
31
|
+
type: 'text';
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type AnthropicContent = AnthropicContentBlock | AnthropicImageBlock;
|
|
36
|
+
|
|
37
|
+
export class AnthropicProvider extends BaseLLMProvider {
|
|
38
|
+
readonly provider: 'anthropic' = 'anthropic';
|
|
39
|
+
override readonly defaultModel = 'claude-3-5-sonnet-20241022';
|
|
40
|
+
|
|
41
|
+
private authToken: string;
|
|
42
|
+
private timeoutMs: number;
|
|
43
|
+
private apiUrl: string;
|
|
44
|
+
|
|
45
|
+
constructor(config: AnthropicProviderConfig) {
|
|
46
|
+
super(config.apiKey, config.baseUrl);
|
|
47
|
+
this.authToken = config.apiKey;
|
|
48
|
+
this.timeoutMs = config.timeoutMs ?? 60000;
|
|
49
|
+
const base = config.baseUrl ?? 'https://api.anthropic.com';
|
|
50
|
+
const path = config.apiPath ?? '/v1/messages';
|
|
51
|
+
this.apiUrl = `${base}${path}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async chat(messages: ChatMessage[], options: LLMRequestOptions = { model: this.defaultModel }): Promise<LLMResponse> {
|
|
55
|
+
const systemMessage = messages.find(m => m.role === MessageRole.System);
|
|
56
|
+
const conversationMessages = messages.filter(m => m.role !== MessageRole.System);
|
|
57
|
+
|
|
58
|
+
const requestBody: Record<string, unknown> = {
|
|
59
|
+
model: options.model,
|
|
60
|
+
messages: conversationMessages.map(msg => ({
|
|
61
|
+
role: msg.role,
|
|
62
|
+
content: this.normalizeContent(msg.content),
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (systemMessage) {
|
|
67
|
+
requestBody.system = this.getTextContent(systemMessage.content);
|
|
68
|
+
}
|
|
69
|
+
this.applyRequestOptions(requestBody, options);
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(this.apiUrl, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'x-api-key': this.authToken,
|
|
78
|
+
'anthropic-version': '2023-06-01',
|
|
79
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(requestBody),
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
clearTimeout(timeoutId);
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const error = await response.text();
|
|
89
|
+
throw new Error(`Anthropic API error: ${response.status} - ${error}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = await response.json() as {
|
|
93
|
+
id: string;
|
|
94
|
+
model: string;
|
|
95
|
+
stop_reason: string | null;
|
|
96
|
+
usage: { input_tokens: number; output_tokens: number };
|
|
97
|
+
content: Array<{ type: string; text?: string; id?: string; name?: string; input?: Record<string, unknown> }>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const responseMessage = data.content.find(c => c.type === 'text');
|
|
101
|
+
const toolCalls = data.content
|
|
102
|
+
.filter(c => c.type === 'tool_use')
|
|
103
|
+
.map(c => ({
|
|
104
|
+
id: c.id ?? '',
|
|
105
|
+
type: 'function' as const,
|
|
106
|
+
function: {
|
|
107
|
+
name: c.name ?? '',
|
|
108
|
+
arguments: JSON.stringify(c.input ?? {}),
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const result: LLMResponse = {
|
|
113
|
+
id: data.id,
|
|
114
|
+
model: data.model,
|
|
115
|
+
role: 'assistant',
|
|
116
|
+
content: responseMessage?.text ?? '',
|
|
117
|
+
finishReason: mapFinishReason(data.stop_reason),
|
|
118
|
+
usage: {
|
|
119
|
+
promptTokens: data.usage.input_tokens,
|
|
120
|
+
completionTokens: data.usage.output_tokens,
|
|
121
|
+
totalTokens: data.usage.input_tokens + data.usage.output_tokens,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (toolCalls.length > 0) {
|
|
126
|
+
result.toolCalls = toolCalls;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
clearTimeout(timeoutId);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async chatStream(messages: ChatMessage[], options: AnthropicStreamingOptions): Promise<void> {
|
|
137
|
+
const systemMessage = messages.find(m => m.role === MessageRole.System);
|
|
138
|
+
const conversationMessages = messages.filter(m => m.role !== MessageRole.System);
|
|
139
|
+
|
|
140
|
+
const requestBody: Record<string, unknown> = {
|
|
141
|
+
model: options.model,
|
|
142
|
+
messages: conversationMessages.map(msg => ({
|
|
143
|
+
role: msg.role,
|
|
144
|
+
content: this.normalizeContent(msg.content),
|
|
145
|
+
})),
|
|
146
|
+
stream: true,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (systemMessage) {
|
|
150
|
+
requestBody.system = this.getTextContent(systemMessage.content);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (options.temperature !== undefined) {
|
|
154
|
+
requestBody.temperature = options.temperature;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (options.maxTokens !== undefined) {
|
|
158
|
+
requestBody.max_tokens = options.maxTokens;
|
|
159
|
+
} else {
|
|
160
|
+
requestBody.max_tokens = 4096;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.applyRequestOptions(requestBody, options);
|
|
164
|
+
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(this.apiUrl, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
'x-api-key': this.authToken,
|
|
174
|
+
'anthropic-version': '2023-06-01',
|
|
175
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify(requestBody),
|
|
178
|
+
signal: controller.signal,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
clearTimeout(timeoutId);
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const error = await response.text();
|
|
185
|
+
throw new Error(`Anthropic API error: ${response.status} - ${error}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// SSE streaming response
|
|
189
|
+
const reader = response.body?.getReader();
|
|
190
|
+
if (!reader) {
|
|
191
|
+
throw new Error('No response body');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const decoder = new TextDecoder();
|
|
195
|
+
let fullContent = '';
|
|
196
|
+
let buffer = '';
|
|
197
|
+
|
|
198
|
+
while (true) {
|
|
199
|
+
const { done, value } = await reader.read();
|
|
200
|
+
if (done) break;
|
|
201
|
+
|
|
202
|
+
buffer += decoder.decode(value, { stream: true });
|
|
203
|
+
const lines = buffer.split('\n');
|
|
204
|
+
buffer = lines.pop() ?? '';
|
|
205
|
+
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
if (line.startsWith('data: ')) {
|
|
208
|
+
const data = line.slice(6);
|
|
209
|
+
if (data === '[DONE]') continue;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const event = JSON.parse(data);
|
|
213
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
214
|
+
const text = event.delta.text;
|
|
215
|
+
fullContent += text;
|
|
216
|
+
options.onChunk?.(text);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Ignore parse errors for partial lines
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
options.onComplete?.(fullContent);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 将消息内容规范化为 Anthropic 格式
|
|
234
|
+
* @param content - MessageContent 类型: string | MessagePart[]
|
|
235
|
+
* - string: 纯文本内容,直接返回
|
|
236
|
+
* - MessagePart[]: 文本/图片部分数组,转换为 Anthropic 内容块
|
|
237
|
+
* @returns 纯文本返回 string,多模态内容返回 AnthropicContent[]
|
|
238
|
+
*/
|
|
239
|
+
private normalizeContent(content: MessageContent): string | AnthropicContent[] {
|
|
240
|
+
// 步骤1: 如果内容是纯字符串,直接返回(Anthropic 接受字符串格式)
|
|
241
|
+
if (typeof content === 'string') {
|
|
242
|
+
return content;
|
|
243
|
+
}
|
|
244
|
+
// 步骤2: 如果内容是数组,规范化每个部分
|
|
245
|
+
return content.map(part => this.normalizePart(part));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 将单个消息部分规范化为 Anthropic 内容块
|
|
250
|
+
* @param part - MessagePart 类型: TextPart | ImagePart
|
|
251
|
+
* - TextPart: { type: 'text', text: string }
|
|
252
|
+
* - ImagePart: { type: 'image_url', image_url: { url: string, detail?: 'low'|'high'|'auto' } }
|
|
253
|
+
* @returns AnthropicContent: { type: 'text', text: string } | { type: 'image', source: {...} }
|
|
254
|
+
*/
|
|
255
|
+
private normalizePart(part: MessagePart): AnthropicContent {
|
|
256
|
+
// 步骤1: 处理文本部分 - 转换为 Anthropic 文本块
|
|
257
|
+
if (part.type === 'text') {
|
|
258
|
+
return { type: 'text', text: part.text };
|
|
259
|
+
}
|
|
260
|
+
// 步骤2: 处理图片部分 - 转换为 Anthropic 图片块
|
|
261
|
+
if (part.type === 'image_url') {
|
|
262
|
+
const urlOrData = part.image_url.url;
|
|
263
|
+
// 步骤2a: 如果是 data URI(base64),解析并转换为 Anthropic base64 格式
|
|
264
|
+
if (urlOrData.startsWith('data:')) {
|
|
265
|
+
// 解析 data URI 格式: data:image/png;base64,<编码数据>
|
|
266
|
+
const match = urlOrData.match(/^data:(\w+\/\w+);base64,(.+)$/);
|
|
267
|
+
if (match) {
|
|
268
|
+
return {
|
|
269
|
+
type: 'image',
|
|
270
|
+
source: {
|
|
271
|
+
type: 'base64',
|
|
272
|
+
media_type: match[1]!, // 例如 'image/png'
|
|
273
|
+
data: match[2]!, // base64 编码内容
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// 步骤2b: 如果是普通 URL,转换为 Anthropic URL 格式
|
|
279
|
+
return {
|
|
280
|
+
type: 'image',
|
|
281
|
+
source: {
|
|
282
|
+
type: 'url',
|
|
283
|
+
media_type: 'image/png', // URL 默认假设为 PNG
|
|
284
|
+
data: urlOrData,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// 降级处理: 未知类型返回空文本块
|
|
289
|
+
return { type: 'text', text: '' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private getTextContent(content: MessageContent): string {
|
|
293
|
+
if (typeof content === 'string') {
|
|
294
|
+
return content;
|
|
295
|
+
}
|
|
296
|
+
return content.filter(isTextPart).map(p => p.text).join('\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
protected override applyRequestOptions(requestBody: Record<string, unknown>, options: LLMRequestOptions): void {
|
|
300
|
+
super.applyRequestOptions(requestBody, options);
|
|
301
|
+
if (options.temperature !== undefined) {
|
|
302
|
+
requestBody.temperature = options.temperature;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (options.maxTokens !== undefined) {
|
|
306
|
+
requestBody.max_tokens = options.maxTokens;
|
|
307
|
+
} else {
|
|
308
|
+
requestBody.max_tokens = 4096;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (options.stop !== undefined) {
|
|
312
|
+
requestBody.stop_sequences = options.stop;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.tools !== undefined) {
|
|
316
|
+
requestBody.tools = options.tools;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async listModels(): Promise<ModelInfo[]> {
|
|
321
|
+
return [
|
|
322
|
+
{ provider: 'anthropic', model: 'claude-3-5-sonnet-20241022', displayName: 'Claude 3.5 Sonnet', contextWindow: 200000, supportsTools: true, supportsVision: true },
|
|
323
|
+
{ provider: 'anthropic', model: 'claude-3-opus-20240229', displayName: 'Claude 3 Opus', contextWindow: 200000, supportsTools: true, supportsVision: true },
|
|
324
|
+
{ provider: 'anthropic', model: 'claude-3-haiku-20240307', displayName: 'Claude 3 Haiku', contextWindow: 200000, supportsTools: true, supportsVision: true },
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async validateKey(): Promise<boolean> {
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch(this.apiUrl, {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: {
|
|
333
|
+
'Content-Type': 'application/json',
|
|
334
|
+
'x-api-key': this.authToken,
|
|
335
|
+
'anthropic-version': '2023-06-01',
|
|
336
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
337
|
+
},
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
model: this.defaultModel,
|
|
340
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
341
|
+
max_tokens: 1,
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
return response.ok || response.status === 400;
|
|
345
|
+
} catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function mapFinishReason(reason: string | null): LLMResponse['finishReason'] {
|
|
352
|
+
switch (reason) {
|
|
353
|
+
case 'end_turn': return 'stop';
|
|
354
|
+
case 'max_tokens': return 'length';
|
|
355
|
+
case 'stop_sequence': return 'stop';
|
|
356
|
+
case 'tool_use': return 'tool_calls';
|
|
357
|
+
default: return 'stop';
|
|
358
|
+
}
|
|
359
|
+
}
|