provider-kit 1.1.1 → 1.2.1
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 +22 -13
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/providers/provider-error-adapter.js +167 -28
package/README.md
CHANGED
|
@@ -54,26 +54,35 @@ const provider = providerRegistry.getProvider('openai')
|
|
|
54
54
|
const reply = await provider.chat('gpt-4o-mini', [{ role: 'user', content: 'Hi' }])
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
### Model
|
|
57
|
+
### Model auto-routing (health probes + real-time switching)
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Periodically detects which models are available and routes to the best one. Changes take effect immediately — no restart needed.
|
|
60
60
|
|
|
61
61
|
```js
|
|
62
62
|
import { createRouter } from 'provider-kit'
|
|
63
63
|
|
|
64
|
-
const router = createRouter(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
const router = createRouter({
|
|
65
|
+
probes: [
|
|
66
|
+
{ provider: 'openai', model: 'gpt-4', apiKey: process.env.OPENAI_API_KEY },
|
|
67
|
+
{ provider: 'openai', model: 'gpt-4o-mini', apiKey: process.env.OPENAI_API_KEY },
|
|
68
|
+
{ provider: 'anthropic', model: 'claude-3-haiku', apiKey: process.env.ANTHROPIC_API_KEY },
|
|
69
|
+
{ provider: 'ollama', model: 'llama3.2', baseUrl: 'http://localhost:11434' },
|
|
70
|
+
],
|
|
71
|
+
strategy: 'latency', // 'latency' | 'failover' | 'round-robin'
|
|
72
|
+
probeInterval: 30000, // ping every 30s (0 = manual only)
|
|
73
|
+
onProbeResult: (results) => console.log(results),
|
|
74
|
+
})
|
|
70
75
|
|
|
71
|
-
const reply = await router.chat([{ role: 'user', content: '
|
|
72
|
-
//
|
|
73
|
-
```
|
|
76
|
+
const reply = await router.chat([{ role: 'user', content: 'Hello' }])
|
|
77
|
+
// → auto-routed to the healthiest model
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
// Manual probe
|
|
80
|
+
const status = await router.checkNow()
|
|
81
|
+
// → [{ provider, model, ok, latency, error }]
|
|
82
|
+
|
|
83
|
+
// Stop auto-probe
|
|
84
|
+
router.stop()
|
|
85
|
+
```
|
|
77
86
|
|
|
78
87
|
### Streaming
|
|
79
88
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "provider-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
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, createRouter } from './providers/provider-error-adapter.js';
|
|
13
|
+
export { ProviderError, AbortError, withRetry, withTimeout, safeProviderCall, classifyError, createCancelSignal, createRouter, createMonitor } 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';
|
|
@@ -103,43 +103,182 @@ export function createCancelSignal() {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
106
|
+
* createMonitor — observability wrapper
|
|
107
107
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
108
|
+
* Wraps provider calls with latency tracking, error classification,
|
|
109
|
+
* and logging hooks. No external dependencies.
|
|
110
110
|
*
|
|
111
111
|
* Usage:
|
|
112
|
-
* const
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* ]);
|
|
117
|
-
* const reply = await router.chat([{ role: 'user', content: 'Hi' }]);
|
|
112
|
+
* const monitor = createMonitor({ onCall: (record) => console.table(record) });
|
|
113
|
+
* const provider = monitor.wrap(await createProvider('openai', key));
|
|
114
|
+
* await provider.chat('gpt-4', messages);
|
|
115
|
+
* // → onCall receives { provider, model, latency, ok, error, timestamp }
|
|
118
116
|
*/
|
|
119
|
-
export function
|
|
120
|
-
|
|
121
|
-
throw new ProviderError('createRouter requires a non-empty array of strategies', { type: 'bad_request' });
|
|
122
|
-
}
|
|
117
|
+
export function createMonitor(opts = {}) {
|
|
118
|
+
const onCall = opts.onCall || (() => {});
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
120
|
+
function wrap(provider) {
|
|
121
|
+
const origChat = provider.chat.bind(provider);
|
|
122
|
+
const origStream = provider.chatStream?.bind(provider);
|
|
123
|
+
|
|
124
|
+
provider.chat = async (model, messages, options) => {
|
|
125
|
+
const start = Date.now();
|
|
127
126
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
return
|
|
131
|
-
() => provider.chat(entry.model, messages),
|
|
132
|
-
{ provider: entry.provider, retries: 1, timeout: 15000 }
|
|
133
|
-
);
|
|
127
|
+
const result = await origChat(model, messages, options);
|
|
128
|
+
onCall({ provider: provider.name || 'unknown', model, latency: Date.now() - start, ok: true, tokens: result.usage?.total_tokens || 0, timestamp: Date.now() });
|
|
129
|
+
return result;
|
|
134
130
|
} catch (e) {
|
|
135
|
-
const ce = classifyError(e,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
const ce = classifyError(e, provider.name);
|
|
132
|
+
onCall({ provider: provider.name || 'unknown', model, latency: Date.now() - start, ok: false, error: ce.type, message: ce.message, timestamp: Date.now() });
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (origStream) {
|
|
138
|
+
provider.chatStream = async function* (model, messages, options) {
|
|
139
|
+
const start = Date.now();
|
|
140
|
+
try {
|
|
141
|
+
const stream = origStream(model, messages, options);
|
|
142
|
+
for await (const chunk of stream) yield chunk;
|
|
143
|
+
onCall({ provider: provider.name || 'unknown', model, latency: Date.now() - start, ok: true, timestamp: Date.now() });
|
|
144
|
+
} catch (e) {
|
|
145
|
+
const ce = classifyError(e, provider.name);
|
|
146
|
+
onCall({ provider: provider.name || 'unknown', model, latency: Date.now() - start, ok: false, error: ce.type, timestamp: Date.now() });
|
|
147
|
+
throw e;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return provider;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { wrap };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* createRouter — model health probe + auto-routing
|
|
160
|
+
*
|
|
161
|
+
* Periodically checks each model's availability and latency.
|
|
162
|
+
* Routes requests to the best available model in real time.
|
|
163
|
+
* Changes take effect immediately — no restart needed.
|
|
164
|
+
*
|
|
165
|
+
* Usage:
|
|
166
|
+
* const router = createRouter({
|
|
167
|
+
* probes: [
|
|
168
|
+
* { provider: 'openai', model: 'gpt-4', apiKey: 'sk-...' },
|
|
169
|
+
* { provider: 'openai', model: 'gpt-4o-mini', apiKey: 'sk-...' },
|
|
170
|
+
* { provider: 'anthropic', model: 'claude-3-haiku', apiKey: 'sk-...' },
|
|
171
|
+
* { provider: 'ollama', model: 'llama3', baseUrl: 'http://localhost:11434' },
|
|
172
|
+
* ],
|
|
173
|
+
* strategy: 'latency', // 'latency' | 'failover' | 'round-robin' | 'cheapest'
|
|
174
|
+
* probeInterval: 30000, // check every 30s (0 = no auto-probe)
|
|
175
|
+
* probeTimeout: 5000, // per-probe timeout
|
|
176
|
+
* onProbeResult: (results) => console.log(results),
|
|
177
|
+
* });
|
|
178
|
+
*
|
|
179
|
+
* const reply = await router.chat([{ role: 'user', content: 'Hi' }]);
|
|
180
|
+
* // → routed to the best available model
|
|
181
|
+
*/
|
|
182
|
+
export function createRouter(opts) {
|
|
183
|
+
const probes = Array.isArray(opts) ? opts : opts.probes || opts.strategies || [];
|
|
184
|
+
if (!Array.isArray(probes) || probes.length === 0) {
|
|
185
|
+
throw new ProviderError('createRouter requires probes array', { type: 'bad_request' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const strategy = opts.strategy || 'latency';
|
|
189
|
+
const probeInterval = opts.probeInterval ?? 0;
|
|
190
|
+
const probeTimeout = opts.probeTimeout ?? 5000;
|
|
191
|
+
const onProbeResult = opts.onProbeResult || null;
|
|
192
|
+
|
|
193
|
+
// Probe results: { provider, model, ok, latency, error, timestamp }
|
|
194
|
+
let results = probes.map(p => ({ provider: p.provider, model: p.model, ok: true, latency: 0, error: null, timestamp: 0 }));
|
|
195
|
+
let rrIndex = 0;
|
|
196
|
+
let probeTimer = null;
|
|
197
|
+
|
|
198
|
+
// Probe a single model
|
|
199
|
+
async function probeOne(entry) {
|
|
200
|
+
const start = Date.now();
|
|
201
|
+
try {
|
|
202
|
+
const { createProvider } = await import('./openai-compatible.js');
|
|
203
|
+
const provider = await createProvider(entry.provider, entry.apiKey, { baseUrl: entry.baseUrl });
|
|
204
|
+
await withTimeout(
|
|
205
|
+
() => provider.chat(entry.model, [{ role: 'user', content: 'Hi' }], { max_tokens: 1 }),
|
|
206
|
+
probeTimeout
|
|
207
|
+
);
|
|
208
|
+
return { ok: true, latency: Date.now() - start, error: null };
|
|
209
|
+
} catch (e) {
|
|
210
|
+
const ce = classifyError(e, entry.provider);
|
|
211
|
+
return { ok: false, latency: Date.now() - start, error: ce.type === 'auth' ? 'auth_failed' : ce.message };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Probe all models, update results
|
|
216
|
+
async function probeAll() {
|
|
217
|
+
const newResults = await Promise.all(probes.map(async (entry, i) => {
|
|
218
|
+
const { ok, latency, error } = await probeOne(entry);
|
|
219
|
+
return { provider: entry.provider, model: entry.model, ok, latency, error, timestamp: Date.now() };
|
|
220
|
+
}));
|
|
221
|
+
results = newResults;
|
|
222
|
+
if (onProbeResult) onProbeResult(results);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Pick the best model based on strategy
|
|
226
|
+
function pick() {
|
|
227
|
+
const alive = results.filter(r => r.ok);
|
|
228
|
+
if (alive.length === 0) return probes[0]; // all dead → try first anyway
|
|
229
|
+
|
|
230
|
+
switch (strategy) {
|
|
231
|
+
case 'latency':
|
|
232
|
+
alive.sort((a, b) => a.latency - b.latency);
|
|
233
|
+
return probes[results.indexOf(alive[0])];
|
|
234
|
+
case 'round-robin': {
|
|
235
|
+
const idx = rrIndex % alive.length;
|
|
236
|
+
rrIndex++;
|
|
237
|
+
return probes[results.indexOf(alive[idx])];
|
|
238
|
+
}
|
|
239
|
+
case 'failover': {
|
|
240
|
+
const preferred = probes.map((p, i) => ({ p, i })).sort((a, b) => a.i - b.i);
|
|
241
|
+
for (const { p, i } of preferred) {
|
|
242
|
+
if (results[i]?.ok) return p;
|
|
243
|
+
}
|
|
244
|
+
return probes[0];
|
|
139
245
|
}
|
|
246
|
+
default:
|
|
247
|
+
return probes[probes.indexOf(alive[0])];
|
|
140
248
|
}
|
|
141
|
-
throw new ProviderError(`All models failed: ${errors.map(e => `${e.provider}/${e.model}`).join(', ')}`, { type: 'server_error' });
|
|
142
249
|
}
|
|
143
250
|
|
|
144
|
-
|
|
251
|
+
// Start auto-probe
|
|
252
|
+
if (probeInterval > 0) {
|
|
253
|
+
probeAll(); // first probe immediately
|
|
254
|
+
probeTimer = setInterval(probeAll, probeInterval);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Chat — route to best model
|
|
258
|
+
async function chat(messages) {
|
|
259
|
+
const entry = pick();
|
|
260
|
+
const idx = probes.indexOf(entry);
|
|
261
|
+
const r = results[idx];
|
|
262
|
+
|
|
263
|
+
if (!r?.ok && strategy !== 'failover') {
|
|
264
|
+
// All dead — force probe once
|
|
265
|
+
const probeResult = await probeOne(entry);
|
|
266
|
+
results[idx] = { ...results[idx], ...probeResult, timestamp: Date.now() };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { createProvider } = await import('./openai-compatible.js');
|
|
270
|
+
const provider = await createProvider(entry.provider, entry.apiKey, { baseUrl: entry.baseUrl });
|
|
271
|
+
return safeProviderCall(
|
|
272
|
+
() => provider.chat(entry.model, messages),
|
|
273
|
+
{ provider: entry.provider, retries: 1, timeout: 30000 }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Manual probe trigger
|
|
278
|
+
async function checkNow() { await probeAll(); return results; }
|
|
279
|
+
|
|
280
|
+
// Stop auto-probe
|
|
281
|
+
function stop() { if (probeTimer) { clearInterval(probeTimer); probeTimer = null; } }
|
|
282
|
+
|
|
283
|
+
return { chat, probes, results: () => results, strategy, checkNow, stop };
|
|
145
284
|
}
|