@aeye/ai 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.
Files changed (54) hide show
  1. package/README.md +1324 -0
  2. package/dist/ai.d.ts +204 -0
  3. package/dist/ai.d.ts.map +1 -0
  4. package/dist/ai.js +837 -0
  5. package/dist/ai.js.map +1 -0
  6. package/dist/apis/base.d.ts +181 -0
  7. package/dist/apis/base.d.ts.map +1 -0
  8. package/dist/apis/base.js +359 -0
  9. package/dist/apis/base.js.map +1 -0
  10. package/dist/apis/chat.d.ts +82 -0
  11. package/dist/apis/chat.d.ts.map +1 -0
  12. package/dist/apis/chat.js +214 -0
  13. package/dist/apis/chat.js.map +1 -0
  14. package/dist/apis/embed.d.ts +27 -0
  15. package/dist/apis/embed.d.ts.map +1 -0
  16. package/dist/apis/embed.js +70 -0
  17. package/dist/apis/embed.js.map +1 -0
  18. package/dist/apis/image.d.ts +77 -0
  19. package/dist/apis/image.d.ts.map +1 -0
  20. package/dist/apis/image.js +313 -0
  21. package/dist/apis/image.js.map +1 -0
  22. package/dist/apis/models.d.ts +108 -0
  23. package/dist/apis/models.d.ts.map +1 -0
  24. package/dist/apis/models.js +118 -0
  25. package/dist/apis/models.js.map +1 -0
  26. package/dist/apis/speech.d.ts +27 -0
  27. package/dist/apis/speech.d.ts.map +1 -0
  28. package/dist/apis/speech.js +79 -0
  29. package/dist/apis/speech.js.map +1 -0
  30. package/dist/apis/transcribe.d.ts +26 -0
  31. package/dist/apis/transcribe.d.ts.map +1 -0
  32. package/dist/apis/transcribe.js +103 -0
  33. package/dist/apis/transcribe.js.map +1 -0
  34. package/dist/common.d.ts +10 -0
  35. package/dist/common.d.ts.map +1 -0
  36. package/dist/common.js +17 -0
  37. package/dist/common.js.map +1 -0
  38. package/dist/index.d.ts +17 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +17 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/modelDetection.d.ts +31 -0
  43. package/dist/modelDetection.d.ts.map +1 -0
  44. package/dist/modelDetection.js +78 -0
  45. package/dist/modelDetection.js.map +1 -0
  46. package/dist/registry.d.ts +150 -0
  47. package/dist/registry.d.ts.map +1 -0
  48. package/dist/registry.js +639 -0
  49. package/dist/registry.js.map +1 -0
  50. package/dist/types.d.ts +767 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +8 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +53 -0
