provider-kit 0.1.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/README.md +126 -0
- package/package.json +19 -0
- package/src/core/persistent-config.js +107 -0
- package/src/index.js +16 -0
- package/src/providers/anthropic-adapter.js +368 -0
- package/src/providers/azure-adapter.js +323 -0
- package/src/providers/bedrock-adapter.js +388 -0
- package/src/providers/cohere-adapter.js +319 -0
- package/src/providers/gemini-adapter.js +282 -0
- package/src/providers/local-provider.js +185 -0
- package/src/providers/openai-compatible.js +840 -0
- package/src/providers/provider-error-adapter.js +100 -0
- package/src/providers/provider-manager.js +321 -0
- package/src/providers/provider-registry.js +316 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { ProviderError } from './provider-error-adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Azure OpenAI API 适配器
|
|
4
|
+
*
|
|
5
|
+
* Azure OpenAI 与 OpenAI API 兼容,但有以下差异:
|
|
6
|
+
* - Endpoint: /{deployment-id}/chat/completions?api-version={version}
|
|
7
|
+
* - Header: api-key (不是 Authorization: Bearer)
|
|
8
|
+
* - 需要指定 deployment ID 而不是 model
|
|
9
|
+
*
|
|
10
|
+
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class AzureOpenAIAdapter {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.id = config.id || 'azure';
|
|
16
|
+
this.name = config.name || 'Azure OpenAI';
|
|
17
|
+
this.nameCn = config.nameCn || 'Azure OpenAI';
|
|
18
|
+
this.resourceName = config.resourceName || null; // your-resource-name
|
|
19
|
+
this.deploymentId = config.deploymentId || null; // your-deployment-id
|
|
20
|
+
this.apiVersion = config.apiVersion || '2024-02-15-preview';
|
|
21
|
+
this.baseUrl = config.baseUrl || `https://${this.resourceName}.openai.azure.com/openai/deployments`;
|
|
22
|
+
this.apiKey = config.apiKey || null;
|
|
23
|
+
this.defaultModel = config.defaultModel || this.deploymentId;
|
|
24
|
+
this.models = config.models || [];
|
|
25
|
+
this.connected = false;
|
|
26
|
+
this.description = config.description || 'Azure 托管的 OpenAI 服务';
|
|
27
|
+
this.timeout = config.timeout || 60000;
|
|
28
|
+
this.headers = config.headers || {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 连接/验证
|
|
33
|
+
*/
|
|
34
|
+
async connect(apiKey) {
|
|
35
|
+
if (apiKey) this.apiKey = apiKey;
|
|
36
|
+
|
|
37
|
+
if (!this.apiKey) {
|
|
38
|
+
throw new ProviderError('API Key required for Azure OpenAI');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!this.resourceName) {
|
|
42
|
+
throw new ProviderError('Resource name required for Azure OpenAI');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.connected = true;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 断开连接
|
|
51
|
+
*/
|
|
52
|
+
disconnect() {
|
|
53
|
+
this.connected = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 构建 URL
|
|
58
|
+
*/
|
|
59
|
+
buildUrl(deploymentId, endpoint = 'chat/completions') {
|
|
60
|
+
return `${this.baseUrl}/${deploymentId}/${endpoint}?api-version=${this.apiVersion}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 发送聊天消息
|
|
65
|
+
*/
|
|
66
|
+
async chat(model, messages, options = {}) {
|
|
67
|
+
if (!this.connected) {
|
|
68
|
+
throw new ProviderError('Azure OpenAI provider not connected');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Azure 使用 deployment ID 而不是 model
|
|
72
|
+
const deploymentId = model || this.deploymentId || this.defaultModel;
|
|
73
|
+
const url = this.buildUrl(deploymentId);
|
|
74
|
+
|
|
75
|
+
const body = {
|
|
76
|
+
messages,
|
|
77
|
+
stream: options.stream || false,
|
|
78
|
+
temperature: options.temperature,
|
|
79
|
+
top_p: options.top_p,
|
|
80
|
+
max_tokens: options.max_tokens,
|
|
81
|
+
...options.extra
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Function Calling
|
|
85
|
+
if (options.tools && options.tools.length > 0) {
|
|
86
|
+
body.tools = options.tools;
|
|
87
|
+
if (options.tool_choice) {
|
|
88
|
+
body.tool_choice = options.tool_choice;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const headers = {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
'api-key': this.apiKey,
|
|
95
|
+
...this.headers
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const response = await fetch(url, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers,
|
|
101
|
+
body: JSON.stringify(body),
|
|
102
|
+
signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const error = await response.json().catch(() => ({}));
|
|
107
|
+
throw new ProviderError(
|
|
108
|
+
error.error?.message ||
|
|
109
|
+
`Azure OpenAI API error: ${response.status} ${response.statusText}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
const choice = data.choices?.[0];
|
|
115
|
+
|
|
116
|
+
const result = {
|
|
117
|
+
content: choice?.message?.content || '',
|
|
118
|
+
model: data.model || deploymentId,
|
|
119
|
+
usage: data.usage,
|
|
120
|
+
raw: data
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Tool calls
|
|
124
|
+
if (choice?.message?.tool_calls && choice.message.tool_calls.length > 0) {
|
|
125
|
+
result.toolCalls = choice.message.tool_calls.map(tc => ({
|
|
126
|
+
id: tc.id,
|
|
127
|
+
name: tc.function.name,
|
|
128
|
+
arguments: tc.function.arguments
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 流式聊天
|
|
137
|
+
*/
|
|
138
|
+
async *chatStream(model, messages, options = {}) {
|
|
139
|
+
if (!this.connected) {
|
|
140
|
+
throw new ProviderError('Azure OpenAI provider not connected');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const deploymentId = model || this.deploymentId || this.defaultModel;
|
|
144
|
+
const url = this.buildUrl(deploymentId);
|
|
145
|
+
|
|
146
|
+
const body = {
|
|
147
|
+
messages,
|
|
148
|
+
stream: true,
|
|
149
|
+
temperature: options.temperature,
|
|
150
|
+
top_p: options.top_p,
|
|
151
|
+
max_tokens: options.max_tokens,
|
|
152
|
+
...options.extra
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (options.tools && options.tools.length > 0) {
|
|
156
|
+
body.tools = options.tools;
|
|
157
|
+
if (options.tool_choice) {
|
|
158
|
+
body.tool_choice = options.tool_choice;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const headers = {
|
|
163
|
+
'Content-Type': 'application/json',
|
|
164
|
+
'api-key': this.apiKey,
|
|
165
|
+
...this.headers
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers,
|
|
171
|
+
body: JSON.stringify(body),
|
|
172
|
+
signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
const error = await response.json().catch(() => ({}));
|
|
177
|
+
throw new ProviderError(
|
|
178
|
+
error.error?.message ||
|
|
179
|
+
`Azure OpenAI API error: ${response.status} ${response.statusText}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const reader = response.body.getReader();
|
|
184
|
+
const decoder = new TextDecoder();
|
|
185
|
+
let buffer = '';
|
|
186
|
+
|
|
187
|
+
const toolCallChunks = new Map();
|
|
188
|
+
|
|
189
|
+
while (true) {
|
|
190
|
+
const { done, value } = await reader.read();
|
|
191
|
+
if (done) break;
|
|
192
|
+
|
|
193
|
+
buffer += decoder.decode(value, { stream: true });
|
|
194
|
+
const lines = buffer.split('\n');
|
|
195
|
+
buffer = lines.pop() || '';
|
|
196
|
+
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
if (line.startsWith('data: ')) {
|
|
199
|
+
const data = line.slice(6);
|
|
200
|
+
if (data === '[DONE]') {
|
|
201
|
+
if (toolCallChunks.size > 0) {
|
|
202
|
+
const toolCalls = Array.from(toolCallChunks.values()).map(tc => ({
|
|
203
|
+
id: tc.id,
|
|
204
|
+
name: tc.name,
|
|
205
|
+
arguments: tc.arguments
|
|
206
|
+
}));
|
|
207
|
+
yield { type: 'tool_calls', toolCalls, done: false };
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const json = JSON.parse(data);
|
|
214
|
+
const delta = json.choices?.[0]?.delta;
|
|
215
|
+
|
|
216
|
+
if (delta?.content) {
|
|
217
|
+
yield { type: 'content', content: delta.content, done: false };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (delta?.tool_calls) {
|
|
221
|
+
for (const tc of delta.tool_calls) {
|
|
222
|
+
const idx = tc.index || 0;
|
|
223
|
+
if (!toolCallChunks.has(idx)) {
|
|
224
|
+
toolCallChunks.set(idx, { id: '', name: '', arguments: '' });
|
|
225
|
+
}
|
|
226
|
+
const chunk = toolCallChunks.get(idx);
|
|
227
|
+
if (tc.id) chunk.id = tc.id;
|
|
228
|
+
if (tc.function?.name) chunk.name = tc.function.name;
|
|
229
|
+
if (tc.function?.arguments) chunk.arguments += tc.function.arguments;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
// 忽略解析错误
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
yield { done: true };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 获取模型列表(Azure 中是 deployments)
|
|
244
|
+
*/
|
|
245
|
+
async fetchModels() {
|
|
246
|
+
// Azure 需要额外的 API 调用来列出 deployments
|
|
247
|
+
// 这里返回配置的模型列表
|
|
248
|
+
return this.models;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 获取模型列表(本地)
|
|
253
|
+
*/
|
|
254
|
+
getModels() {
|
|
255
|
+
return this.models;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Embedding API
|
|
260
|
+
*/
|
|
261
|
+
async embeddings(input, model = 'text-embedding-ada-002') {
|
|
262
|
+
if (!this.connected) {
|
|
263
|
+
throw new ProviderError('Azure OpenAI provider not connected');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const deploymentId = model;
|
|
267
|
+
const url = this.buildUrl(deploymentId, 'embeddings');
|
|
268
|
+
|
|
269
|
+
const body = {
|
|
270
|
+
input: Array.isArray(input) ? input : [input]
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const headers = {
|
|
274
|
+
'Content-Type': 'application/json',
|
|
275
|
+
'api-key': this.apiKey,
|
|
276
|
+
...this.headers
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const response = await fetch(url, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers,
|
|
282
|
+
body: JSON.stringify(body)
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
const error = await response.json().catch(() => ({}));
|
|
287
|
+
throw new ProviderError(error.error?.message || `Embedding API error: ${response.status}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const data = await response.json();
|
|
291
|
+
return data.data.map(d => d.embedding);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 获取状态
|
|
296
|
+
*/
|
|
297
|
+
getStatus() {
|
|
298
|
+
return {
|
|
299
|
+
id: this.id,
|
|
300
|
+
name: this.name,
|
|
301
|
+
nameCn: this.nameCn,
|
|
302
|
+
baseUrl: this.baseUrl,
|
|
303
|
+
resourceName: this.resourceName,
|
|
304
|
+
deploymentId: this.deploymentId,
|
|
305
|
+
connected: this.connected,
|
|
306
|
+
modelCount: this.models.length,
|
|
307
|
+
defaultModel: this.defaultModel,
|
|
308
|
+
hasApiKey: !!this.apiKey,
|
|
309
|
+
transport: 'azure_openai'
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function createAzureOpenAIProvider(config) {
|
|
315
|
+
return new AzureOpenAIAdapter({
|
|
316
|
+
id: 'azure',
|
|
317
|
+
name: 'Azure OpenAI',
|
|
318
|
+
nameCn: 'Azure OpenAI',
|
|
319
|
+
...config
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default AzureOpenAIAdapter;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { ProviderError } from './provider-error-adapter.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// 加载用户配置
|
|
9
|
+
function loadModelConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const configPath = path.join(__dirname, '../config/model-selection.json');
|
|
12
|
+
if (fs.existsSync(configPath)) {
|
|
13
|
+
const data = fs.readFileSync(configPath, 'utf8');
|
|
14
|
+
const config = JSON.parse(data);
|
|
15
|
+
return config.modelSelection?.bedrock || {};
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.warn('[BedrockAdapter] Failed to load model config:', e.message);
|
|
19
|
+
}
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const modelConfig = loadModelConfig();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* AWS Bedrock API 适配器
|
|
27
|
+
*
|
|
28
|
+
* AWS Bedrock 使用 AWS Signature V4 认证:
|
|
29
|
+
* - Endpoint: /model/{model-id}/invoke
|
|
30
|
+
* - Header: AWS Signature V4
|
|
31
|
+
* - 支持多种模型格式(Claude, Llama, Titan 等)
|
|
32
|
+
*
|
|
33
|
+
* 注意:此适配器需要 AWS SDK 或手动实现 SigV4 签名
|
|
34
|
+
* 参考文档: https://docs.aws.amazon.com/bedrock/latest/APIReference
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export class BedrockAdapter {
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.id = config.id || 'bedrock';
|
|
40
|
+
this.name = config.name || 'Bedrock';
|
|
41
|
+
this.nameCn = config.nameCn || 'AWS Bedrock';
|
|
42
|
+
this.region = config.region || 'us-east-1';
|
|
43
|
+
this.baseUrl = config.baseUrl || `https://bedrock-runtime.${this.region}.amazonaws.com`;
|
|
44
|
+
this.accessKeyId = config.accessKeyId || null;
|
|
45
|
+
this.secretAccessKey = config.secretAccessKey || null;
|
|
46
|
+
|
|
47
|
+
// 完全从配置读取模型
|
|
48
|
+
this.defaultModel = config.defaultModel || modelConfig.defaultModel || null;
|
|
49
|
+
|
|
50
|
+
// 可用模型列表完全来自配置
|
|
51
|
+
this.models = config.models || modelConfig.availableModels || [];
|
|
52
|
+
this.connected = false;
|
|
53
|
+
this.description = config.description || 'AWS Bedrock 多模型服务';
|
|
54
|
+
this.timeout = config.timeout || 60000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 连接/验证
|
|
59
|
+
*/
|
|
60
|
+
async connect(credentials) {
|
|
61
|
+
if (credentials) {
|
|
62
|
+
this.accessKeyId = credentials.accessKeyId;
|
|
63
|
+
this.secretAccessKey = credentials.secretAccessKey;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!this.accessKeyId || !this.secretAccessKey) {
|
|
67
|
+
throw new ProviderError('AWS credentials required for Bedrock');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.connected = true;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 断开连接
|
|
76
|
+
*/
|
|
77
|
+
disconnect() {
|
|
78
|
+
this.connected = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 转换消息格式: OpenAI -> Bedrock (根据模型类型)
|
|
83
|
+
*/
|
|
84
|
+
convertMessages(messages, modelId) {
|
|
85
|
+
// Claude 模型使用 Anthropic 格式
|
|
86
|
+
if (modelId.startsWith('anthropic.')) {
|
|
87
|
+
return this.convertToAnthropicFormat(messages);
|
|
88
|
+
}
|
|
89
|
+
// Llama 模型使用标准格式
|
|
90
|
+
else if (modelId.startsWith('meta.llama')) {
|
|
91
|
+
return this.convertToLlamaFormat(messages);
|
|
92
|
+
}
|
|
93
|
+
// Titan 模型使用 Amazon 格式
|
|
94
|
+
else if (modelId.startsWith('amazon.titan')) {
|
|
95
|
+
return this.convertToTitanFormat(messages);
|
|
96
|
+
}
|
|
97
|
+
// 默认使用通用格式
|
|
98
|
+
else {
|
|
99
|
+
return { prompt: messages.map(m => `${m.role}: ${m.content}`).join('\n') };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 转换为 Anthropic 格式(Bedrock 上的 Claude)
|
|
105
|
+
*/
|
|
106
|
+
convertToAnthropicFormat(messages) {
|
|
107
|
+
const systemMessages = [];
|
|
108
|
+
const anthropicMessages = [];
|
|
109
|
+
|
|
110
|
+
for (const msg of messages) {
|
|
111
|
+
if (msg.role === 'system') {
|
|
112
|
+
systemMessages.push(msg.content);
|
|
113
|
+
} else {
|
|
114
|
+
anthropicMessages.push({
|
|
115
|
+
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
116
|
+
content: msg.content
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
system: systemMessages.join('\n\n'),
|
|
123
|
+
messages: anthropicMessages,
|
|
124
|
+
anthropic_version: 'bedrock-2023-05-31'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 转换为 Llama 格式
|
|
130
|
+
*/
|
|
131
|
+
convertToLlamaFormat(messages) {
|
|
132
|
+
return {
|
|
133
|
+
prompt: messages.map(m => {
|
|
134
|
+
if (m.role === 'system') return `<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n${m.content}<|eot_id|>`;
|
|
135
|
+
if (m.role === 'user') return `<|start_header_id|>user<|end_header_id|>\n\n${m.content}<|eot_id|>`;
|
|
136
|
+
if (m.role === 'assistant') return `<|start_header_id|>assistant<|end_header_id|>\n\n${m.content}<|eot_id|>`;
|
|
137
|
+
return '';
|
|
138
|
+
}).join('') + '<|start_header_id|>assistant<|end_header_id|>\n\n'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 转换为 Titan 格式
|
|
144
|
+
*/
|
|
145
|
+
convertToTitanFormat(messages) {
|
|
146
|
+
const prompt = messages.map(m => `${m.role}: ${m.content}`).join('\n');
|
|
147
|
+
return {
|
|
148
|
+
inputText: prompt,
|
|
149
|
+
textGenerationConfig: {
|
|
150
|
+
maxTokenCount: 4096,
|
|
151
|
+
temperature: 0.7,
|
|
152
|
+
topP: 0.9
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 简化版:使用 API Key 方式(需要通过 IAM 生成临时凭证或使用代理)
|
|
159
|
+
* 完整实现需要 AWS SDK 的 SigV4 签名
|
|
160
|
+
*/
|
|
161
|
+
async chat(model, messages, options = {}) {
|
|
162
|
+
if (!this.connected) {
|
|
163
|
+
throw new ProviderError('Bedrock provider not connected');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 注意:实际使用需要实现 AWS Signature V4
|
|
167
|
+
console.warn('⚠️ Bedrock adapter requires AWS Signature V4. Consider using AWS SDK.');
|
|
168
|
+
|
|
169
|
+
const modelId = model || this.defaultModel;
|
|
170
|
+
const url = `${this.baseUrl}/model/${modelId}/invoke`;
|
|
171
|
+
|
|
172
|
+
const body = {
|
|
173
|
+
...this.convertMessages(messages, modelId),
|
|
174
|
+
max_tokens: options.max_tokens || 4096,
|
|
175
|
+
temperature: options.temperature || 0.7
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// 这里需要实现 AWS SigV4 签名
|
|
179
|
+
// 建议使用 AWS SDK 或第三方库
|
|
180
|
+
throw new ProviderError('Bedrock requires AWS SDK for signing. Please use AWS SDK integration.');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 流式聊天
|
|
185
|
+
*/
|
|
186
|
+
async *chatStream(model, messages, options = {}) {
|
|
187
|
+
throw new ProviderError('Bedrock streaming requires AWS SDK. Please use AWS SDK integration.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 获取模型列表
|
|
192
|
+
*/
|
|
193
|
+
async fetchModels() {
|
|
194
|
+
return this.models;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 获取模型列表(本地)
|
|
199
|
+
*/
|
|
200
|
+
getModels() {
|
|
201
|
+
return this.models;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 获取状态
|
|
206
|
+
*/
|
|
207
|
+
getStatus() {
|
|
208
|
+
return {
|
|
209
|
+
id: this.id,
|
|
210
|
+
name: this.name,
|
|
211
|
+
nameCn: this.nameCn,
|
|
212
|
+
baseUrl: this.baseUrl,
|
|
213
|
+
region: this.region,
|
|
214
|
+
connected: this.connected,
|
|
215
|
+
modelCount: this.models.length,
|
|
216
|
+
defaultModel: this.defaultModel,
|
|
217
|
+
hasApiKey: !!(this.accessKeyId && this.secretAccessKey),
|
|
218
|
+
transport: 'bedrock_invoke',
|
|
219
|
+
note: 'Requires AWS SDK for production use'
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Bedrock 代理适配器(通过代理服务使用)
|
|
226
|
+
*
|
|
227
|
+
* 如果你有一个代理服务将 Bedrock 转换为 OpenAI 格式,
|
|
228
|
+
* 可以使用 OpenAICompatibleProvider
|
|
229
|
+
*/
|
|
230
|
+
export class BedrockProxyAdapter {
|
|
231
|
+
constructor(config) {
|
|
232
|
+
this.id = config.id || 'bedrock-proxy';
|
|
233
|
+
this.name = config.name || 'Bedrock Proxy';
|
|
234
|
+
this.nameCn = config.nameCn || 'AWS Bedrock 代理';
|
|
235
|
+
this.baseUrl = config.baseUrl || 'http://localhost:8000/v1';
|
|
236
|
+
this.apiKey = config.apiKey || null;
|
|
237
|
+
this.defaultModel = config.defaultModel || 'anthropic.claude-3-5-sonnet-20241022-v2:0';
|
|
238
|
+
this.models = config.models || [];
|
|
239
|
+
this.connected = false;
|
|
240
|
+
this.skipAuth = config.skipAuth || false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async connect(apiKey) {
|
|
244
|
+
if (apiKey) this.apiKey = apiKey;
|
|
245
|
+
this.connected = true;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
disconnect() {
|
|
250
|
+
this.connected = false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async chat(model, messages, options = {}) {
|
|
254
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
255
|
+
|
|
256
|
+
const headers = {
|
|
257
|
+
'Content-Type': 'application/json'
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (this.apiKey && !this.skipAuth) {
|
|
261
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const response = await fetch(url, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers,
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
model: model || this.defaultModel,
|
|
269
|
+
messages,
|
|
270
|
+
...options
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
throw new ProviderError(`Bedrock Proxy error: ${response.status}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const data = await response.json();
|
|
279
|
+
return {
|
|
280
|
+
content: data.choices?.[0]?.message?.content || '',
|
|
281
|
+
model: data.model,
|
|
282
|
+
usage: data.usage,
|
|
283
|
+
raw: data
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async *chatStream(model, messages, options = {}) {
|
|
288
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
289
|
+
|
|
290
|
+
const headers = {
|
|
291
|
+
'Content-Type': 'application/json'
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (this.apiKey && !this.skipAuth) {
|
|
295
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const response = await fetch(url, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers,
|
|
301
|
+
body: JSON.stringify({
|
|
302
|
+
model: model || this.defaultModel,
|
|
303
|
+
messages,
|
|
304
|
+
stream: true,
|
|
305
|
+
...options
|
|
306
|
+
})
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
throw new ProviderError(`Bedrock Proxy error: ${response.status}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const reader = response.body.getReader();
|
|
314
|
+
const decoder = new TextDecoder();
|
|
315
|
+
let buffer = '';
|
|
316
|
+
|
|
317
|
+
while (true) {
|
|
318
|
+
const { done, value } = await reader.read();
|
|
319
|
+
if (done) break;
|
|
320
|
+
|
|
321
|
+
buffer += decoder.decode(value, { stream: true });
|
|
322
|
+
const lines = buffer.split('\n');
|
|
323
|
+
buffer = lines.pop() || '';
|
|
324
|
+
|
|
325
|
+
for (const line of lines) {
|
|
326
|
+
if (line.startsWith('data: ')) {
|
|
327
|
+
const data = line.slice(6);
|
|
328
|
+
if (data === '[DONE]') {
|
|
329
|
+
yield { done: true };
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const json = JSON.parse(data);
|
|
335
|
+
const content = json.choices?.[0]?.delta?.content;
|
|
336
|
+
if (content) {
|
|
337
|
+
yield { type: 'content', content, done: false };
|
|
338
|
+
}
|
|
339
|
+
} catch (e) {
|
|
340
|
+
// 忽略解析错误
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
yield { done: true };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
getModels() {
|
|
350
|
+
return this.models;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getStatus() {
|
|
354
|
+
return {
|
|
355
|
+
id: this.id,
|
|
356
|
+
name: this.name,
|
|
357
|
+
nameCn: this.nameCn,
|
|
358
|
+
baseUrl: this.baseUrl,
|
|
359
|
+
connected: this.connected,
|
|
360
|
+
modelCount: this.models.length,
|
|
361
|
+
defaultModel: this.defaultModel,
|
|
362
|
+
hasApiKey: !!this.apiKey,
|
|
363
|
+
transport: 'bedrock_proxy'
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function createBedrockProvider(credentials = null, overrides = {}) {
|
|
369
|
+
return new BedrockAdapter({
|
|
370
|
+
id: 'bedrock',
|
|
371
|
+
name: 'Bedrock',
|
|
372
|
+
nameCn: 'AWS Bedrock',
|
|
373
|
+
...credentials,
|
|
374
|
+
...overrides
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function createBedrockProxyProvider(apiKey = null, overrides = {}) {
|
|
379
|
+
return new BedrockProxyAdapter({
|
|
380
|
+
id: 'bedrock-proxy',
|
|
381
|
+
name: 'Bedrock Proxy',
|
|
382
|
+
nameCn: 'AWS Bedrock 代理',
|
|
383
|
+
apiKey,
|
|
384
|
+
...overrides
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export default BedrockAdapter;
|