@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.
- package/README.md +1324 -0
- package/dist/ai.d.ts +204 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +837 -0
- package/dist/ai.js.map +1 -0
- package/dist/apis/base.d.ts +181 -0
- package/dist/apis/base.d.ts.map +1 -0
- package/dist/apis/base.js +359 -0
- package/dist/apis/base.js.map +1 -0
- package/dist/apis/chat.d.ts +82 -0
- package/dist/apis/chat.d.ts.map +1 -0
- package/dist/apis/chat.js +214 -0
- package/dist/apis/chat.js.map +1 -0
- package/dist/apis/embed.d.ts +27 -0
- package/dist/apis/embed.d.ts.map +1 -0
- package/dist/apis/embed.js +70 -0
- package/dist/apis/embed.js.map +1 -0
- package/dist/apis/image.d.ts +77 -0
- package/dist/apis/image.d.ts.map +1 -0
- package/dist/apis/image.js +313 -0
- package/dist/apis/image.js.map +1 -0
- package/dist/apis/models.d.ts +108 -0
- package/dist/apis/models.d.ts.map +1 -0
- package/dist/apis/models.js +118 -0
- package/dist/apis/models.js.map +1 -0
- package/dist/apis/speech.d.ts +27 -0
- package/dist/apis/speech.d.ts.map +1 -0
- package/dist/apis/speech.js +79 -0
- package/dist/apis/speech.js.map +1 -0
- package/dist/apis/transcribe.d.ts +26 -0
- package/dist/apis/transcribe.d.ts.map +1 -0
- package/dist/apis/transcribe.js +103 -0
- package/dist/apis/transcribe.js.map +1 -0
- package/dist/common.d.ts +10 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +17 -0
- package/dist/common.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/modelDetection.d.ts +31 -0
- package/dist/modelDetection.d.ts.map +1 -0
- package/dist/modelDetection.js +78 -0
- package/dist/modelDetection.js.map +1 -0
- package/dist/registry.d.ts +150 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +639 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +767 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- 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
|