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.
@@ -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;