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 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 fallback (like OpenRouter auto-failover)
57
+ ### Model auto-routing (health probes + real-time switching)
58
58
 
59
- Try GPT-4 first. If it's rate-limited or times out, fall back to Claude, then Ollama.
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
- { 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
- ])
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: 'Explain quantum computing' }])
72
- // Automatically tries gpt-4 gpt-4o-mini claude-haiku llama3.2
73
- ```
76
+ const reply = await router.chat([{ role: 'user', content: 'Hello' }])
77
+ // → auto-routed to the healthiest model
74
78
 
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.
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.1.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
- * createRoutermodel fallback router
106
+ * createMonitorobservability wrapper
107
107
  *
108
- * Tries models in order. If one fails (rate_limit/timeout/server_error),
109
- * automatically falls back to the next.
108
+ * Wraps provider calls with latency tracking, error classification,
109
+ * and logging hooks. No external dependencies.
110
110
  *
111
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' }]);
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 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
- }
117
+ export function createMonitor(opts = {}) {
118
+ const onCall = opts.onCall || (() => {});
123
119
 
124
- async function chat(messages) {
125
- const errors = [];
126
- for (const entry of strategies) {
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 { 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
- );
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, 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;
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
- return { chat, strategies };
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
  }