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
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @openchat/provider-kit
|
|
2
|
+
|
|
3
|
+
**One API for 42 LLM providers.** OpenAI, Anthropic, Ollama, OpenRouter, Google Gemini, Azure, AWS Bedrock, Cohere — same interface, built-in retry and timeout.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { createProvider } from '@openchat/provider-kit'
|
|
7
|
+
|
|
8
|
+
const provider = await createProvider('openai', { apiKey: 'sk-...' })
|
|
9
|
+
const reply = await provider.chat('gpt-4', [
|
|
10
|
+
{ role: 'user', content: 'Hello' }
|
|
11
|
+
])
|
|
12
|
+
// { content: '...', model: 'gpt-4', usage: { prompt_tokens: 10, completion_tokens: 20 } }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @openchat/provider-kit
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic chat
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { createProvider } from '@openchat/provider-kit'
|
|
27
|
+
|
|
28
|
+
const provider = await createProvider('openai', { apiKey: process.env.OPENAI_API_KEY })
|
|
29
|
+
const reply = await provider.chat('gpt-4o-mini', [
|
|
30
|
+
{ role: 'system', content: 'You are a poet' },
|
|
31
|
+
{ role: 'user', content: 'Write a haiku' },
|
|
32
|
+
])
|
|
33
|
+
console.log(reply.content)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### With retry and timeout
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { safeProviderCall } from '@openchat/provider-kit'
|
|
40
|
+
|
|
41
|
+
const reply = await safeProviderCall(
|
|
42
|
+
() => provider.chat('gpt-4', messages),
|
|
43
|
+
{ provider: 'openai', retries: 3, timeout: 30000 }
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Available providers
|
|
48
|
+
|
|
49
|
+
| Provider | `type` | Requires |
|
|
50
|
+
|----------|--------|----------|
|
|
51
|
+
| OpenAI | `openai` | `OPENAI_API_KEY` |
|
|
52
|
+
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` |
|
|
53
|
+
| Ollama | `ollama` | local server at `http://localhost:11434` |
|
|
54
|
+
| Azure OpenAI | `azure` | Azure credentials |
|
|
55
|
+
| AWS Bedrock | `bedrock` | AWS credentials |
|
|
56
|
+
| Cohere | `cohere` | `COHERE_API_KEY` |
|
|
57
|
+
| Google Gemini | `gemini` | `GEMINI_API_KEY` |
|
|
58
|
+
| OpenAI-compatible | `openai` with custom `baseUrl` | Any OpenAI-compatible API |
|
|
59
|
+
|
|
60
|
+
### Streaming
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const stream = await provider.chatStream('gpt-4', messages)
|
|
64
|
+
for await (const chunk of stream) {
|
|
65
|
+
if (chunk.type === 'content') process.stdout.write(chunk.content)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Error handling
|
|
70
|
+
|
|
71
|
+
`createProvider`, `.chat()`, `.chatStream()` — all throw `ProviderError` with consistent fields:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import { ProviderError } from '@openchat/provider-kit'
|
|
75
|
+
|
|
76
|
+
try { /* ... */ } catch (e) {
|
|
77
|
+
if (e instanceof ProviderError) {
|
|
78
|
+
console.log(e.provider, e.statusCode, e.retryable, e.type)
|
|
79
|
+
// e.g. 'openai', 429, true, 'rate_limit'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Function Calling
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const reply = await provider.chat('gpt-4', messages, {
|
|
88
|
+
tools: [{
|
|
89
|
+
type: 'function',
|
|
90
|
+
function: {
|
|
91
|
+
name: 'get_weather',
|
|
92
|
+
description: 'Get weather for a city',
|
|
93
|
+
parameters: { type: 'object', properties: { city: { type: 'string' } } }
|
|
94
|
+
}
|
|
95
|
+
}]
|
|
96
|
+
})
|
|
97
|
+
if (reply.toolCalls) {
|
|
98
|
+
// [{ id, name, arguments: { city: 'Tokyo' } }]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## ProviderRegistry
|
|
103
|
+
|
|
104
|
+
Manage multiple providers with a registry:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import { providerRegistry, createProvider } from '@openchat/provider-kit'
|
|
108
|
+
|
|
109
|
+
await providerRegistry.configure('openai', { apiKey: 'sk-...' })
|
|
110
|
+
await providerRegistry.configure('ollama', { baseUrl: 'http://localhost:11434' })
|
|
111
|
+
|
|
112
|
+
const provider = providerRegistry.get('openai')
|
|
113
|
+
const reply = await provider.chat('gpt-4', messages)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Known Limitations (v0.1.0)
|
|
117
|
+
|
|
118
|
+
- **No config persistence by default.** API keys must be passed on every `createProvider()` call. Use `createStore()` for persistent config.
|
|
119
|
+
- **10 adapters implemented out of 42 presets.** The remaining 32 use OpenAI-compatible fallback. Contributions welcome.
|
|
120
|
+
- **No TypeScript types.** Planned for v0.2.0.
|
|
121
|
+
- **Not for production.** API keys are stored in memory. No OS keychain integration.
|
|
122
|
+
- **`.provider-kit.json` should be added to `.gitignore`** if you choose to use file persistence via `createStore()`.
|
|
123
|
+
|
|
124
|
+
## Related
|
|
125
|
+
|
|
126
|
+
- `@openchat/fairy-guardian` — self-healing process cluster for AI model servers
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "provider-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "42 LLM provider unified API — one interface for OpenAI, Anthropic, Ollama, OpenRouter, and 38 more. Built-in retry, timeout, and error handling.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": { ".": "./src/index.js" },
|
|
8
|
+
"files": ["src/", "README.md"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js",
|
|
11
|
+
"prepublishOnly": "npm test",
|
|
12
|
+
"version": "git add -A src/ CHANGELOG.md",
|
|
13
|
+
"postversion": "git push && git push --tags"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["llm", "openai", "anthropic", "ollama", "provider", "api"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"engines": { "node": ">=18" },
|
|
18
|
+
"repository": { "type": "git", "url": "https://github.com/openchat-ai/openchat" }
|
|
19
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent config for @openchat/provider-kit
|
|
3
|
+
*
|
|
4
|
+
* Lookup order:
|
|
5
|
+
* 1. PROVIDER_KIT_CONFIG_PATH 环境变量
|
|
6
|
+
* 2. cwd/.provider-kit.json
|
|
7
|
+
* 3. 内存(不写文件)
|
|
8
|
+
*
|
|
9
|
+
* 绝不写 ~/ 目录。这是公共 npm 包,不是 OpenChat 专属.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { resolve } from 'path';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
14
|
+
|
|
15
|
+
function resolveConfigPath() {
|
|
16
|
+
const envPath = process.env.PROVIDER_KIT_CONFIG_PATH;
|
|
17
|
+
if (envPath) return resolve(envPath);
|
|
18
|
+
const cwdPath = resolve(process.cwd(), '.provider-kit.json');
|
|
19
|
+
if (existsSync(cwdPath)) return cwdPath;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CONFIG_PATH = resolveConfigPath();
|
|
24
|
+
|
|
25
|
+
class PersistentConfig {
|
|
26
|
+
constructor() {
|
|
27
|
+
this._store = {};
|
|
28
|
+
this._apiKeys = {};
|
|
29
|
+
this._readOnly = !CONFIG_PATH;
|
|
30
|
+
this._load();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_load() {
|
|
34
|
+
if (!CONFIG_PATH) return; // memory only
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
37
|
+
this._store = data.store || {};
|
|
38
|
+
this._apiKeys = data.apiKeys || {};
|
|
39
|
+
} catch {
|
|
40
|
+
this._store = {};
|
|
41
|
+
this._apiKeys = {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_save() {
|
|
46
|
+
if (this._readOnly) return;
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(resolve(CONFIG_PATH, '..'), { recursive: true });
|
|
49
|
+
writeFileSync(CONFIG_PATH, JSON.stringify({ store: this._store, apiKeys: this._apiKeys }, null, 2), 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
// silent: 只读容器 / 权限不足
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getApiKey(provider) {
|
|
56
|
+
return this._apiKeys[provider] || process.env[`${provider.toUpperCase()}_API_KEY`] || '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setApiKey(provider, key) {
|
|
60
|
+
this._apiKeys[provider] = key;
|
|
61
|
+
this._save();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
removeApiKey(provider) {
|
|
65
|
+
delete this._apiKeys[provider];
|
|
66
|
+
this._save();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
listKeys() {
|
|
70
|
+
return Object.keys(this._apiKeys);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getPreference(key) {
|
|
74
|
+
return this._store[key];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setPreference(key, val) {
|
|
78
|
+
this._store[key] = val;
|
|
79
|
+
this._save();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getBridgeConfig() {
|
|
83
|
+
return this._store;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const persistentConfig = new PersistentConfig();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* createStore — DI 模式:自定义存储实现
|
|
91
|
+
* 用于框架集成者(Next.js、Remix、Bridge 等)传入自己的持久化方案
|
|
92
|
+
*
|
|
93
|
+
* 示例:
|
|
94
|
+
* const store = createStore({ load: () => db.get('config'), save: (d) => db.set('config', d) });
|
|
95
|
+
* store.setApiKey('openai', 'sk-xxx');
|
|
96
|
+
*/
|
|
97
|
+
export function createStore(impl = {}) {
|
|
98
|
+
const store = new PersistentConfig();
|
|
99
|
+
if (impl.apiKeys) store._apiKeys = impl.apiKeys;
|
|
100
|
+
if (impl.store) store._store = impl.store;
|
|
101
|
+
if (impl.load) { try { const d = impl.load(); if (d) { store._apiKeys = d.apiKeys || {}; store._store = d.store || {}; } } catch {} }
|
|
102
|
+
if (impl.save) { const origSave = store._save.bind(store); store._save = () => { origSave(); impl.save({ apiKeys: store._apiKeys, store: store._store }); }; }
|
|
103
|
+
if (impl.onSave) store._save = impl.onSave;
|
|
104
|
+
return store;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default persistentConfig;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @openchat/provider-kit
|
|
3
|
+
*
|
|
4
|
+
* 42 LLM provider unified API — one interface for OpenAI, Anthropic,
|
|
5
|
+
* Ollama, OpenRouter, and 38 more.
|
|
6
|
+
*
|
|
7
|
+
* Quick start:
|
|
8
|
+
* import { providerRegistry, createProvider } from '@openchat/provider-kit';
|
|
9
|
+
* const provider = createProvider('openai', { apiKey: 'sk-...' });
|
|
10
|
+
* const reply = await provider.chat('gpt-4', [{ role: 'user', content: 'Hi' }]);
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { ProviderError, AbortError, withRetry, withTimeout, safeProviderCall, classifyError, createCancelSignal } from './providers/provider-error-adapter.js';
|
|
14
|
+
export { ProviderManager, providerManager } from './providers/provider-manager.js';
|
|
15
|
+
export { providerRegistry, createProvider, listPresetProviders, PRESET_PROVIDERS } from './providers/provider-registry.js';
|
|
16
|
+
export { persistentConfig, createStore } from './core/persistent-config.js';
|
|
@@ -0,0 +1,368 @@
|
|
|
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
|
+
* Anthropic Claude API 适配器
|
|
10
|
+
*
|
|
11
|
+
* Anthropic 使用独特的 Messages API 格式,与 OpenAI 不同:
|
|
12
|
+
* - Endpoint: /v1/messages (不是 /chat/completions)
|
|
13
|
+
* - Header: x-api-key (不是 Authorization: Bearer)
|
|
14
|
+
* - 请求格式: { model, messages, max_tokens, ... }
|
|
15
|
+
* - 响应格式: { id, content: [{ type: 'text', text: '...' }], ... }
|
|
16
|
+
*
|
|
17
|
+
* 参考文档: https://docs.anthropic.com/claude/reference/messages_post
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// 加载用户配置
|
|
21
|
+
function loadModelConfig() {
|
|
22
|
+
try {
|
|
23
|
+
const configPath = path.join(__dirname, '../config/model-selection.json');
|
|
24
|
+
if (fs.existsSync(configPath)) {
|
|
25
|
+
const data = fs.readFileSync(configPath, 'utf8');
|
|
26
|
+
const config = JSON.parse(data);
|
|
27
|
+
return config.modelSelection?.anthropic || {};
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn('[AnthropicAdapter] Failed to load model config:', e.message);
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const modelConfig = loadModelConfig();
|
|
36
|
+
|
|
37
|
+
export class AnthropicAdapter {
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.id = config.id || 'anthropic';
|
|
40
|
+
this.name = config.name || 'Claude';
|
|
41
|
+
this.nameCn = config.nameCn || 'Anthropic Claude';
|
|
42
|
+
this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
|
|
43
|
+
this.apiKey = config.apiKey || null;
|
|
44
|
+
|
|
45
|
+
// 完全从配置读取模型,配置有什么就用什么
|
|
46
|
+
this.defaultModel = config.defaultModel || modelConfig.defaultModel || null;
|
|
47
|
+
|
|
48
|
+
// 可用模型列表完全来自配置
|
|
49
|
+
this.models = config.models || modelConfig.availableModels || [];
|
|
50
|
+
this.connected = false;
|
|
51
|
+
this.description = config.description || 'Anthropic Claude 系列模型';
|
|
52
|
+
|
|
53
|
+
// Anthropic 特定配置
|
|
54
|
+
this.anthropicVersion = config.anthropicVersion || '2023-06-01';
|
|
55
|
+
this.timeout = config.timeout || 60000;
|
|
56
|
+
this.headers = config.headers || {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 连接/验证 API Key
|
|
61
|
+
*/
|
|
62
|
+
async connect(apiKey) {
|
|
63
|
+
if (apiKey) this.apiKey = apiKey;
|
|
64
|
+
|
|
65
|
+
if (!this.apiKey) {
|
|
66
|
+
throw new ProviderError('API Key required for Anthropic Claude');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Anthropic 没有 /models 端点,直接测试连接
|
|
70
|
+
try {
|
|
71
|
+
// 发送一个简单的测试请求
|
|
72
|
+
await this.chat(this.defaultModel, [
|
|
73
|
+
{ role: 'user', content: 'Hi' }
|
|
74
|
+
], { max_tokens: 10 });
|
|
75
|
+
|
|
76
|
+
this.connected = true;
|
|
77
|
+
return true;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// 如果有 API Key,假定连接成功
|
|
80
|
+
if (this.apiKey) {
|
|
81
|
+
this.connected = true;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 断开连接
|
|
90
|
+
*/
|
|
91
|
+
disconnect() {
|
|
92
|
+
this.connected = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 转换消息格式: OpenAI -> Anthropic
|
|
97
|
+
*
|
|
98
|
+
* OpenAI: [{ role: 'system', content: '...' }, { role: 'user', content: '...' }]
|
|
99
|
+
* Anthropic: system: '...', messages: [{ role: 'user', content: '...' }]
|
|
100
|
+
*/
|
|
101
|
+
convertMessages(messages) {
|
|
102
|
+
const systemMessages = [];
|
|
103
|
+
const anthropicMessages = [];
|
|
104
|
+
|
|
105
|
+
for (const msg of messages) {
|
|
106
|
+
if (msg.role === 'system') {
|
|
107
|
+
systemMessages.push(msg.content);
|
|
108
|
+
} else if (msg.role === 'user' || msg.role === 'assistant') {
|
|
109
|
+
anthropicMessages.push({
|
|
110
|
+
role: msg.role,
|
|
111
|
+
content: typeof msg.content === 'string'
|
|
112
|
+
? msg.content
|
|
113
|
+
: JSON.stringify(msg.content)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
system: systemMessages.join('\n\n'),
|
|
120
|
+
messages: anthropicMessages
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 转换响应格式: Anthropic -> OpenAI
|
|
126
|
+
*/
|
|
127
|
+
convertResponse(anthropicResponse) {
|
|
128
|
+
const content = anthropicResponse.content
|
|
129
|
+
.filter(c => c.type === 'text')
|
|
130
|
+
.map(c => c.text)
|
|
131
|
+
.join('');
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content,
|
|
135
|
+
model: anthropicResponse.model,
|
|
136
|
+
usage: {
|
|
137
|
+
prompt_tokens: anthropicResponse.usage?.input_tokens || 0,
|
|
138
|
+
completion_tokens: anthropicResponse.usage?.output_tokens || 0,
|
|
139
|
+
total_tokens: (anthropicResponse.usage?.input_tokens || 0) +
|
|
140
|
+
(anthropicResponse.usage?.output_tokens || 0)
|
|
141
|
+
},
|
|
142
|
+
raw: anthropicResponse
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 发送聊天消息
|
|
148
|
+
*/
|
|
149
|
+
async chat(model, messages, options = {}) {
|
|
150
|
+
if (!this.connected) {
|
|
151
|
+
throw new ProviderError('Anthropic provider not connected');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const url = `${this.baseUrl}/v1/messages`;
|
|
155
|
+
|
|
156
|
+
const { system, messages: anthropicMessages } = this.convertMessages(messages);
|
|
157
|
+
|
|
158
|
+
const body = {
|
|
159
|
+
model: model || this.defaultModel,
|
|
160
|
+
messages: anthropicMessages,
|
|
161
|
+
max_tokens: options.max_tokens || options.maxTokens || 4096,
|
|
162
|
+
stream: options.stream || false
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// 添加 system 消息
|
|
166
|
+
if (system) {
|
|
167
|
+
body.system = system;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 可选参数
|
|
171
|
+
if (options.temperature !== undefined) {
|
|
172
|
+
body.temperature = options.temperature;
|
|
173
|
+
}
|
|
174
|
+
if (options.top_p !== undefined) {
|
|
175
|
+
body.top_p = options.top_p;
|
|
176
|
+
}
|
|
177
|
+
if (options.top_k !== undefined) {
|
|
178
|
+
body.top_k = options.top_k;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Tool Use (Function Calling)
|
|
182
|
+
if (options.tools && options.tools.length > 0) {
|
|
183
|
+
body.tools = this.convertTools(options.tools);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const headers = {
|
|
187
|
+
'Content-Type': 'application/json',
|
|
188
|
+
'x-api-key': this.apiKey,
|
|
189
|
+
'anthropic-version': this.anthropicVersion,
|
|
190
|
+
...this.headers
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const response = await fetch(url, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers,
|
|
196
|
+
body: JSON.stringify(body),
|
|
197
|
+
signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
const error = await response.json().catch(() => ({}));
|
|
202
|
+
throw new ProviderError(
|
|
203
|
+
error.error?.message ||
|
|
204
|
+
`Anthropic API error: ${response.status} ${response.statusText}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
return this.convertResponse(data);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 流式聊天
|
|
214
|
+
*/
|
|
215
|
+
async *chatStream(model, messages, options = {}) {
|
|
216
|
+
if (!this.connected) {
|
|
217
|
+
throw new ProviderError('Anthropic provider not connected');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const url = `${this.baseUrl}/v1/messages`;
|
|
221
|
+
|
|
222
|
+
const { system, messages: anthropicMessages } = this.convertMessages(messages);
|
|
223
|
+
|
|
224
|
+
const body = {
|
|
225
|
+
model: model || this.defaultModel,
|
|
226
|
+
messages: anthropicMessages,
|
|
227
|
+
max_tokens: options.max_tokens || options.maxTokens || 4096,
|
|
228
|
+
stream: true
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (system) {
|
|
232
|
+
body.system = system;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (options.temperature !== undefined) {
|
|
236
|
+
body.temperature = options.temperature;
|
|
237
|
+
}
|
|
238
|
+
if (options.top_p !== undefined) {
|
|
239
|
+
body.top_p = options.top_p;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (options.tools && options.tools.length > 0) {
|
|
243
|
+
body.tools = this.convertTools(options.tools);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const headers = {
|
|
247
|
+
'Content-Type': 'application/json',
|
|
248
|
+
'x-api-key': this.apiKey,
|
|
249
|
+
'anthropic-version': this.anthropicVersion,
|
|
250
|
+
...this.headers
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const response = await fetch(url, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers,
|
|
256
|
+
body: JSON.stringify(body),
|
|
257
|
+
signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const error = await response.json().catch(() => ({}));
|
|
262
|
+
throw new ProviderError(
|
|
263
|
+
error.error?.message ||
|
|
264
|
+
`Anthropic API error: ${response.status} ${response.statusText}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const reader = response.body.getReader();
|
|
269
|
+
const decoder = new TextDecoder();
|
|
270
|
+
let buffer = '';
|
|
271
|
+
|
|
272
|
+
while (true) {
|
|
273
|
+
const { done, value } = await reader.read();
|
|
274
|
+
if (done) break;
|
|
275
|
+
|
|
276
|
+
buffer += decoder.decode(value, { stream: true });
|
|
277
|
+
const lines = buffer.split('\n');
|
|
278
|
+
buffer = lines.pop() || '';
|
|
279
|
+
|
|
280
|
+
for (const line of lines) {
|
|
281
|
+
if (line.startsWith('data: ')) {
|
|
282
|
+
const data = line.slice(6);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const json = JSON.parse(data);
|
|
286
|
+
|
|
287
|
+
// 处理不同的事件类型
|
|
288
|
+
if (json.type === 'content_block_delta') {
|
|
289
|
+
const delta = json.delta;
|
|
290
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
291
|
+
yield { type: 'content', content: delta.text, done: false };
|
|
292
|
+
}
|
|
293
|
+
} else if (json.type === 'message_stop') {
|
|
294
|
+
yield { done: true };
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {
|
|
298
|
+
// 忽略解析错误
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
yield { done: true };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 转换工具格式: OpenAI tools -> Anthropic tools
|
|
309
|
+
*/
|
|
310
|
+
convertTools(openaiTools) {
|
|
311
|
+
return openaiTools.map(tool => ({
|
|
312
|
+
name: tool.function.name,
|
|
313
|
+
description: tool.function.description || '',
|
|
314
|
+
input_schema: tool.function.parameters || {
|
|
315
|
+
type: 'object',
|
|
316
|
+
properties: {}
|
|
317
|
+
}
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 获取模型列表
|
|
323
|
+
*/
|
|
324
|
+
async fetchModels() {
|
|
325
|
+
// Anthropic 没有 /models 端点,返回硬编码列表
|
|
326
|
+
return this.models;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 获取模型列表(本地)
|
|
331
|
+
*/
|
|
332
|
+
getModels() {
|
|
333
|
+
return this.models;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 获取状态
|
|
338
|
+
*/
|
|
339
|
+
getStatus() {
|
|
340
|
+
return {
|
|
341
|
+
id: this.id,
|
|
342
|
+
name: this.name,
|
|
343
|
+
nameCn: this.nameCn,
|
|
344
|
+
baseUrl: this.baseUrl,
|
|
345
|
+
connected: this.connected,
|
|
346
|
+
modelCount: this.models.length,
|
|
347
|
+
defaultModel: this.defaultModel,
|
|
348
|
+
hasApiKey: !!this.apiKey,
|
|
349
|
+
transport: 'anthropic_messages'
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 创建 Anthropic Provider
|
|
356
|
+
*/
|
|
357
|
+
export function createAnthropicProvider(apiKey = null, overrides = {}) {
|
|
358
|
+
return new AnthropicAdapter({
|
|
359
|
+
id: 'anthropic',
|
|
360
|
+
name: 'Claude',
|
|
361
|
+
nameCn: 'Anthropic Claude',
|
|
362
|
+
baseUrl: 'https://api.anthropic.com',
|
|
363
|
+
apiKey,
|
|
364
|
+
...overrides
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default AnthropicAdapter;
|