provider-kit 1.0.0 → 1.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 +192 -144
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/providers/provider-error-adapter.js +42 -0
package/README.md
CHANGED
|
@@ -1,144 +1,192 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npm install provider-kit
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for await (const chunk of stream) {
|
|
85
|
-
if (chunk.type === 'content') process.stdout.write(chunk.content)
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
1
|
+
# Every LLM has its own SDK. One API for all of them.
|
|
2
|
+
|
|
3
|
+
**Switch between OpenAI, Anthropic, Ollama, Google Gemini, and 38 more — without changing a line of code.** provider-kit wraps 42 LLM providers behind one consistent `chat()` interface, with built-in retry, timeout, error handling, and model fallback routing.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install provider-kit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { createProvider } from 'provider-kit'
|
|
15
|
+
|
|
16
|
+
const client = await createProvider('openai', process.env.OPENAI_API_KEY)
|
|
17
|
+
const stream = client.chatStream('gpt-4o-mini', [
|
|
18
|
+
{ role: 'user', content: 'Write a haiku' },
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
for await (const chunk of stream) {
|
|
22
|
+
if (chunk.type === 'content') process.stdout.write(chunk.content)
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Save as `demo.mjs`, run `node demo.mjs`, see the first token in under 60 seconds.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Basic chat
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { createProvider } from 'provider-kit'
|
|
36
|
+
|
|
37
|
+
const provider = await createProvider('openai', process.env.OPENAI_API_KEY)
|
|
38
|
+
const reply = await provider.chat('gpt-4o-mini', [
|
|
39
|
+
{ role: 'user', content: 'Hello' },
|
|
40
|
+
])
|
|
41
|
+
console.log(reply.content)
|
|
42
|
+
// → "Hello! How can I help you today?"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Multiple providers
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { providerRegistry } from 'provider-kit'
|
|
49
|
+
|
|
50
|
+
await providerRegistry.configure('openai', { apiKey: process.env.OPENAI_API_KEY, defaultModel: 'gpt-4o-mini' })
|
|
51
|
+
await providerRegistry.configure('ollama', { baseUrl: 'http://localhost:11434', defaultModel: 'llama3.2' })
|
|
52
|
+
|
|
53
|
+
const provider = providerRegistry.getProvider('openai')
|
|
54
|
+
const reply = await provider.chat('gpt-4o-mini', [{ role: 'user', content: 'Hi' }])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Model fallback (like OpenRouter auto-failover)
|
|
58
|
+
|
|
59
|
+
Try GPT-4 first. If it's rate-limited or times out, fall back to Claude, then Ollama.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { createRouter } from 'provider-kit'
|
|
63
|
+
|
|
64
|
+
const router = createRouter([
|
|
65
|
+
{ provider: 'openai', model: 'gpt-4', apiKey: process.env.OPENAI_API_KEY },
|
|
66
|
+
{ provider: 'openai', model: 'gpt-4o-mini', apiKey: process.env.OPENAI_API_KEY },
|
|
67
|
+
{ provider: 'anthropic', model: 'claude-3-haiku', apiKey: process.env.ANTHROPIC_API_KEY },
|
|
68
|
+
{ provider: 'ollama', model: 'llama3.2', baseUrl: 'http://localhost:11434' },
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
const reply = await router.chat([{ role: 'user', content: 'Explain quantum computing' }])
|
|
72
|
+
// Automatically tries gpt-4 → gpt-4o-mini → claude-haiku → llama3.2
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Router skips auth/bad_request/quota errors (those won't work on another model either).
|
|
76
|
+
Only retries on rate_limit, timeout, server_error, and network errors.
|
|
77
|
+
|
|
78
|
+
### Streaming
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const stream = provider.chatStream('gpt-4o-mini', [
|
|
82
|
+
{ role: 'user', content: 'Count to 10' },
|
|
83
|
+
])
|
|
84
|
+
for await (const chunk of stream) {
|
|
85
|
+
if (chunk.type === 'content') process.stdout.write(chunk.content)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Cancel streaming anytime:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { createCancelSignal } from 'provider-kit'
|
|
93
|
+
const { signal, cancel } = createCancelSignal()
|
|
94
|
+
setTimeout(() => cancel('User stopped'), 3000)
|
|
95
|
+
const stream = provider.chatStream('gpt-4', messages, { signal })
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Function Calling
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
const reply = await provider.chat('gpt-4', messages, {
|
|
102
|
+
tools: [{
|
|
103
|
+
type: 'function',
|
|
104
|
+
function: {
|
|
105
|
+
name: 'get_weather',
|
|
106
|
+
description: 'Get weather for a city',
|
|
107
|
+
parameters: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: { city: { type: 'string' } },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}],
|
|
113
|
+
})
|
|
114
|
+
if (reply.toolCalls) {
|
|
115
|
+
console.log(reply.toolCalls[0].name) // → "get_weather"
|
|
116
|
+
console.log(reply.toolCalls[0].arguments) // → { city: "Tokyo" }
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Error handling
|
|
121
|
+
|
|
122
|
+
Every error is a `ProviderError` with a consistent structure:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import { ProviderError, withRetry, withTimeout, safeProviderCall } from 'provider-kit'
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const reply = await safeProviderCall(
|
|
129
|
+
() => provider.chat('gpt-4', messages),
|
|
130
|
+
{ provider: 'openai', retries: 3, timeout: 30000 }
|
|
131
|
+
)
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (e instanceof ProviderError) {
|
|
134
|
+
console.log(e.provider) // → "openai"
|
|
135
|
+
console.log(e.statusCode) // → 429
|
|
136
|
+
console.log(e.retryable) // → true
|
|
137
|
+
console.log(e.type) // → "rate_limit" | "auth" | "timeout" | "server_error" | "quota" | "bad_request" | "network" | "unknown"
|
|
138
|
+
console.log(e.message) // → "Rate limit exceeded — slow down or upgrade your plan"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Type | Meaning | Retryable |
|
|
144
|
+
|------|---------|-----------|
|
|
145
|
+
| `rate_limit` | Too many requests | ✅ |
|
|
146
|
+
| `auth` | Bad API key | ❌ |
|
|
147
|
+
| `timeout` | Provider didn't respond | ✅ |
|
|
148
|
+
| `server_error` | Provider 5xx error | ✅ |
|
|
149
|
+
| `quota` | Token budget exhausted | ❌ |
|
|
150
|
+
| `bad_request` | Invalid input | ❌ |
|
|
151
|
+
| `network` | DNS/connection failure | ✅ |
|
|
152
|
+
| `unknown` | Catch-all | depends |
|
|
153
|
+
|
|
154
|
+
### Classify raw errors
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import { classifyError } from 'provider-kit'
|
|
158
|
+
|
|
159
|
+
const err = new Error('429 Too Many Requests')
|
|
160
|
+
console.log(classifyError(err, 'openai').type) // → "rate_limit"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Available providers
|
|
166
|
+
|
|
167
|
+
| Provider | `type` | Config |
|
|
168
|
+
|----------|--------|--------|
|
|
169
|
+
| OpenAI | `openai` | `apiKey` |
|
|
170
|
+
| Anthropic | `anthropic` | `apiKey` |
|
|
171
|
+
| Google Gemini | `gemini` | `apiKey` |
|
|
172
|
+
| Ollama | `ollama` | `baseUrl` (default http://localhost:11434) |
|
|
173
|
+
| Azure OpenAI | `azure` | `apiKey` + `baseUrl` |
|
|
174
|
+
| AWS Bedrock | `bedrock` | AWS credentials |
|
|
175
|
+
| Cohere | `cohere` | `apiKey` |
|
|
176
|
+
| Any OpenAI-compatible | `openai` + custom `baseUrl` | `apiKey` + `baseUrl` |
|
|
177
|
+
|
|
178
|
+
32 more providers available via OpenAI-compatible fallback — just set the `baseUrl`.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Known Limitations (v1.0.0)
|
|
183
|
+
|
|
184
|
+
- **10 adapters implemented out of 42 presets.** The remaining 32 use OpenAI-compatible fallback. Contributions welcome.
|
|
185
|
+
- **No TypeScript types.** Planned.
|
|
186
|
+
- **API keys are stored in memory.** No OS keychain integration. Do not hardcode keys in source files.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Related
|
|
191
|
+
|
|
192
|
+
- `fairy-guardian` — self-healing process cluster for AI model servers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "provider-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* const reply = await provider.chat('gpt-4', [{ role: 'user', content: 'Hi' }]);
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export { ProviderError, AbortError, withRetry, withTimeout, safeProviderCall, classifyError, createCancelSignal } from './providers/provider-error-adapter.js';
|
|
13
|
+
export { ProviderError, AbortError, withRetry, withTimeout, safeProviderCall, classifyError, createCancelSignal, createRouter } from './providers/provider-error-adapter.js';
|
|
14
14
|
export { ProviderManager, providerManager } from './providers/provider-manager.js';
|
|
15
15
|
export { providerRegistry, createProvider, listPresetProviders, PRESET_PROVIDERS } from './providers/provider-registry.js';
|
|
16
16
|
export { persistentConfig, createStore } from './core/persistent-config.js';
|
|
@@ -101,3 +101,45 @@ export function createCancelSignal() {
|
|
|
101
101
|
cancel: (reason) => controller.abort(new AbortError(reason || 'Cancelled by user')),
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* createRouter — model fallback router
|
|
107
|
+
*
|
|
108
|
+
* Tries models in order. If one fails (rate_limit/timeout/server_error),
|
|
109
|
+
* automatically falls back to the next.
|
|
110
|
+
*
|
|
111
|
+
* Usage:
|
|
112
|
+
* const router = createRouter([
|
|
113
|
+
* { provider: 'openai', model: 'gpt-4', apiKey: 'sk-...' },
|
|
114
|
+
* { provider: 'openai', model: 'gpt-4o-mini', apiKey: 'sk-...' },
|
|
115
|
+
* { provider: 'anthropic', model: 'claude-3-haiku', apiKey: 'sk-...' },
|
|
116
|
+
* ]);
|
|
117
|
+
* const reply = await router.chat([{ role: 'user', content: 'Hi' }]);
|
|
118
|
+
*/
|
|
119
|
+
export function createRouter(strategies) {
|
|
120
|
+
if (!Array.isArray(strategies) || strategies.length === 0) {
|
|
121
|
+
throw new ProviderError('createRouter requires a non-empty array of strategies', { type: 'bad_request' });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function chat(messages) {
|
|
125
|
+
const errors = [];
|
|
126
|
+
for (const entry of strategies) {
|
|
127
|
+
try {
|
|
128
|
+
const { createProvider } = await import('./openai-compatible.js');
|
|
129
|
+
const provider = await createProvider(entry.provider, entry.apiKey, { baseUrl: entry.baseUrl });
|
|
130
|
+
return await safeProviderCall(
|
|
131
|
+
() => provider.chat(entry.model, messages),
|
|
132
|
+
{ provider: entry.provider, retries: 1, timeout: 15000 }
|
|
133
|
+
);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
const ce = classifyError(e, entry.provider);
|
|
136
|
+
if (ce.type === 'auth' || ce.type === 'bad_request' || ce.type === 'quota') throw ce;
|
|
137
|
+
errors.push({ provider: entry.provider, model: entry.model, error: ce.message });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new ProviderError(`All models failed: ${errors.map(e => `${e.provider}/${e.model}`).join(', ')}`, { type: 'server_error' });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { chat, strategies };
|
|
145
|
+
}
|