package/dist/ai.js ADDED
@@ -0,0 +1,837 @@
1
+ /**
2
+ * AI Class - Main Entry Point
3
+ *
4
+ * Instance-based AI library similar to OpenAI's SDK.
5
+ * Provides type-safe, scoped access to AI capabilities with hooks and context injection.
6
+ */
7
+ import { accumulateUsage, Agent, Prompt, resolveFn, Tool } from '@aeye/core';
8
+ import { ChatAPI } from './apis/chat';
9
+ import { EmbedAPI } from './apis/embed';
10
+ import { ImageAPI } from './apis/image';
11
+ import { ModelsAPI } from './apis/models';
12
+ import { SpeechAPI } from './apis/speech';
13
+ import { TranscribeAPI } from './apis/transcribe';
14
+ import { ModelRegistry } from './registry';
15
+ /**
16
+ * AI Class
17
+ *
18
+ * @template T - AIBaseTypes container with all type information
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const ai = AI.with<AppContext, AppMetadata>()
23
+ * .providers({ openai, openrouter })
24
+ * .create({
25
+ * defaultContext: { baseURL: 'https://api.example.com' },
26
+ * providedContext: async (ctx) => ({ db, user }),
27
+ * hooks: {
28
+ * beforeRequest: async (ctx, request, selected, estimatedTokens, estimatedCost) => {
29
+ * // Check budget
30
+ * },
31
+ * afterRequest: async (ctx, request, response, responseComplete, selected, usage, cost) => {
32
+ * // Track usage
33
+ * }
34
+ * }
35
+ * });
36
+ *
37
+ * // Use the API
38
+ * const response = await ai.chat.get([{ role: 'user', content: 'Hi' }]);
39
+ * ```
40
+ */
41
+ export class AI {
42
+ // Configuration
43
+ config;
44
+ registry;
45
+ // API Surfaces
46
+ chat;
47
+ image;
48
+ speech;
49
+ transcribe;
50
+ embed;
51
+ models;
52
+ // Direct Access
53
+ providers;
54
+ components = [];
55
+ hooks;
56
+ // Cumulative tracking for statistics
57
+ cumulativeCost = 0;
58
+ cumulativeLatency = 0;
59
+ cumulativeRequestCount = 0;
60
+ // Computed Values
61
+ tokens;
62
+ /**
63
+ * Static builder for creating AI instances with type inference
64
+ */
65
+ static with() {
66
+ return {
67
+ providers: (providers) => {
68
+ return {
69
+ create: (config) => {
70
+ return new AI({
71
+ providers,
72
+ ...config,
73
+ });
74
+ },
75
+ };
76
+ },
77
+ };
78
+ }
79
+ constructor(config) {
80
+ this.config = config;
81
+ this.hooks = {};
82
+ // Prepare model sources
83
+ const modelSources = [...(config.modelSources || [])];
84
+ // Register given models as a source so they are not lost on refresh
85
+ if (config.models) {
86
+ modelSources.unshift({
87
+ name: 'config',
88
+ fetchModels: async () => config.models,
89
+ });
90
+ }
91
+ // Initialize registry with model sources
92
+ this.registry = new ModelRegistry(config.providers, config.modelOverrides || [], config.defaultCostPerMillionTokens || 5.0, modelSources, config.defaultWeights, config.weightProfiles || {});
93
+ // Push in given models without fetching
94
+ if (config.models) {
95
+ this.registry.registerModels(config.models);
96
+ }
97
+ // Register model handlers
98
+ if (config.modelHandlers) {
99
+ for (const handler of config.modelHandlers) {
100
+ this.registry.registerHandler(handler);
101
+ }
102
+ }
103
+ // Initialize API surfaces
104
+ this.chat = new ChatAPI(this);
105
+ this.image = new ImageAPI(this);
106
+ this.speech = new SpeechAPI(this);
107
+ this.transcribe = new TranscribeAPI(this);
108
+ this.embed = new EmbedAPI(this);
109
+ this.models = new ModelsAPI(this.registry);
110
+ // Direct access to providers
111
+ this.providers = config.providers;
112
+ // Token calculation helpers
113
+ this.tokens = {
114
+ text: {
115
+ divisor: this.config.tokens?.textDivisor || 4,
116
+ base64Divisor: this.config.tokens?.textBase64Divisor || 3,
117
+ fallback: this.config.tokens?.textFallback || 1000,
118
+ max: this.config.tokens?.textMax,
119
+ },
120
+ image: {
121
+ divisor: this.config.tokens?.imageDivisor || 1125,
122
+ base64Divisor: this.config.tokens?.imageBase64Divisor || 1500,
123
+ fallback: this.config.tokens?.imageFallback || 1360,
124
+ max: this.config.tokens?.imageMax || 1360,
125
+ },
126
+ file: {
127
+ divisor: this.config.tokens?.fileDivisor || 3,
128
+ base64Divisor: this.config.tokens?.fileBase64Divisor || 4,
129
+ fallback: this.config.tokens?.fileFallback || 1000,
130
+ max: this.config.tokens?.fileMax,
131
+ },
132
+ audio: {
133
+ divisor: this.config.tokens?.audioDivisor || 3,
134
+ base64Divisor: this.config.tokens?.audioBase64Divisor || 4,
135
+ fallback: this.config.tokens?.audioFallback || 200,
136
+ max: this.config.tokens?.audioMax,
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * Sets the hooks for this AI instance and returns the instance.
142
+ *
143
+ * @param hooks - Hooks to set
144
+ * @returns this
145
+ */
146
+ withHooks(hooks) {
147
+ this.hooks = hooks;
148
+ return this;
149
+ }
150
+ /**
151
+ * Build full context from required context
152
+ *
153
+ * Merges defaults → provided → required in that order
154
+ *
155
+ * @param requiredCtx - Required context provided by caller
156
+ * @returns Full AIContext with all fields populated
157
+ */
158
+ async buildContext(requiredCtx) {
159
+ const { defaultContext, providedContext } = this.config;
160
+ const ctx = {
161
+ ...defaultContext,
162
+ ...(await providedContext?.({
163
+ ...defaultContext,
164
+ ...requiredCtx,
165
+ })),
166
+ ...requiredCtx,
167
+ ai: this,
168
+ };
169
+ return ctx;
170
+ }
171
+ /**
172
+ * Build core context with executor and streamer injected from ChatAPI
173
+ *
174
+ * This is used by Prompts to get stream/execute functions in the context.
175
+ *
176
+ * @param requiredCtx - Required context provided by caller
177
+ * @returns Full context with stream and execute functions
178
+ */
179
+ async buildCoreContext(requiredCtx) {
180
+ const ctx = await this.buildContext(requiredCtx);
181
+ if (ctx.executor && ctx.streamer && ctx.estimateUsage) {
182
+ return ctx;
183
+ }
184
+ // Get executor and streamer from ChatAPI
185
+ const execute = this.chat.createExecutor();
186
+ const stream = this.chat.createStreamer();
187
+ const estimateUsage = this.estimateMessageUsage.bind(this);
188
+ return {
189
+ ...ctx,
190
+ execute,
191
+ stream,
192
+ estimateUsage,
193
+ };
194
+ }
195
+ /**
196
+ * Build full metadata from required metadata
197
+ *
198
+ * Merges defaults → provided → required in that order
199
+ *
200
+ * @param requiredMetadata - Required metadata provided by caller
201
+ * @returns Full AIMetadata with all fields populated
202
+ */
203
+ async buildMetadata(requiredMetadata) {
204
+ const { defaultMetadata, providedMetadata } = this.config;
205
+ const merged = this.mergeMetadata(defaultMetadata, requiredMetadata);
206
+ return providedMetadata
207
+ ? this.mergeMetadata(defaultMetadata, await providedMetadata(merged), requiredMetadata)
208
+ : merged;
209
+ }
210
+ /**
211
+ * Merges multiple metadata objects into one. Some properties may be
212
+ * merged in a special way depending on their type. The metadata objects
213
+ * are merged in the order they are provided, with later objects
214
+ * overriding earlier ones.
215
+ *
216
+ * @param metadatas
217
+ * @returns
218
+ */
219
+ mergeMetadata(...metadatas) {
220
+ const mergeArray = (a, b) => {
221
+ return a?.length && b?.length
222
+ ? Array.from(new Set([...a, ...b]))
223
+ : a?.length
224
+ ? a
225
+ : b?.length
226
+ ? b
227
+ : [];
228
+ };
229
+ const removeArray = (a, b) => {
230
+ if (a?.length && b?.length) {
231
+ return a.filter(item => !b.includes(item));
232
+ }
233
+ else {
234
+ return a?.length ? a : undefined;
235
+ }
236
+ };
237
+ const mergeMin = (a, b) => {
238
+ return (a !== undefined && b !== undefined)
239
+ ? Math.min(a, b)
240
+ : a !== undefined ? a : b;
241
+ };
242
+ const mergeMax = (a, b) => {
243
+ return (a !== undefined && b !== undefined)
244
+ ? Math.max(a, b)
245
+ : a !== undefined ? a : b;
246
+ };
247
+ const mergeAverage = (a, b) => {
248
+ return (a !== undefined && b !== undefined)
249
+ ? (a + b) / 2
250
+ : a !== undefined ? a : b;
251
+ };
252
+ const mergeBudget = (a, b) => {
253
+ if (!a)
254
+ return b;
255
+ if (!b)
256
+ return a;
257
+ const merged = { ...a };
258
+ for (const key in b) {
259
+ switch (key) {
260
+ case 'maxCostPerRequest':
261
+ merged[key] = mergeMin(a[key], b[key]);
262
+ break;
263
+ case 'maxCostPerMillionTokens':
264
+ merged[key] = mergeMax(a[key], b[key]);
265
+ break;
266
+ default:
267
+ merged[key] = b[key];
268
+ }
269
+ }
270
+ return merged;
271
+ };
272
+ const mergeWeights = (a, b) => {
273
+ if (!a)
274
+ return b;
275
+ if (!b)
276
+ return a;
277
+ const merged = { ...a };
278
+ for (const key in b) {
279
+ switch (key) {
280
+ case 'cost':
281
+ case 'speed':
282
+ case 'accuracy':
283
+ case 'contextWindow':
284
+ merged[key] = mergeAverage(a[key], b[key]);
285
+ break;
286
+ default:
287
+ merged[key] = b[key];
288
+ break;
289
+ }
290
+ }
291
+ return merged;
292
+ };
293
+ const mergeProviders = (a, b) => {
294
+ if (!a)
295
+ return b;
296
+ if (!b)
297
+ return a;
298
+ const merged = { ...a };
299
+ for (const key in b) {
300
+ switch (key) {
301
+ case 'preferred':
302
+ merged[key] = mergeArray(a[key], b[key]);
303
+ break;
304
+ case 'excluded':
305
+ merged[key] = mergeArray(a[key], b[key]);
306
+ break;
307
+ default:
308
+ merged[key] = b[key];
309
+ break;
310
+ }
311
+ }
312
+ merged.allow = removeArray(merged.allow, merged.deny);
313
+ return merged;
314
+ };
315
+ const merged = (metadatas[0] || {});
316
+ for (let i = 1; i < metadatas.length; i++) {
317
+ const metadata = metadatas[i];
318
+ for (const key in metadata) {
319
+ if (metadata[key] === undefined) {
320
+ continue;
321
+ }
322
+ switch (key) {
323
+ case 'required':
324
+ case 'optional':
325
+ merged[key] = mergeArray(merged[key], metadata[key]);
326
+ break;
327
+ case 'budget':
328
+ merged[key] = mergeBudget(merged[key], metadata[key]);
329
+ break;
330
+ case 'weights':
331
+ merged[key] = mergeWeights(merged[key], metadata[key]);
332
+ break;
333
+ case 'providers':
334
+ merged[key] = mergeProviders(merged[key], metadata[key]);
335
+ break;
336
+ case 'minContextWindow':
337
+ merged[key] = mergeMax(merged[key], metadata[key]);
338
+ break;
339
+ default:
340
+ merged[key] = metadata[key];
341
+ break;
342
+ }
343
+ }
344
+ }
345
+ return merged;
346
+ }
347
+ /**
348
+ * Select a model based on metadata (used by API classes)
349
+ * @internal
350
+ */
351
+ selectModel(metadata) {
352
+ return this.registry.selectModel(metadata);
353
+ }
354
+ /**
355
+ * Estimate usage for a message (used by API classes and Prompts)
356
+ *
357
+ * @param message - Message to estimate usage for
358
+ * @returns Usage statistics structured to match ModelPricing
359
+ */
360
+ estimateMessageUsage(message) {
361
+ const usage = {};
362
+ // If message has pre-calculated tokens, return as text input
363
+ if (message.tokens !== undefined) {
364
+ return {
365
+ text: {
366
+ input: message.tokens
367
+ }
368
+ };
369
+ }
370
+ let textTokens = 0;
371
+ let imageTokens = 0;
372
+ let audioTokens = 0;
373
+ let fileTokens = 0;
374
+ let addText = (text) => {
375
+ if (text) {
376
+ textTokens += Math.ceil(text.length / this.tokens.text.divisor);
377
+ }
378
+ };
379
+ addText(message.role);
380
+ addText(message.name || '');
381
+ addText(message.refusal || '');
382
+ addText(message.toolCallId || '');
383
+ if (message.toolCalls?.length) {
384
+ addText(JSON.stringify(message.toolCalls));
385
+ }
386
+ if (typeof message.content === 'string') {
387
+ addText(message.content);
388
+ }
389
+ else if (Array.isArray(message.content)) {
390
+ for (const content of message.content) {
391
+ addText(content.type);
392
+ const metrics = this.tokens[content.type || 'text'];
393
+ let contentTokens = 0;
394
+ if (typeof content.content === 'string') {
395
+ if (content.content.startsWith('data:')) {
396
+ // Base64 data
397
+ contentTokens = metrics.max
398
+ ? Math.min(metrics.max, Math.ceil(content.content.length / metrics.base64Divisor))
399
+ : Math.ceil(content.content.length / metrics.base64Divisor);
400
+ }
401
+ else if (content.content.startsWith('http') && content.type !== 'text') {
402
+ // URL - use fallback estimate
403
+ contentTokens = metrics.fallback;
404
+ }
405
+ else {
406
+ addText(content.content);
407
+ continue; // Already added as text
408
+ }
409
+ }
410
+ else if (content.content instanceof Uint8Array) {
411
+ // Binary data
412
+ contentTokens = Math.ceil(content.content.byteLength / metrics.divisor);
413
+ }
414
+ else if (content.content instanceof Blob) {
415
+ // Blob data
416
+ contentTokens = Math.ceil(content.content.size / metrics.divisor);
417
+ }
418
+ else {
419
+ // URL or Stream - use fallback estimate
420
+ contentTokens = metrics.fallback;
421
+ }
422
+ // Categorize tokens by content type
423
+ switch (content.type) {
424
+ case 'image':
425
+ imageTokens += contentTokens;
426
+ break;
427
+ case 'audio':
428
+ audioTokens += contentTokens;
429
+ break;
430
+ case 'file':
431
+ fileTokens += contentTokens;
432
+ break;
433
+ default:
434
+ textTokens += contentTokens;
435
+ }
436
+ }
437
+ }
438
+ // Build structured usage object
439
+ if (textTokens > 0) {
440
+ usage.text = { input: textTokens };
441
+ }
442
+ if (imageTokens > 0) {
443
+ usage.image = { input: imageTokens };
444
+ }
445
+ if (audioTokens > 0) {
446
+ usage.audio = { input: audioTokens };
447
+ }
448
+ // fileTokens are treated as text for now
449
+ if (fileTokens > 0) {
450
+ if (!usage.text) {
451
+ usage.text = {};
452
+ }
453
+ usage.text.input = (usage.text.input || 0) + fileTokens;
454
+ }
455
+ return usage;
456
+ }
457
+ /**
458
+ * Estimate usage for a request (used by API classes)
459
+ * @internal
460
+ */
461
+ estimateRequestUsage(request) {
462
+ const usage = {};
463
+ for (const msg of request.messages) {
464
+ const msgUsage = this.estimateMessageUsage(msg);
465
+ accumulateUsage(usage, msgUsage);
466
+ }
467
+ return usage;
468
+ }
469
+ /**
470
+ * Calculate cost for a request (used by API classes)
471
+ * Matches the structure of Usage to ModelPricing for accurate cost calculation.
472
+ * @internal
473
+ */
474
+ calculateCost(model, usage) {
475
+ // Apply overrides if they match
476
+ let pricing = model.pricing;
477
+ if (this.config.modelOverrides) {
478
+ for (const override of this.config.modelOverrides) {
479
+ if (this.matchesOverride(model, override)) {
480
+ pricing = { ...pricing, ...override.overrides.pricing };
481
+ }
482
+ }
483
+ }
484
+ let cost = 0;
485
+ // Text usage costs
486
+ if (usage.text && pricing.text) {
487
+ if (usage.text.input && pricing.text.input) {
488
+ cost += (usage.text.input * pricing.text.input) / 1_000_000;
489
+ }
490
+ if (usage.text.output && pricing.text.output) {
491
+ cost += (usage.text.output * pricing.text.output) / 1_000_000;
492
+ }
493
+ if (usage.text.cached) {
494
+ // Use cached pricing if available, otherwise fall back to input pricing
495
+ const cachedPrice = pricing.text.cached ?? pricing.text.input;
496
+ if (cachedPrice) {
497
+ cost += (usage.text.cached * cachedPrice) / 1_000_000;
498
+ }
499
+ }
500
+ }
501
+ // Audio usage costs
502
+ if (usage.audio && pricing.audio) {
503
+ // Time-based pricing (per second)
504
+ if (usage.audio.seconds && pricing.audio.perSecond) {
505
+ cost += usage.audio.seconds * pricing.audio.perSecond;
506
+ }
507
+ // Token-based pricing
508
+ if (usage.audio.input && pricing.audio.input) {
509
+ cost += (usage.audio.input * pricing.audio.input) / 1_000_000;
510
+ }
511
+ if (usage.audio.output && pricing.audio.output) {
512
+ cost += (usage.audio.output * pricing.audio.output) / 1_000_000;
513
+ }
514
+ }
515
+ // Image usage costs
516
+ if (usage.image && pricing.image) {
517
+ // Input image tokens (for image analysis)
518
+ if (usage.image.input && pricing.image.input) {
519
+ cost += (usage.image.input * pricing.image.input) / 1_000_000;
520
+ }
521
+ // Output image costs (for image generation)
522
+ if (usage.image.output && pricing.image.output) {
523
+ for (const usageOutput of usage.image.output) {
524
+ // Find matching price for this quality/size
525
+ const priceEntry = pricing.image.output.find(p => p.quality === usageOutput.quality);
526
+ if (priceEntry) {
527
+ const sizeEntry = priceEntry.sizes.find(s => s.width === usageOutput.size.width && s.height === usageOutput.size.height);
528
+ if (sizeEntry) {
529
+ cost += sizeEntry.cost * usageOutput.count;
530
+ }
531
+ }
532
+ }
533
+ }
534
+ }
535
+ // Reasoning usage costs
536
+ if (usage.reasoning && pricing.reasoning) {
537
+ if (usage.reasoning.input && pricing.reasoning.input) {
538
+ cost += (usage.reasoning.input * pricing.reasoning.input) / 1_000_000;
539
+ }
540
+ if (usage.reasoning.output && pricing.reasoning.output) {
541
+ cost += (usage.reasoning.output * pricing.reasoning.output) / 1_000_000;
542
+ }
543
+ if (usage.reasoning.cached) {
544
+ // Use cached pricing if available, otherwise fall back to input pricing
545
+ const cachedPrice = pricing.reasoning.cached ?? pricing.reasoning.input;
546
+ if (cachedPrice) {
547
+ cost += (usage.reasoning.cached * cachedPrice) / 1_000_000;
548
+ }
549
+ }
550
+ }
551
+ // Embeddings usage costs
552
+ if (usage.embeddings && pricing.embeddings) {
553
+ if (usage.embeddings.tokens && pricing.embeddings.cost) {
554
+ cost += (usage.embeddings.tokens * pricing.embeddings.cost) / 1_000_000;
555
+ }
556
+ }
557
+ // Fixed cost per request
558
+ if (pricing.perRequest) {
559
+ cost += pricing.perRequest;
560
+ }
561
+ return cost;
562
+ }
563
+ /**
564
+ * Determines whether a given override matches a model.
565
+ *
566
+ * @param model - Model information
567
+ * @param override - Model override to check
568
+ * @returns
569
+ */
570
+ matchesOverride(model, override) {
571
+ // Provider match
572
+ if (override.provider && override.provider !== model.provider) {
573
+ return false;
574
+ }
575
+ // Exact model ID match
576
+ if (override.modelId && override.modelId !== model.id) {
577
+ return false;
578
+ }
579
+ // Pattern match
580
+ if (override.modelPattern && !override.modelPattern.test(model.id)) {
581
+ return false;
582
+ }
583
+ return true;
584
+ }
585
+ /**
586
+ * Run a component with AI instance context.
587
+ *
588
+ * @param component - Component to run
589
+ * @param input - Input for the component
590
+ * @param ctx - Optional context
591
+ */
592
+ async run(component, input, ...[ctx]) {
593
+ const coreContext = await this.buildCoreContext(ctx || {});
594
+ if (component instanceof Prompt) {
595
+ coreContext.metadata = this.mergeMetadata(component.input.metadata, coreContext.metadata);
596
+ }
597
+ return await component.run(input, coreContext);
598
+ }
599
+ /**
600
+ * Create an enhanced prompt bound to this AI instance.
601
+ *
602
+ * @template TName - Name of the prompt.
603
+ * @template TInput - Input type for the prompt.
604
+ * @template TOutput - Output type for the prompt.
605
+ * @template TTools - Tools used by the prompt.
606
+ * @param options - Prompt configuration options.
607
+ * @returns - An enhanced prompt instance.
608
+ */
609
+ prompt(options) {
610
+ const { input, schema, config, reconfig, retool, validate, applicable, metadataFn, ...rest } = options;
611
+ const hydrateFn = (optionWithInjection, getOptionWithoutInjection) => {
612
+ return typeof optionWithInjection === 'function'
613
+ ? getOptionWithoutInjection(resolveFn(optionWithInjection))
614
+ : optionWithInjection;
615
+ };
616
+ const getContext = (ctxPartial) => {
617
+ return this.buildContext(ctxPartial);
618
+ };
619
+ const prompt = new Prompt({
620
+ ...rest,
621
+ input: hydrateFn(input, (r) => (async (input, ctxPartial) => {
622
+ return r(input, await getContext(ctxPartial));
623
+ })),
624
+ schema: hydrateFn(schema, (r) => (async (input, ctxPartial) => {
625
+ return r(input, await getContext(ctxPartial));
626
+ })),
627
+ config: hydrateFn(config, (r) => (async (input, ctxPartial) => {
628
+ return r(input, await getContext(ctxPartial));
629
+ })),
630
+ reconfig: hydrateFn(reconfig, (r) => (async (newConfig, ctxPartial) => {
631
+ return r(newConfig, await getContext(ctxPartial));
632
+ })),
633
+ retool: hydrateFn(retool, (r) => (async (newTools, ctxPartial) => {
634
+ return r(newTools, await getContext(ctxPartial));
635
+ })),
636
+ metadataFn: hydrateFn(metadataFn, (r) => (async (input, ctxPartial) => {
637
+ return r(input, await getContext(ctxPartial));
638
+ })),
639
+ validate: hydrateFn(validate, (r) => (async (output, ctxPartial) => {
640
+ return r(output, await getContext(ctxPartial));
641
+ })),
642
+ applicable: hydrateFn(applicable, (r) => (async (ctxPartial) => {
643
+ return r(await getContext(ctxPartial));
644
+ })),
645
+ });
646
+ // Wrap the prompt's stream method to inject executor/streamer via buildCoreContext
647
+ const ai = this;
648
+ // @ts-ignore
649
+ const originalStream = prompt.stream.bind(prompt);
650
+ prompt.stream = async function* (input, preferStream, toolsOnly,
651
+ // @ts-ignore
652
+ events, ctxRequired) {
653
+ // Build core context with executor/streamer
654
+ const coreContext = await ai.buildCoreContext(ctxRequired);
655
+ coreContext.metadata = ai.mergeMetadata(prompt.input.metadata, coreContext.metadata);
656
+ const stream = originalStream(input, preferStream, toolsOnly, events, coreContext);
657
+ let result = await stream.next();
658
+ while (!result.done) {
659
+ yield result.value;
660
+ result = await stream.next();
661
+ }
662
+ return result.value;
663
+ };
664
+ this.components.push(prompt);
665
+ return prompt;
666
+ }
667
+ /**
668
+ * Create an enhanced tool bound to this AI instance
669
+ */
670
+ tool(options) {
671
+ const { input, schema, call, validate, applicable, ...rest } = options;
672
+ const hydrateFn = (optionWithInjection, getOptionWithoutInjection) => {
673
+ return typeof optionWithInjection === 'function'
674
+ ? getOptionWithoutInjection(resolveFn(optionWithInjection))
675
+ : optionWithInjection;
676
+ };
677
+ const getContext = (ctxPartial) => {
678
+ return this.buildContext(ctxPartial);
679
+ };
680
+ const tool = new Tool({
681
+ ...rest,
682
+ instructionsFn: hydrateFn(rest.instructionsFn, (r) => (async (ctxPartial) => {
683
+ return r(await getContext(ctxPartial));
684
+ })),
685
+ descriptionFn: hydrateFn(rest.descriptionFn, (r) => (async (ctxPartial) => {
686
+ return r(await getContext(ctxPartial));
687
+ })),
688
+ input: hydrateFn(input, (r) => (async (ctxPartial) => {
689
+ return r(await getContext(ctxPartial));
690
+ })),
691
+ schema: hydrateFn(schema, (r) => (async (ctxPartial) => {
692
+ return r(await getContext(ctxPartial));
693
+ })),
694
+ call: hydrateFn(call, (r) => async (params, refs, ctxPartial) => {
695
+ return r(params, refs, await getContext(ctxPartial));
696
+ }),
697
+ validate: hydrateFn(validate, (r) => (async (params, ctxPartial) => {
698
+ return r(params, await getContext(ctxPartial));
699
+ })),
700
+ applicable: hydrateFn(applicable, (r) => (async (ctxPartial) => {
701
+ return r(await getContext(ctxPartial));
702
+ })),
703
+ });
704
+ this.components.push(tool);
705
+ return tool;
706
+ }
707
+ /**
708
+ * Create an enhanced agent bound to this AI instance
709
+ */
710
+ agent(options) {
711
+ const { call, applicable, ...rest } = options;
712
+ const hydrateFn = (optionWithInjection, getOptionWithoutInjection) => {
713
+ return typeof optionWithInjection === 'function'
714
+ ? getOptionWithoutInjection(resolveFn(optionWithInjection))
715
+ : optionWithInjection;
716
+ };
717
+ const getContext = (ctxPartial) => {
718
+ return this.buildContext(ctxPartial);
719
+ };
720
+ const agent = new Agent({
721
+ ...rest,
722
+ call: hydrateFn(call, (r) => (async (input, refs, ctxPartial) => {
723
+ return r(input, refs, await getContext(ctxPartial));
724
+ })),
725
+ applicable: hydrateFn(applicable, (r) => (async (ctxPartial) => {
726
+ return r(await getContext(ctxPartial));
727
+ })),
728
+ });
729
+ this.components.push(agent);
730
+ return agent;
731
+ }
732
+ /**
733
+ * Extend this AI instance with additional context type
734
+ *
735
+ * Creates a new AI instance that shares the base model registry but can have
736
+ * its own models, overrides, handlers, and extended context type.
737
+ *
738
+ * @example
739
+ * ```typescript
740
+ * const chatAI = ai.extend<{ chat: Chat, chatMessage: ChatMessage }>({
741
+ * defaultContext: { chat, chatMessage },
742
+ * modelOverrides: [...]
743
+ * });
744
+ * const chatAgent = chatAI.agent({...});
745
+ * ```
746
+ */
747
+ extend(config) {
748
+ return new AI({
749
+ // Base config
750
+ ...this.config,
751
+ // Override with new options
752
+ ...config,
753
+ // Merge objects additively
754
+ defaultContext: {
755
+ ...this.config.defaultContext,
756
+ ...config?.defaultContext,
757
+ },
758
+ defaultMetadata: this.mergeMetadata(this.config.defaultMetadata, config?.defaultMetadata),
759
+ providers: {
760
+ ...this.config.providers,
761
+ ...config?.providers,
762
+ },
763
+ // Merge arrays additively
764
+ models: [
765
+ ...(this.config.models || []),
766
+ ...(config?.models || []),
767
+ ],
768
+ modelOverrides: [
769
+ ...(this.config.modelOverrides || []),
770
+ ...(config?.modelOverrides || []),
771
+ ],
772
+ modelHandlers: [
773
+ ...(this.config.modelHandlers || []),
774
+ ...(config?.modelHandlers || []),
775
+ ],
776
+ // Token estimation numbers
777
+ tokens: {
778
+ ...this.config.tokens,
779
+ ...config?.tokens,
780
+ },
781
+ // Default scoring weights
782
+ defaultWeights: {
783
+ ...this.config.defaultWeights,
784
+ ...config?.defaultWeights,
785
+ },
786
+ // Weight profiles: merge
787
+ weightProfiles: {
788
+ ...this.config.weightProfiles,
789
+ ...config?.weightProfiles,
790
+ },
791
+ });
792
+ }
793
+ /**
794
+ * Track a request's cost and latency for statistics.
795
+ * Called internally by API classes after successful requests.
796
+ *
797
+ * @param cost - Cost of the request in dollars
798
+ * @param latency - Latency of the request in milliseconds
799
+ * @internal
800
+ */
801
+ trackRequest(cost, latency) {
802
+ this.cumulativeCost += cost;
803
+ this.cumulativeLatency += latency;
804
+ this.cumulativeRequestCount += 1;
805
+ }
806
+ /**
807
+ * Get library statistics
808
+ */
809
+ stats() {
810
+ const models = this.registry.listModels();
811
+ const modelsByProvider = {};
812
+ for (const model of models) {
813
+ modelsByProvider[model.provider] = (modelsByProvider[model.provider] || 0) + 1;
814
+ }
815
+ // Calculate aggregate metrics
816
+ let totalRequests = 0;
817
+ let successfulRequests = 0;
818
+ let failedRequests = 0;
819
+ for (const model of models) {
820
+ if (model.metrics) {
821
+ totalRequests += model.metrics.requestCount || 0;
822
+ successfulRequests += model.metrics.successCount || 0;
823
+ failedRequests += model.metrics.failureCount || 0;
824
+ }
825
+ }
826
+ return {
827
+ totalModels: models.length,
828
+ modelsByProvider,
829
+ totalRequests,
830
+ successfulRequests,
831
+ failedRequests,
832
+ averageCost: this.cumulativeRequestCount > 0 ? this.cumulativeCost / this.cumulativeRequestCount : 0,
833
+ averageLatency: this.cumulativeRequestCount > 0 ? this.cumulativeLatency / this.cumulativeRequestCount : 0,
834
+ };
835
+ }
836
+ }
837
+ //# sourceMappingURL=ai.js.map