universal-llm-client 4.2.0 → 4.5.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/CHANGELOG.md +142 -103
- package/LICENSE +21 -21
- package/README.md +640 -591
- package/dist/ai-model.d.ts +12 -1
- package/dist/ai-model.d.ts.map +1 -1
- package/dist/ai-model.js +36 -1
- package/dist/ai-model.js.map +1 -1
- package/dist/gemma-channel.d.ts +14 -0
- package/dist/gemma-channel.d.ts.map +1 -0
- package/dist/gemma-channel.js +38 -0
- package/dist/gemma-channel.js.map +1 -0
- package/dist/gemma-diffusion.d.ts +49 -0
- package/dist/gemma-diffusion.d.ts.map +1 -0
- package/dist/gemma-diffusion.js +147 -0
- package/dist/gemma-diffusion.js.map +1 -0
- package/dist/http.d.ts +4 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +14 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +183 -7
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +28 -3
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/google.d.ts +22 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +225 -13
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/ollama.d.ts +2 -0
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +59 -30
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +14 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +200 -22
- package/dist/providers/openai.js.map +1 -1
- package/dist/router.d.ts +2 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -0
- package/dist/router.js.map +1 -1
- package/dist/stream-decoder.d.ts +12 -0
- package/dist/stream-decoder.d.ts.map +1 -1
- package/dist/stream-decoder.js +182 -5
- package/dist/stream-decoder.js.map +1 -1
- package/dist/thinking.d.ts +36 -0
- package/dist/thinking.d.ts.map +1 -0
- package/dist/thinking.js +52 -0
- package/dist/thinking.js.map +1 -0
- package/package.json +118 -116
- package/src/ai-model.ts +400 -350
- package/src/auditor.ts +213 -213
- package/src/client.ts +402 -402
- package/src/debug/debug-google-streaming.ts +1 -1
- package/src/demos/basic/universal-llm-examples.ts +3 -3
- package/src/demos/diffusion-gemma/.env +29 -0
- package/src/demos/diffusion-gemma/.env.example +27 -0
- package/src/demos/diffusion-gemma/CLAUDE.md +95 -0
- package/src/demos/diffusion-gemma/README.md +59 -0
- package/src/demos/diffusion-gemma/canvas.ts +1606 -0
- package/src/demos/diffusion-gemma/docker-compose.yml +29 -0
- package/src/demos/diffusion-gemma/probe-stream.ts +51 -0
- package/src/demos/diffusion-gemma/probe-tools.ts +55 -0
- package/src/demos/diffusion-gemma/server.ts +1205 -0
- package/src/demos/diffusion-gemma/start-vllm.sh +98 -0
- package/src/gemma-channel.ts +47 -0
- package/src/gemma-diffusion.ts +167 -0
- package/src/http.ts +261 -247
- package/src/index.ts +180 -161
- package/src/interfaces.ts +843 -657
- package/src/mcp.ts +345 -345
- package/src/providers/anthropic.ts +796 -762
- package/src/providers/google.ts +840 -620
- package/src/providers/index.ts +8 -8
- package/src/providers/ollama.ts +503 -469
- package/src/providers/openai.ts +587 -392
- package/src/router.ts +785 -780
- package/src/stream-decoder.ts +535 -361
- package/src/structured-output.ts +759 -759
- package/src/test-scripts/test-google-deep-research.ts +33 -0
- package/src/test-scripts/test-google-streaming-enhanced.ts +147 -147
- package/src/test-scripts/test-google-streaming.ts +1 -1
- package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -189
- package/src/test-scripts/test-google-thinking.ts +46 -0
- package/src/test-scripts/test-system-message-positions.ts +163 -163
- package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -83
- package/src/test-scripts/test-vllm-qwen36.ts +256 -0
- package/src/tests/ai-model.test.ts +1614 -1614
- package/src/tests/auditor.test.ts +224 -224
- package/src/tests/gemma-diffusion.test.ts +115 -0
- package/src/tests/http.test.ts +200 -200
- package/src/tests/interfaces.test.ts +117 -117
- package/src/tests/providers/anthropic.test.ts +118 -0
- package/src/tests/providers/google.test.ts +841 -660
- package/src/tests/providers/ollama.test.ts +1034 -954
- package/src/tests/providers/openai.test.ts +1511 -1122
- package/src/tests/router.test.ts +254 -254
- package/src/tests/stream-decoder.test.ts +263 -179
- package/src/tests/structured-output.test.ts +1450 -1450
- package/src/tests/thinking.test.ts +65 -0
- package/src/tests/tools.test.ts +175 -175
- package/src/thinking.ts +73 -0
- package/src/tools.ts +246 -246
- package/src/zod-adapter.ts +72 -72
package/README.md
CHANGED
|
@@ -1,591 +1,640 @@
|
|
|
1
|
-
# universal-llm-client
|
|
2
|
-
|
|
3
|
-
A universal LLM client for JavaScript/TypeScript with **transparent provider failover
|
|
4
|
-
|
|
5
|
-
```typescript
|
|
6
|
-
import { AIModel } from 'universal-llm-client';
|
|
7
|
-
|
|
8
|
-
const model = new AIModel({
|
|
9
|
-
model: 'gemini-
|
|
10
|
-
providers: [
|
|
11
|
-
{ type: 'google', apiKey: process.env.GOOGLE_API_KEY },
|
|
12
|
-
{ type: 'openai', url: 'https://openrouter.ai/api', apiKey: process.env.OPENROUTER_KEY },
|
|
13
|
-
{ type: 'ollama' },
|
|
14
|
-
],
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const response = await model.chat([
|
|
18
|
-
{ role: 'user', content: 'Hello!' },
|
|
19
|
-
]);
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
> **One model, multiple backends.** If Google fails, it transparently fails over to OpenRouter, then to local Ollama. Your code never knows the difference.
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Features
|
|
27
|
-
|
|
28
|
-
- 🔄 **Transparent Failover** — Priority-ordered provider chain with retries, health tracking, and cooldowns
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const response = await model.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
**
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
for
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
universal-llm-client
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
-
|
|
581
|
-
-
|
|
582
|
-
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1
|
+
# universal-llm-client
|
|
2
|
+
|
|
3
|
+
A universal LLM client for JavaScript/TypeScript with **transparent provider failover** and a **provider-agnostic reasoning API** — one set of code across OpenAI, Anthropic, Google Gemini, Ollama, vLLM, and any OpenAI-compatible endpoint. Streaming tool execution, structured output, generation stats, and native observability included.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { AIModel } from 'universal-llm-client';
|
|
7
|
+
|
|
8
|
+
const model = new AIModel({
|
|
9
|
+
model: 'gemini-3.5-flash',
|
|
10
|
+
providers: [
|
|
11
|
+
{ type: 'google', apiKey: process.env.GOOGLE_API_KEY },
|
|
12
|
+
{ type: 'openai', url: 'https://openrouter.ai/api', apiKey: process.env.OPENROUTER_KEY },
|
|
13
|
+
{ type: 'ollama' },
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const response = await model.chat([
|
|
18
|
+
{ role: 'user', content: 'Hello!' },
|
|
19
|
+
]);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> **One model, multiple backends.** If Google fails, it transparently fails over to OpenRouter, then to local Ollama. Your code never knows the difference.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- 🔄 **Transparent Failover** — Priority-ordered provider chain with retries, health tracking, and cooldowns
|
|
29
|
+
- 🧠 **Unified Reasoning** — One `thinking` flag (`true`/`false` or a level: `'minimal' | 'low' | 'medium' | 'high'`) mapped to each backend's native control; chain-of-thought surfaced as `response.reasoning` + streaming `thinking` events (with `<think>`-tag parsing as a fallback)
|
|
30
|
+
- 🛠️ **Tool Calling** — Register tools once, works across all providers. Autonomous multi-turn execution loop
|
|
31
|
+
- 📋 **Structured Output** — Zod schema validation, JSON Schema support, streaming, and type-safe responses
|
|
32
|
+
- 🌊 **Streaming** — First-class async generator streaming with pluggable decoder strategies
|
|
33
|
+
- 🔬 **Deep Research** — Drive Google Gemini's agentic Deep Research (background interactions with polling + streaming)
|
|
34
|
+
- 📈 **Generation Stats** — `usage.tokensPerSecond` and `durationMs` reported across providers
|
|
35
|
+
- 🔌 **Flexible Transport** — Custom headers, query params, auth header/prefix, and base path for Azure OpenAI and gateways
|
|
36
|
+
- 🔍 **Observability** — Built-in auditor interface for logging, cost tracking, and behavioral analysis
|
|
37
|
+
- 🌐 **Universal Runtime** — Node.js 22+, Bun, Deno, and modern browsers
|
|
38
|
+
- 🤖 **MCP Native** — Bridge MCP servers to LLM tools with zero glue code
|
|
39
|
+
- 📊 **Embeddings** — Single and batch embedding generation
|
|
40
|
+
|
|
41
|
+
## Supported Providers
|
|
42
|
+
|
|
43
|
+
| Provider | Type | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| **Ollama** | `ollama` | Local or cloud models, NDJSON streaming, model pulling, vision/multimodal, native thinking |
|
|
46
|
+
| **OpenAI + Compat** | `openai` | GPT series, o-series + **any OpenAI-compatible endpoint**: xAI/Grok, Mistral, DeepSeek, Cohere Compatibility, Groq, Together, Fireworks, OpenRouter, Perplexity Sonar, vLLM, LM Studio, TGI, most self-hosted servers |
|
|
47
|
+
| **Google AI Studio** | `google` | Gemini models, system instructions, multimodal, native thinking + grounding |
|
|
48
|
+
| **Vertex AI** | `vertex` | Same as Google AI but with regional endpoints, Bearer tokens, service tiers (flex/priority) |
|
|
49
|
+
| **Anthropic (Claude)** | `anthropic` | Claude 3.5/4 models via native Messages API. Excellent tool use, extended thinking with signatures, strong prompt caching |
|
|
50
|
+
| **LlamaCpp** | `llamacpp` | Local llama.cpp / llama-server instances (OpenAI-compatible under the hood) |
|
|
51
|
+
|
|
52
|
+
**Most of the world** is reachable via `type: 'openai'` + a `url` override. We only maintain dedicated clients for fundamentally different protocols (Anthropic Messages, Google Gemini) that offer unique high-value capabilities, plus Ollama for local developer experience. See `docs/guide/providers.md` and the research survey in `docs/research/provider-api-landscape-2026.md`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bun add universal-llm-client
|
|
60
|
+
# or
|
|
61
|
+
npm install universal-llm-client
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Optional**: For MCP integration:
|
|
65
|
+
```bash
|
|
66
|
+
bun add @modelcontextprotocol/sdk
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
### Basic Chat
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { AIModel } from 'universal-llm-client';
|
|
77
|
+
|
|
78
|
+
const model = new AIModel({
|
|
79
|
+
model: 'qwen3:4b',
|
|
80
|
+
providers: [{ type: 'ollama' }],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const response = await model.chat([
|
|
84
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
85
|
+
{ role: 'user', content: 'What is the capital of France?' },
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
console.log(response.message.content);
|
|
89
|
+
// "The capital of France is Paris."
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Streaming
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
for await (const event of model.chatStream([
|
|
96
|
+
{ role: 'user', content: 'Write a haiku about code.' },
|
|
97
|
+
])) {
|
|
98
|
+
if (event.type === 'text') {
|
|
99
|
+
process.stdout.write(event.content);
|
|
100
|
+
} else if (event.type === 'thinking') {
|
|
101
|
+
// Model reasoning (when supported)
|
|
102
|
+
console.log('[thinking]', event.content);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Thinking & Reasoning
|
|
108
|
+
|
|
109
|
+
Set one `thinking` value — `true`/`false` or a level (`'minimal' | 'low' | 'medium' | 'high'`) —
|
|
110
|
+
and it maps to each provider's native control (Gemini `thinkingLevel`/`thinkingBudget`, OpenAI
|
|
111
|
+
`reasoning_effort`, vLLM `enable_thinking`, Anthropic `budget_tokens`, Ollama `think`):
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const model = new AIModel({
|
|
115
|
+
model: 'gemini-3.5-flash',
|
|
116
|
+
thinking: 'high', // true | false | 'minimal' | 'low' | 'medium' | 'high'
|
|
117
|
+
providers: [{ type: 'google', apiKey: process.env.GOOGLE_API_KEY }],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const res = await model.chat([{ role: 'user', content: 'Solve this step by step: ...' }]);
|
|
121
|
+
console.log(res.message.content); // final answer (clean)
|
|
122
|
+
console.log(res.reasoning); // chain-of-thought, when the model exposes it
|
|
123
|
+
|
|
124
|
+
// Per-call override (e.g. turn thinking off for structured output)
|
|
125
|
+
await model.chat(messages, { thinking: false });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Deep Research (Gemini)
|
|
129
|
+
|
|
130
|
+
Run Google's agentic Deep Research — creates a background interaction and polls to completion:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const result = await model.deepResearch('Research the history of Google TPUs.', {
|
|
134
|
+
tools: ['google_search', 'url_context'],
|
|
135
|
+
});
|
|
136
|
+
console.log(result.status, result.report);
|
|
137
|
+
|
|
138
|
+
// Or stream intermediate thoughts and steps as they arrive:
|
|
139
|
+
for await (const ev of model.deepResearchStream('Compare RISC-V vs ARM in 2026.')) {
|
|
140
|
+
if (ev.type === 'thought') console.log('[thinking]', ev.content);
|
|
141
|
+
else if (ev.type === 'text') process.stdout.write(ev.content);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Tool Calling
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
model.registerTool(
|
|
149
|
+
'get_weather',
|
|
150
|
+
'Get current weather for a location',
|
|
151
|
+
{
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
city: { type: 'string', description: 'City name' },
|
|
155
|
+
},
|
|
156
|
+
required: ['city'],
|
|
157
|
+
},
|
|
158
|
+
async (args) => {
|
|
159
|
+
const { city } = args as { city: string };
|
|
160
|
+
return { temperature: 22, condition: 'sunny', city };
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Autonomous tool execution — the model calls tools and loops until done
|
|
165
|
+
const response = await model.chatWithTools([
|
|
166
|
+
{ role: 'user', content: "What's the weather in Tokyo?" },
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
console.log(response.message.content);
|
|
170
|
+
// "The weather in Tokyo is 22°C and sunny."
|
|
171
|
+
console.log(response.toolExecutions);
|
|
172
|
+
// [{ tool_call_id: 'call_abc', output: { temperature: 22, condition: 'sunny', city: 'Tokyo' }, duration: 5 }]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Provider Failover
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const model = new AIModel({
|
|
179
|
+
model: 'gemini-2.5-flash',
|
|
180
|
+
retries: 2, // retries per provider before failover
|
|
181
|
+
timeout: 30000, // request timeout in ms
|
|
182
|
+
providers: [
|
|
183
|
+
{ type: 'google', apiKey: process.env.GOOGLE_KEY, priority: 0 },
|
|
184
|
+
{ type: 'openai', url: 'https://openrouter.ai/api', apiKey: process.env.OPENROUTER_KEY, priority: 1 },
|
|
185
|
+
{ type: 'ollama', url: 'http://localhost:11434', priority: 2 },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// If Google returns 500, retries twice, then seamlessly tries OpenRouter.
|
|
190
|
+
// If OpenRouter also fails, falls back to local Ollama.
|
|
191
|
+
// Your code sees a single response.
|
|
192
|
+
const response = await model.chat([{ role: 'user', content: 'Hello' }]);
|
|
193
|
+
|
|
194
|
+
// Check provider health at any time
|
|
195
|
+
console.log(model.getProviderStatus());
|
|
196
|
+
// [{ id: 'google-0', healthy: true }, { id: 'openai-1', healthy: true }, ...]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Multimodal (Vision)
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { AIModel, multimodalMessage } from 'universal-llm-client';
|
|
203
|
+
|
|
204
|
+
const model = new AIModel({
|
|
205
|
+
model: 'gemini-2.5-flash',
|
|
206
|
+
providers: [{ type: 'google', apiKey: process.env.GOOGLE_KEY }],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const response = await model.chat([
|
|
210
|
+
multimodalMessage('What do you see in this image?', [
|
|
211
|
+
'https://example.com/photo.jpg',
|
|
212
|
+
]),
|
|
213
|
+
]);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Embeddings
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const embedModel = new AIModel({
|
|
220
|
+
model: 'nomic-embed-text-v2-moe:latest',
|
|
221
|
+
providers: [{ type: 'ollama' }],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const vector = await embedModel.embed('Hello world');
|
|
225
|
+
// [0.006, 0.026, -0.009, ...]
|
|
226
|
+
|
|
227
|
+
const vectors = await embedModel.embedArray(['Hello', 'World']);
|
|
228
|
+
// [[0.006, ...], [0.012, ...]]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Structured Output
|
|
232
|
+
|
|
233
|
+
Get typed, validated JSON responses from any LLM using Zod schemas:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { AIModel } from 'universal-llm-client';
|
|
237
|
+
import { z } from 'zod';
|
|
238
|
+
|
|
239
|
+
const model = new AIModel({
|
|
240
|
+
model: 'gemini-2.5-flash',
|
|
241
|
+
providers: [
|
|
242
|
+
{ type: 'google', apiKey: process.env.GOOGLE_API_KEY },
|
|
243
|
+
{ type: 'ollama' },
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Define your schema
|
|
248
|
+
const UserSchema = z.object({
|
|
249
|
+
name: z.string(),
|
|
250
|
+
age: z.number(),
|
|
251
|
+
email: z.string().email(),
|
|
252
|
+
interests: z.array(z.string()),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Method 1: generateStructured (throws on validation failure)
|
|
256
|
+
const user = await model.generateStructured(UserSchema, [
|
|
257
|
+
{ role: 'user', content: 'Generate a user profile for a software developer' },
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
console.log(user.name); // TypeScript knows this is string
|
|
261
|
+
console.log(user.age); // TypeScript knows this is number
|
|
262
|
+
console.log(user.email); // TypeScript knows this is string
|
|
263
|
+
console.log(user.interests); // TypeScript knows this is string[]
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Non-throwing variant:**
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// Method 2: tryParseStructured (returns result object, never throws)
|
|
270
|
+
const result = await model.tryParseStructured(UserSchema, messages);
|
|
271
|
+
|
|
272
|
+
if (result.ok) {
|
|
273
|
+
console.log('User:', result.value.name);
|
|
274
|
+
} else {
|
|
275
|
+
console.log('Error:', result.error.message);
|
|
276
|
+
console.log('Raw LLM output:', result.rawOutput);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Via chat options:**
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Method 3: chat with output parameter
|
|
284
|
+
const response = await model.chat(messages, {
|
|
285
|
+
output: { schema: UserSchema },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// response.structured is typed as { name: string, age: number, ... }
|
|
289
|
+
if (response.structured) {
|
|
290
|
+
console.log(response.structured.name);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Streaming structured output:**
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// Stream partial validated objects as JSON generates
|
|
298
|
+
for await (const partial of model.generateStructuredStream(UserSchema, messages)) {
|
|
299
|
+
console.log('Partial:', partial);
|
|
300
|
+
// Partial: { name: 'Alice' }
|
|
301
|
+
// Partial: { name: 'Alice', age: 30 }
|
|
302
|
+
// Partial: { name: 'Alice', age: 30, email: 'alice@example.com' }
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Raw JSON Schema (without Zod):**
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const response = await model.chat(messages, {
|
|
310
|
+
jsonSchema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
name: { type: 'string' },
|
|
314
|
+
age: { type: 'number' },
|
|
315
|
+
},
|
|
316
|
+
required: ['name', 'age'],
|
|
317
|
+
},
|
|
318
|
+
name: 'Person', // Optional, used for LLM guidance
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Separate module import (tree-shaking):**
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Import only structured output types if you don't need the full client
|
|
326
|
+
import {
|
|
327
|
+
StructuredOutputError,
|
|
328
|
+
type StructuredOutputResult,
|
|
329
|
+
type StructuredOutputOptions,
|
|
330
|
+
parseStructured,
|
|
331
|
+
tryParseStructured,
|
|
332
|
+
zodToJsonSchema,
|
|
333
|
+
} from 'universal-llm-client/structured-output';
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Vision with structured output:**
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
const ImageAnalysisSchema = z.object({
|
|
340
|
+
objects: z.array(z.string()),
|
|
341
|
+
scene: z.string(),
|
|
342
|
+
mood: z.string(),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const response = await model.generateStructured(ImageAnalysisSchema, [
|
|
346
|
+
multimodalMessage('Analyze this image', ['https://example.com/photo.jpg']),
|
|
347
|
+
]);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Provider compatibility:**
|
|
351
|
+
|
|
352
|
+
| Provider | Method | Notes |
|
|
353
|
+
|----------|--------|-------|
|
|
354
|
+
| OpenAI | `response_format.json_schema` | Strict mode enabled |
|
|
355
|
+
| Ollama | `format: { schema }` | Model must support grammar |
|
|
356
|
+
| Google | `responseMimeType + responseSchema` | Some features stripped |
|
|
357
|
+
|
|
358
|
+
### Observability
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { AIModel, ConsoleAuditor, BufferedAuditor } from 'universal-llm-client';
|
|
362
|
+
|
|
363
|
+
// Simple console logging
|
|
364
|
+
const model = new AIModel({
|
|
365
|
+
model: 'qwen3:4b',
|
|
366
|
+
providers: [{ type: 'ollama' }],
|
|
367
|
+
auditor: new ConsoleAuditor('[LLM]'),
|
|
368
|
+
});
|
|
369
|
+
// [LLM] REQUEST [ollama] (qwen3:4b) →
|
|
370
|
+
// [LLM] RESPONSE [ollama] (qwen3:4b) 1200ms 68 tokens
|
|
371
|
+
|
|
372
|
+
// Buffered for custom sinks (OpenTelemetry, DB, etc.)
|
|
373
|
+
const auditor = new BufferedAuditor({
|
|
374
|
+
maxBufferSize: 100,
|
|
375
|
+
onFlush: async (events) => {
|
|
376
|
+
await sendToOpenTelemetry(events);
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### MCP Integration
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { AIModel, MCPToolBridge } from 'universal-llm-client';
|
|
385
|
+
|
|
386
|
+
const model = new AIModel({
|
|
387
|
+
model: 'qwen3:4b',
|
|
388
|
+
providers: [{ type: 'ollama' }],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const mcp = new MCPToolBridge({
|
|
392
|
+
servers: {
|
|
393
|
+
filesystem: {
|
|
394
|
+
command: 'npx',
|
|
395
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', './'],
|
|
396
|
+
},
|
|
397
|
+
weather: {
|
|
398
|
+
url: 'https://mcp.example.com/weather',
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await mcp.connect();
|
|
404
|
+
await mcp.registerTools(model);
|
|
405
|
+
|
|
406
|
+
// MCP tools are now callable via chatWithTools
|
|
407
|
+
const response = await model.chatWithTools([
|
|
408
|
+
{ role: 'user', content: 'List files in the current directory' },
|
|
409
|
+
]);
|
|
410
|
+
|
|
411
|
+
await mcp.disconnect();
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Stream Decoders
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import { AIModel, createDecoder } from 'universal-llm-client';
|
|
418
|
+
|
|
419
|
+
// Passthrough — raw text, no parsing
|
|
420
|
+
// Standard Chat — text + native reasoning + tool calls
|
|
421
|
+
// Interleaved Reasoning — parses <think> and <progress> tags from text streams
|
|
422
|
+
|
|
423
|
+
const decoder = createDecoder('interleaved-reasoning', (event) => {
|
|
424
|
+
switch (event.type) {
|
|
425
|
+
case 'text': console.log(event.content); break;
|
|
426
|
+
case 'thinking': console.log('[think]', event.content); break;
|
|
427
|
+
case 'progress': console.log('[progress]', event.content); break;
|
|
428
|
+
case 'tool_call': console.log('[tool]', event.calls); break;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
decoder.push('<think>Let me analyze this</think>The answer is 42');
|
|
433
|
+
decoder.flush();
|
|
434
|
+
|
|
435
|
+
console.log(decoder.getCleanContent()); // "The answer is 42"
|
|
436
|
+
console.log(decoder.getReasoning()); // "Let me analyze this"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## API Reference
|
|
442
|
+
|
|
443
|
+
### `AIModel`
|
|
444
|
+
|
|
445
|
+
The universal client. One class, multiple backends.
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
new AIModel(config: AIModelConfig)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Config:**
|
|
452
|
+
|
|
453
|
+
| Property | Type | Default | Description |
|
|
454
|
+
|---|---|---|---|
|
|
455
|
+
| `model` | `string` | — | Model name (e.g., `'gemini-2.5-flash'`) |
|
|
456
|
+
| `providers` | `ProviderConfig[]` | — | Ordered list of provider backends |
|
|
457
|
+
| `retries` | `number` | `2` | Retries per provider before failover |
|
|
458
|
+
| `timeout` | `number` | `30000` | Request timeout in ms |
|
|
459
|
+
| `auditor` | `Auditor` | `NoopAuditor` | Observability sink |
|
|
460
|
+
| `thinking` | `boolean` | `false` | Enable model thinking/reasoning |
|
|
461
|
+
| `debug` | `boolean` | `false` | Debug logging |
|
|
462
|
+
| `defaultParameters` | `object` | — | Default parameters for all requests |
|
|
463
|
+
|
|
464
|
+
**Provider Config:**
|
|
465
|
+
|
|
466
|
+
| Property | Type | Description |
|
|
467
|
+
|---|---|---|
|
|
468
|
+
| `type` | `string` | `'ollama'`, `'openai'`, `'google'`, `'vertex'`, `'llamacpp'`, `'anthropic'` |
|
|
469
|
+
| `url` | `string` | Provider URL (has sensible defaults) |
|
|
470
|
+
| `apiKey` | `string` | API key or Bearer token |
|
|
471
|
+
| `priority` | `number` | Lower = tried first (defaults to array index) |
|
|
472
|
+
| `model` | `string` | Override model name for this provider |
|
|
473
|
+
| `region` | `string` | Vertex AI region (e.g., `'us-central1'`) |
|
|
474
|
+
| `apiVersion` | `string` | API version (e.g., `'v1beta'`) |
|
|
475
|
+
| `headers` | `Record<string,string>` | Extra headers merged into requests — OpenAI-compatible & Ollama (Azure `api-key`, gateways) |
|
|
476
|
+
| `queryParams` | `Record<string,string>` | Query params appended to URLs — OpenAI-compatible only (e.g. Azure `api-version`) |
|
|
477
|
+
| `authHeader` | `string` | Header name for the key — OpenAI-compatible & Ollama (e.g. `'api-key'`) |
|
|
478
|
+
| `authPrefix` | `string` | Prefix before the key value — OpenAI-compatible & Ollama (e.g. `''` for api-key style) |
|
|
479
|
+
| `apiBasePath` | `string` | OpenAI-compatible only: override or disable the `/v1` suffix (use `''` for full Azure deployment URLs) |
|
|
480
|
+
|
|
481
|
+
**Methods:**
|
|
482
|
+
|
|
483
|
+
| Method | Returns | Description |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| `chat(messages, options?)` | `Promise<LLMChatResponse>` | Send chat request |
|
|
486
|
+
| `chatWithTools(messages, options?)` | `Promise<LLMChatResponse>` | Chat with autonomous tool execution |
|
|
487
|
+
| `chatStream(messages, options?)` | `AsyncGenerator<DecodedEvent>` | Stream chat response |
|
|
488
|
+
| `generateStructured(schema, messages, options?)` | `Promise<T>` | Generate typed JSON validated against Zod schema |
|
|
489
|
+
| `tryParseStructured(schema, messages, options?)` | `Promise<StructuredOutputResult<T>>` | Non-throwing variant returning result object |
|
|
490
|
+
| `generateStructuredStream(schema, messages, options?)` | `AsyncGenerator<T, T>` | Stream partial validated objects as JSON generates |
|
|
491
|
+
| `embed(text)` | `Promise<number[]>` | Generate single embedding |
|
|
492
|
+
| `embedArray(texts)` | `Promise<number[][]>` | Generate batch embeddings |
|
|
493
|
+
| `registerTool(name, desc, params, handler)` | `void` | Register a callable tool |
|
|
494
|
+
| `registerTools(tools)` | `void` | Register multiple tools |
|
|
495
|
+
| `getModels()` | `Promise<string[]>` | List available models |
|
|
496
|
+
| `getModelInfo()` | `Promise<ModelMetadata>` | Get model metadata |
|
|
497
|
+
| `getProviderStatus()` | `ProviderStatus[]` | Check provider health |
|
|
498
|
+
| `setModel(name)` | `void` | Switch model at runtime |
|
|
499
|
+
| `dispose()` | `Promise<void>` | Clean shutdown |
|
|
500
|
+
|
|
501
|
+
### Structured Output
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
import { z } from 'zod';
|
|
505
|
+
|
|
506
|
+
// Define your schema
|
|
507
|
+
const UserSchema = z.object({
|
|
508
|
+
name: z.string(),
|
|
509
|
+
age: z.number(),
|
|
510
|
+
email: z.string().email(),
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Generate typed JSON
|
|
514
|
+
const user = await model.generateStructured(UserSchema, messages);
|
|
515
|
+
// TypeScript infers: { name: string; age: number; email: string }
|
|
516
|
+
|
|
517
|
+
// Non-throwing variant
|
|
518
|
+
const result = await model.tryParseStructured(UserSchema, messages);
|
|
519
|
+
if (result.ok) {
|
|
520
|
+
console.log(result.value.name); // Fully typed
|
|
521
|
+
} else {
|
|
522
|
+
console.log(result.error.message);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Stream partial objects
|
|
526
|
+
for await (const partial of model.generateStructuredStream(UserSchema, messages)) {
|
|
527
|
+
console.log(partial); // Partial validated objects
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Separate module import (tree-shaking):**
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
import {
|
|
535
|
+
StructuredOutputError,
|
|
536
|
+
type StructuredOutputResult,
|
|
537
|
+
parseStructured,
|
|
538
|
+
tryParseStructured,
|
|
539
|
+
zodToJsonSchema,
|
|
540
|
+
} from 'universal-llm-client/structured-output';
|
|
541
|
+
|
|
542
|
+
// Use without importing the full client
|
|
543
|
+
const schema = z.object({ name: z.string() });
|
|
544
|
+
const jsonSchema = zodToJsonSchema(schema);
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### `ToolBuilder` / `ToolExecutor`
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import { ToolBuilder, ToolExecutor } from 'universal-llm-client';
|
|
551
|
+
|
|
552
|
+
// Fluent builder
|
|
553
|
+
const tool = new ToolBuilder('search')
|
|
554
|
+
.description('Search the web')
|
|
555
|
+
.addParameter('query', 'string', 'Search query', true)
|
|
556
|
+
.addParameter('limit', 'number', 'Max results', false)
|
|
557
|
+
.build();
|
|
558
|
+
|
|
559
|
+
// Execution wrappers
|
|
560
|
+
const safeHandler = ToolExecutor.compose(
|
|
561
|
+
myHandler,
|
|
562
|
+
h => ToolExecutor.withTimeout(h, 5000),
|
|
563
|
+
h => ToolExecutor.safe(h),
|
|
564
|
+
h => ToolExecutor.withValidation(h, ['query']),
|
|
565
|
+
);
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Auditor Interface
|
|
569
|
+
|
|
570
|
+
Implement custom observability by providing an `Auditor`:
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
interface Auditor {
|
|
574
|
+
record(event: AuditEvent): void;
|
|
575
|
+
flush?(): Promise<void>;
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**Built-in implementations:**
|
|
580
|
+
- `NoopAuditor` — Zero overhead (default)
|
|
581
|
+
- `ConsoleAuditor` — Structured console logging
|
|
582
|
+
- `BufferedAuditor` — Collects events for custom sinks
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Architecture
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
universal-llm-client
|
|
590
|
+
├── AIModel ← Public API (the only class you import)
|
|
591
|
+
├── Router ← Internal failover engine
|
|
592
|
+
├── BaseLLMClient ← Abstract client with tool execution
|
|
593
|
+
├── Providers
|
|
594
|
+
│ ├── OllamaClient
|
|
595
|
+
│ ├── OpenAICompatibleClient (OpenAI, OpenRouter, Groq, LM Studio, vLLM, LlamaCpp)
|
|
596
|
+
│ └── GoogleClient (AI Studio + Vertex AI)
|
|
597
|
+
├── StreamDecoder ← Pluggable reasoning strategies
|
|
598
|
+
├── Auditor ← Observability interface
|
|
599
|
+
├── MCPToolBridge ← MCP server integration
|
|
600
|
+
└── HTTP Utilities ← Universal fetch-based transport
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Design Principles
|
|
604
|
+
|
|
605
|
+
1. **Single import** — `AIModel` is the only class users need
|
|
606
|
+
2. **Provider agnostic** — Same code works with any backend
|
|
607
|
+
3. **Transparent failover** — Health tracking and cooldowns happen behind the scenes
|
|
608
|
+
4. **Zero dependencies** — Core library depends only on native `fetch`
|
|
609
|
+
5. **Agent-ready** — Stateless, composable instances designed as foundation for agent frameworks
|
|
610
|
+
6. **Observable** — Every request, response, tool call, retry, and failover is auditable
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Runtime Support
|
|
615
|
+
|
|
616
|
+
| Runtime | Version | Status |
|
|
617
|
+
|---|---|---|
|
|
618
|
+
| **Node.js** | 22+ | ✅ Full support |
|
|
619
|
+
| **Bun** | 1.0+ | ✅ Full support |
|
|
620
|
+
| **Deno** | 2.0+ | ✅ Full support |
|
|
621
|
+
| **Browsers** | Modern | ✅ No stdio MCP, HTTP transport only |
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## For Agent Framework Authors
|
|
626
|
+
|
|
627
|
+
`AIModel` is designed as the transport layer for agentic systems:
|
|
628
|
+
|
|
629
|
+
- **Stateless** — No conversation history stored. Your framework manages memory
|
|
630
|
+
- **Composable** — Create separate instances for chat, embeddings, vision
|
|
631
|
+
- **Tool tracing** — `chatWithTools()` returns full execution trace
|
|
632
|
+
- **Context budget** — `getModelInfo()` exposes `contextLength`
|
|
633
|
+
- **Auditor as system bus** — Inject custom sinks for cost tracking, behavioral scoring
|
|
634
|
+
- **StreamDecoder as UI bridge** — Select decoder strategy per-call
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## License
|
|
639
|
+
|
|
640
|
+
MIT
|