@elsium-ai/observe 0.1.6

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/dist/index.js ADDED
@@ -0,0 +1,1082 @@
1
+ // @bun
2
+ // ../core/src/errors.ts
3
+ class ElsiumError extends Error {
4
+ code;
5
+ provider;
6
+ model;
7
+ statusCode;
8
+ retryable;
9
+ retryAfterMs;
10
+ cause;
11
+ metadata;
12
+ constructor(details) {
13
+ super(details.message);
14
+ this.name = "ElsiumError";
15
+ this.code = details.code;
16
+ this.provider = details.provider;
17
+ this.model = details.model;
18
+ this.statusCode = details.statusCode;
19
+ this.retryable = details.retryable;
20
+ this.retryAfterMs = details.retryAfterMs;
21
+ this.cause = details.cause;
22
+ this.metadata = details.metadata;
23
+ }
24
+ toJSON() {
25
+ return {
26
+ name: this.name,
27
+ code: this.code,
28
+ message: this.message,
29
+ provider: this.provider,
30
+ model: this.model,
31
+ statusCode: this.statusCode,
32
+ retryable: this.retryable,
33
+ retryAfterMs: this.retryAfterMs,
34
+ metadata: this.metadata
35
+ };
36
+ }
37
+ static providerError(message, opts) {
38
+ return new ElsiumError({
39
+ code: "PROVIDER_ERROR",
40
+ message,
41
+ provider: opts.provider,
42
+ statusCode: opts.statusCode,
43
+ retryable: opts.retryable ?? false,
44
+ cause: opts.cause
45
+ });
46
+ }
47
+ static rateLimit(provider, retryAfterMs) {
48
+ return new ElsiumError({
49
+ code: "RATE_LIMIT",
50
+ message: `Rate limited by ${provider}`,
51
+ provider,
52
+ statusCode: 429,
53
+ retryable: true,
54
+ retryAfterMs
55
+ });
56
+ }
57
+ static authError(provider) {
58
+ return new ElsiumError({
59
+ code: "AUTH_ERROR",
60
+ message: `Authentication failed for ${provider}. Check your API key.`,
61
+ provider,
62
+ statusCode: 401,
63
+ retryable: false
64
+ });
65
+ }
66
+ static timeout(provider, timeoutMs) {
67
+ return new ElsiumError({
68
+ code: "TIMEOUT",
69
+ message: `Request to ${provider} timed out after ${timeoutMs}ms`,
70
+ provider,
71
+ retryable: true
72
+ });
73
+ }
74
+ static validation(message, metadata) {
75
+ return new ElsiumError({
76
+ code: "VALIDATION_ERROR",
77
+ message,
78
+ retryable: false,
79
+ metadata
80
+ });
81
+ }
82
+ static budgetExceeded(spent, budget) {
83
+ return new ElsiumError({
84
+ code: "BUDGET_EXCEEDED",
85
+ message: `Token budget exceeded: spent ${spent}, budget ${budget}`,
86
+ retryable: false,
87
+ metadata: { spent, budget }
88
+ });
89
+ }
90
+ }
91
+ // ../core/src/utils.ts
92
+ import { randomBytes } from "crypto";
93
+ function cryptoHex(bytes) {
94
+ return randomBytes(bytes).toString("hex");
95
+ }
96
+ function generateId(prefix = "els") {
97
+ const timestamp = Date.now().toString(36);
98
+ const random = cryptoHex(4);
99
+ return `${prefix}_${timestamp}_${random}`;
100
+ }
101
+ // ../core/src/logger.ts
102
+ var LOG_LEVELS = {
103
+ debug: 0,
104
+ info: 1,
105
+ warn: 2,
106
+ error: 3
107
+ };
108
+ function createLogger(options = {}) {
109
+ const { level = "info", pretty = false, context = {} } = options;
110
+ const minLevel = LOG_LEVELS[level];
111
+ function log(logLevel, message, data) {
112
+ if (LOG_LEVELS[logLevel] < minLevel)
113
+ return;
114
+ const entry = {
115
+ ...context,
116
+ level: logLevel,
117
+ message,
118
+ timestamp: new Date().toISOString(),
119
+ ...data ? { data } : {}
120
+ };
121
+ const output = pretty ? JSON.stringify(entry, null, 2) : JSON.stringify(entry);
122
+ if (logLevel === "error") {
123
+ console.error(output);
124
+ } else if (logLevel === "warn") {
125
+ console.warn(output);
126
+ } else {
127
+ console.log(output);
128
+ }
129
+ }
130
+ return {
131
+ debug: (msg, data) => log("debug", msg, data),
132
+ info: (msg, data) => log("info", msg, data),
133
+ warn: (msg, data) => log("warn", msg, data),
134
+ error: (msg, data) => log("error", msg, data),
135
+ child(childContext) {
136
+ return createLogger({
137
+ level,
138
+ pretty,
139
+ context: { ...context, ...childContext }
140
+ });
141
+ }
142
+ };
143
+ }
144
+ // src/span.ts
145
+ function createSpan(name, options = {}) {
146
+ const id = generateId("spn");
147
+ const traceId = options.traceId ?? generateId("trc");
148
+ const kind = options.kind ?? "custom";
149
+ const startTime = Date.now();
150
+ const metadata = {};
151
+ const events = [];
152
+ let status = "running";
153
+ let endTime;
154
+ const span = {
155
+ id,
156
+ traceId,
157
+ name,
158
+ kind,
159
+ addEvent(eventName, data) {
160
+ events.push({
161
+ name: eventName,
162
+ timestamp: Date.now(),
163
+ data
164
+ });
165
+ },
166
+ setMetadata(key, value) {
167
+ if (key === "__proto__" || key === "constructor" || key === "prototype")
168
+ return;
169
+ metadata[key] = value;
170
+ },
171
+ end(result) {
172
+ if (endTime !== undefined)
173
+ return;
174
+ endTime = Date.now();
175
+ status = result?.status ?? "ok";
176
+ if (result?.metadata) {
177
+ for (const [key, value] of Object.entries(result.metadata)) {
178
+ if (key === "__proto__" || key === "constructor" || key === "prototype")
179
+ continue;
180
+ metadata[key] = value;
181
+ }
182
+ }
183
+ options.onEnd?.(span.toJSON());
184
+ },
185
+ child(childName, childKind) {
186
+ return createSpan(childName, {
187
+ traceId,
188
+ parentId: id,
189
+ kind: childKind ?? kind,
190
+ onEnd: options.onEnd
191
+ });
192
+ },
193
+ toJSON() {
194
+ const duration = endTime !== undefined ? endTime - startTime : undefined;
195
+ return {
196
+ id,
197
+ traceId,
198
+ parentId: options.parentId,
199
+ name,
200
+ kind,
201
+ status,
202
+ startTime,
203
+ endTime,
204
+ durationMs: duration !== undefined ? Math.round(duration) : undefined,
205
+ metadata,
206
+ events
207
+ };
208
+ }
209
+ };
210
+ return span;
211
+ }
212
+ // src/cost-engine.ts
213
+ function registerModelTier(model, entry) {
214
+ MODEL_TIERS[model] = entry;
215
+ }
216
+ var MODEL_TIERS = {
217
+ "gpt-5-nano": { tier: "low", costPerMToken: 0.05 },
218
+ "gemini-2.0-flash-lite": { tier: "low", costPerMToken: 0.075 },
219
+ "gemini-2.0-flash": { tier: "low", costPerMToken: 0.1 },
220
+ "gpt-4.1-nano": { tier: "low", costPerMToken: 0.1 },
221
+ "gpt-4o-mini": { tier: "low", costPerMToken: 0.15 },
222
+ "gpt-5-mini": { tier: "low", costPerMToken: 0.25 },
223
+ "gpt-4.1-mini": { tier: "low", costPerMToken: 0.4 },
224
+ "claude-haiku-4-5-20251001": { tier: "low", costPerMToken: 1 },
225
+ "o3-mini": { tier: "mid", costPerMToken: 1.1 },
226
+ "o1-mini": { tier: "mid", costPerMToken: 1.1 },
227
+ "o4-mini": { tier: "mid", costPerMToken: 1.1 },
228
+ "gpt-5": { tier: "mid", costPerMToken: 1.25 },
229
+ "gemini-2.5-pro-preview-05-06": { tier: "mid", costPerMToken: 1.25 },
230
+ o3: { tier: "mid", costPerMToken: 2 },
231
+ "gpt-4.1": { tier: "mid", costPerMToken: 2 },
232
+ "gpt-4o": { tier: "mid", costPerMToken: 2.5 },
233
+ "claude-sonnet-4-6": { tier: "mid", costPerMToken: 3 },
234
+ "claude-opus-4-6": { tier: "high", costPerMToken: 15 },
235
+ o1: { tier: "high", costPerMToken: 15 },
236
+ "o3-pro": { tier: "high", costPerMToken: 20 }
237
+ };
238
+ function createDimension() {
239
+ return { totalCost: 0, totalTokens: 0, callCount: 0, firstCallAt: 0, lastCallAt: 0 };
240
+ }
241
+ function updateDimension(dim, cost, tokens) {
242
+ const now = Date.now();
243
+ dim.totalCost += cost;
244
+ dim.totalTokens += tokens;
245
+ dim.callCount++;
246
+ if (dim.firstCallAt === 0)
247
+ dim.firstCallAt = now;
248
+ dim.lastCallAt = now;
249
+ }
250
+ function createCostEngine(config = {}) {
251
+ const byModel = {};
252
+ const byAgent = {};
253
+ const byUser = {};
254
+ const byFeature = {};
255
+ let totalSpend = 0;
256
+ let totalTokens = 0;
257
+ let pendingSpend = 0;
258
+ let totalCalls = 0;
259
+ const startedAt = Date.now();
260
+ const alerts = [];
261
+ const recentCalls = [];
262
+ const alertedThresholds = new Set;
263
+ const maxAlerts = 1000;
264
+ function emitAlert(alert) {
265
+ alerts.push(alert);
266
+ if (alerts.length > maxAlerts)
267
+ alerts.shift();
268
+ config.onAlert?.(alert);
269
+ }
270
+ function checkDailyBudget() {
271
+ if (!config.dailyBudget)
272
+ return;
273
+ const elapsedMs = Date.now() - startedAt;
274
+ const elapsedDays = Math.max(elapsedMs / (24 * 60 * 60 * 1000), 1 / 24);
275
+ const dailyRate = totalSpend / elapsedDays;
276
+ if (dailyRate <= config.dailyBudget)
277
+ return;
278
+ const key = `daily:${Math.floor(Date.now() / (60 * 60 * 1000))}`;
279
+ if (alertedThresholds.has(key))
280
+ return;
281
+ alertedThresholds.add(key);
282
+ emitAlert({
283
+ type: "budget_exceeded",
284
+ dimension: "daily",
285
+ currentValue: dailyRate,
286
+ limit: config.dailyBudget,
287
+ message: `Daily spend rate $${dailyRate.toFixed(4)} exceeds budget $${config.dailyBudget}`,
288
+ timestamp: Date.now()
289
+ });
290
+ }
291
+ function checkDimensionBudget(limit, dimensionKey, store) {
292
+ if (!limit || !dimensionKey)
293
+ return;
294
+ const dim = store[dimensionKey];
295
+ if (dim && dim.totalCost > limit) {
296
+ throw ElsiumError.budgetExceeded(dim.totalCost, limit);
297
+ }
298
+ }
299
+ function emitThresholdAlertIfNew(threshold, budget) {
300
+ const thresholdAmount = budget * threshold;
301
+ if (totalSpend < thresholdAmount)
302
+ return;
303
+ const key = `threshold:${threshold}`;
304
+ if (alertedThresholds.has(key))
305
+ return;
306
+ alertedThresholds.add(key);
307
+ emitAlert({
308
+ type: "threshold",
309
+ dimension: "total",
310
+ currentValue: totalSpend,
311
+ limit: thresholdAmount,
312
+ message: `Spend reached ${(threshold * 100).toFixed(0)}% of budget ($${totalSpend.toFixed(4)} / $${budget})`,
313
+ timestamp: Date.now()
314
+ });
315
+ }
316
+ function checkAlertThresholds() {
317
+ if (!config.alertThresholds || !config.totalBudget)
318
+ return;
319
+ for (const threshold of config.alertThresholds) {
320
+ emitThresholdAlertIfNew(threshold, config.totalBudget);
321
+ }
322
+ }
323
+ function checkBudgets(dimensions) {
324
+ if (config.totalBudget && totalSpend > config.totalBudget) {
325
+ throw ElsiumError.budgetExceeded(totalSpend, config.totalBudget);
326
+ }
327
+ checkDailyBudget();
328
+ checkDimensionBudget(config.perAgent, dimensions.agent, byAgent);
329
+ checkDimensionBudget(config.perUser, dimensions.user, byUser);
330
+ checkDimensionBudget(config.perFeature, dimensions.feature, byFeature);
331
+ checkAlertThresholds();
332
+ }
333
+ function checkLoopDetection() {
334
+ if (!config.loopDetection)
335
+ return;
336
+ const now = Date.now();
337
+ const oneMinuteAgo = now - 60000;
338
+ while (recentCalls.length > 0 && recentCalls[0].timestamp < oneMinuteAgo) {
339
+ recentCalls.shift();
340
+ }
341
+ if (config.loopDetection.maxCallsPerMinute && recentCalls.length > config.loopDetection.maxCallsPerMinute) {
342
+ emitAlert({
343
+ type: "loop_detected",
344
+ dimension: "calls_per_minute",
345
+ currentValue: recentCalls.length,
346
+ limit: config.loopDetection.maxCallsPerMinute,
347
+ message: `Loop detected: ${recentCalls.length} calls in last minute (max: ${config.loopDetection.maxCallsPerMinute})`,
348
+ timestamp: now
349
+ });
350
+ }
351
+ if (config.loopDetection.maxCostPerMinute) {
352
+ const recentCost = recentCalls.reduce((sum, r) => sum + r.cost, 0);
353
+ if (recentCost > config.loopDetection.maxCostPerMinute) {
354
+ emitAlert({
355
+ type: "loop_detected",
356
+ dimension: "cost_per_minute",
357
+ currentValue: recentCost,
358
+ limit: config.loopDetection.maxCostPerMinute,
359
+ message: `Cost loop detected: $${recentCost.toFixed(4)} in last minute (max: $${config.loopDetection.maxCostPerMinute})`,
360
+ timestamp: now
361
+ });
362
+ }
363
+ }
364
+ }
365
+ function trackDimension(store, key, cost, tokens) {
366
+ if (!key)
367
+ return;
368
+ if (!store[key])
369
+ store[key] = createDimension();
370
+ updateDimension(store[key], cost, tokens);
371
+ }
372
+ function trackCall(response, dimensions = {}) {
373
+ const cost = response.cost.totalCost;
374
+ const tokens = response.usage.totalTokens;
375
+ totalSpend += cost;
376
+ totalTokens += tokens;
377
+ totalCalls++;
378
+ trackDimension(byModel, response.model, cost, tokens);
379
+ trackDimension(byAgent, dimensions.agent, cost, tokens);
380
+ trackDimension(byUser, dimensions.user, cost, tokens);
381
+ trackDimension(byFeature, dimensions.feature, cost, tokens);
382
+ recentCalls.push({ timestamp: Date.now(), cost, model: response.model, tokens });
383
+ checkLoopDetection();
384
+ checkBudgets(dimensions);
385
+ }
386
+ return {
387
+ middleware() {
388
+ return async (ctx, next) => {
389
+ const agent = ctx.metadata.agentName;
390
+ const user = ctx.metadata.userId;
391
+ const feature = ctx.metadata.feature;
392
+ let reserved = 0;
393
+ if (config.totalBudget) {
394
+ const inputText = ctx.request.messages.map((m) => typeof m.content === "string" ? m.content : "").join("");
395
+ const estimatedTokens = Math.ceil(inputText.length / 4);
396
+ const modelTier = MODEL_TIERS[ctx.model];
397
+ if (modelTier) {
398
+ const estimatedCost = estimatedTokens / 1e6 * modelTier.costPerMToken;
399
+ if (totalSpend + pendingSpend + estimatedCost > config.totalBudget) {
400
+ throw ElsiumError.validation("Budget would be exceeded");
401
+ }
402
+ reserved = estimatedCost;
403
+ pendingSpend += reserved;
404
+ }
405
+ }
406
+ try {
407
+ const response = await next(ctx);
408
+ pendingSpend -= reserved;
409
+ trackCall(response, { agent, user, feature });
410
+ return response;
411
+ } catch (error) {
412
+ pendingSpend -= reserved;
413
+ throw error;
414
+ }
415
+ };
416
+ },
417
+ trackCall,
418
+ getReport() {
419
+ const elapsedMs = Math.max(Date.now() - startedAt, 1);
420
+ const elapsedHours = elapsedMs / (60 * 60 * 1000);
421
+ const projectedDailySpend = totalCalls > 0 ? totalSpend / elapsedHours * 24 : 0;
422
+ const projectedMonthlySpend = projectedDailySpend * 30;
423
+ const recommendations = [];
424
+ for (const [model, dim] of Object.entries(byModel)) {
425
+ const tier = MODEL_TIERS[model];
426
+ if (tier?.tier === "high" && dim.callCount > 10) {
427
+ recommendations.push(`Consider using a mid-tier model instead of ${model} for routine tasks. ${dim.callCount} calls at $${dim.totalCost.toFixed(4)} total.`);
428
+ }
429
+ }
430
+ if (config.totalBudget && projectedMonthlySpend > config.totalBudget * 1.2) {
431
+ recommendations.push(`Projected monthly spend ($${projectedMonthlySpend.toFixed(2)}) exceeds budget by ${((projectedMonthlySpend / config.totalBudget - 1) * 100).toFixed(0)}%.`);
432
+ }
433
+ if (totalCalls > 50 && totalTokens / totalCalls < 100) {
434
+ recommendations.push("Average token count per call is very low. Consider batching requests to reduce overhead.");
435
+ }
436
+ return {
437
+ totalSpend,
438
+ totalTokens,
439
+ totalCalls,
440
+ projectedDailySpend,
441
+ projectedMonthlySpend,
442
+ byModel: { ...byModel },
443
+ byAgent: { ...byAgent },
444
+ byUser: { ...byUser },
445
+ byFeature: { ...byFeature },
446
+ recommendations,
447
+ alerts: [...alerts]
448
+ };
449
+ },
450
+ suggestModel(currentModel, inputTokens) {
451
+ const current = MODEL_TIERS[currentModel];
452
+ if (!current || current.tier === "low")
453
+ return null;
454
+ const cheaper = Object.entries(MODEL_TIERS).filter(([, info]) => {
455
+ if (current.tier === "high")
456
+ return info.tier === "mid" || info.tier === "low";
457
+ if (current.tier === "mid")
458
+ return info.tier === "low";
459
+ return false;
460
+ }).sort((a, b) => a[1].costPerMToken - b[1].costPerMToken);
461
+ if (cheaper.length === 0)
462
+ return null;
463
+ const isSimple = inputTokens < 500;
464
+ const [suggestedModel, suggestedInfo] = cheaper[0];
465
+ if (!isSimple && current.tier !== "high")
466
+ return null;
467
+ const estimatedSavings = (current.costPerMToken - suggestedInfo.costPerMToken) / current.costPerMToken * 100;
468
+ return {
469
+ currentModel,
470
+ suggestedModel,
471
+ estimatedSavings,
472
+ reason: isSimple ? `Simple request (${inputTokens} tokens) could use a cheaper model` : `Consider ${suggestedModel} for routine tasks (${estimatedSavings.toFixed(0)}% savings)`
473
+ };
474
+ },
475
+ reset() {
476
+ for (const key of Object.keys(byModel))
477
+ delete byModel[key];
478
+ for (const key of Object.keys(byAgent))
479
+ delete byAgent[key];
480
+ for (const key of Object.keys(byUser))
481
+ delete byUser[key];
482
+ for (const key of Object.keys(byFeature))
483
+ delete byFeature[key];
484
+ totalSpend = 0;
485
+ totalTokens = 0;
486
+ pendingSpend = 0;
487
+ totalCalls = 0;
488
+ alerts.length = 0;
489
+ recentCalls.length = 0;
490
+ alertedThresholds.clear();
491
+ }
492
+ };
493
+ }
494
+ // src/tracer.ts
495
+ var log = createLogger();
496
+ function observe(config = {}) {
497
+ const {
498
+ output = ["console"],
499
+ costTracking = true,
500
+ samplingRate = 1,
501
+ maxSpans = 1e4
502
+ } = config;
503
+ const spans = [];
504
+ const llmCalls = [];
505
+ const exporters = [];
506
+ const handlers = [];
507
+ for (const out of output) {
508
+ if (out === "console") {
509
+ handlers.push(consoleHandler);
510
+ } else if (out === "json-file") {} else {
511
+ exporters.push(out);
512
+ }
513
+ }
514
+ function shouldSample() {
515
+ if (samplingRate >= 1)
516
+ return true;
517
+ return Math.random() < samplingRate;
518
+ }
519
+ function onSpanEnd(span) {
520
+ if (spans.length >= maxSpans) {
521
+ spans.shift();
522
+ }
523
+ spans.push(span);
524
+ for (const handler of handlers) {
525
+ handler(span);
526
+ }
527
+ }
528
+ return {
529
+ startSpan(name, kind) {
530
+ if (!shouldSample()) {
531
+ return createNoopSpan(name, kind);
532
+ }
533
+ return createSpan(name, { kind, onEnd: onSpanEnd });
534
+ },
535
+ getSpans() {
536
+ return [...spans];
537
+ },
538
+ getCostReport() {
539
+ const byModel = {};
540
+ for (const call of llmCalls) {
541
+ if (!byModel[call.model]) {
542
+ byModel[call.model] = { cost: 0, tokens: 0, calls: 0 };
543
+ }
544
+ byModel[call.model].cost += call.cost;
545
+ byModel[call.model].tokens += call.inputTokens + call.outputTokens;
546
+ byModel[call.model].calls++;
547
+ }
548
+ return {
549
+ totalCost: llmCalls.reduce((sum, c) => sum + c.cost, 0),
550
+ totalTokens: llmCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0),
551
+ totalInputTokens: llmCalls.reduce((sum, c) => sum + c.inputTokens, 0),
552
+ totalOutputTokens: llmCalls.reduce((sum, c) => sum + c.outputTokens, 0),
553
+ callCount: llmCalls.length,
554
+ byModel
555
+ };
556
+ },
557
+ trackLLMCall(data) {
558
+ if (!costTracking)
559
+ return;
560
+ llmCalls.push(data);
561
+ if (llmCalls.length > maxSpans) {
562
+ llmCalls.shift();
563
+ }
564
+ },
565
+ reset() {
566
+ spans.length = 0;
567
+ llmCalls.length = 0;
568
+ },
569
+ async flush() {
570
+ for (const exporter of exporters) {
571
+ await exporter.export([...spans]);
572
+ }
573
+ }
574
+ };
575
+ }
576
+ function consoleHandler(span) {
577
+ const duration = span.durationMs !== undefined ? `${span.durationMs}ms` : "running";
578
+ const status = span.status === "error" ? "[ERROR]" : span.status === "ok" ? "[OK]" : "[...]";
579
+ log.info("span", {
580
+ trace: span.traceId,
581
+ span: span.name,
582
+ kind: span.kind,
583
+ status,
584
+ duration,
585
+ ...Object.keys(span.metadata).length > 0 ? { metadata: span.metadata } : {}
586
+ });
587
+ }
588
+ function createNoopSpan(name, kind) {
589
+ const id = generateId("spn");
590
+ const traceId = generateId("trc");
591
+ return {
592
+ id,
593
+ traceId,
594
+ name,
595
+ kind: kind ?? "custom",
596
+ addEvent() {},
597
+ setMetadata() {},
598
+ end() {},
599
+ child(childName, childKind) {
600
+ return createNoopSpan(childName, childKind);
601
+ },
602
+ toJSON() {
603
+ return {
604
+ id,
605
+ traceId,
606
+ name,
607
+ kind: kind ?? "custom",
608
+ status: "ok",
609
+ startTime: 0,
610
+ metadata: {},
611
+ events: []
612
+ };
613
+ }
614
+ };
615
+ }
616
+ // src/metrics.ts
617
+ function createMetrics(options) {
618
+ const maxEntries = options?.maxEntries ?? 50000;
619
+ const entries = [];
620
+ const counters = new Map;
621
+ const gauges = new Map;
622
+ function tagKey(name, tags) {
623
+ if (!tags || Object.keys(tags).length === 0)
624
+ return name;
625
+ const sorted = Object.entries(tags).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",");
626
+ return `${name}{${sorted}}`;
627
+ }
628
+ return {
629
+ increment(name, value = 1, tags) {
630
+ const key = tagKey(name, tags);
631
+ const current = counters.get(key) ?? 0;
632
+ counters.set(key, current + value);
633
+ entries.push({
634
+ name,
635
+ type: "counter",
636
+ value: current + value,
637
+ tags: tags ?? {},
638
+ timestamp: Date.now()
639
+ });
640
+ if (entries.length > maxEntries)
641
+ entries.shift();
642
+ },
643
+ gauge(name, value, tags) {
644
+ const key = tagKey(name, tags);
645
+ gauges.set(key, value);
646
+ entries.push({
647
+ name,
648
+ type: "gauge",
649
+ value,
650
+ tags: tags ?? {},
651
+ timestamp: Date.now()
652
+ });
653
+ if (entries.length > maxEntries)
654
+ entries.shift();
655
+ },
656
+ histogram(name, value, tags) {
657
+ entries.push({
658
+ name,
659
+ type: "histogram",
660
+ value,
661
+ tags: tags ?? {},
662
+ timestamp: Date.now()
663
+ });
664
+ if (entries.length > maxEntries)
665
+ entries.shift();
666
+ },
667
+ getMetrics() {
668
+ return [...entries];
669
+ },
670
+ reset() {
671
+ entries.length = 0;
672
+ counters.clear();
673
+ gauges.clear();
674
+ }
675
+ };
676
+ }
677
+ // src/audit.ts
678
+ import { createHash } from "crypto";
679
+ function computeEventHash(event, previousHash) {
680
+ const content = JSON.stringify({
681
+ id: event.id,
682
+ sequenceId: event.sequenceId,
683
+ type: event.type,
684
+ timestamp: event.timestamp,
685
+ actor: event.actor,
686
+ traceId: event.traceId,
687
+ data: event.data,
688
+ previousHash
689
+ });
690
+ return createHash("sha256").update(content).digest("hex");
691
+ }
692
+
693
+ class InMemoryAuditStorage {
694
+ events = [];
695
+ maxEvents;
696
+ constructor(maxEvents) {
697
+ this.maxEvents = maxEvents ?? 1e4;
698
+ }
699
+ append(event) {
700
+ this.events.push(event);
701
+ if (this.events.length > this.maxEvents) {
702
+ this.events = this.events.slice(-this.maxEvents);
703
+ }
704
+ }
705
+ query(filter) {
706
+ let results = [...this.events];
707
+ if (filter.type) {
708
+ const types = Array.isArray(filter.type) ? filter.type : [filter.type];
709
+ results = results.filter((e) => types.includes(e.type));
710
+ }
711
+ if (filter.actor) {
712
+ results = results.filter((e) => e.actor === filter.actor);
713
+ }
714
+ if (filter.traceId) {
715
+ results = results.filter((e) => e.traceId === filter.traceId);
716
+ }
717
+ if (filter.fromTimestamp !== undefined) {
718
+ const from = filter.fromTimestamp;
719
+ results = results.filter((e) => e.timestamp >= from);
720
+ }
721
+ if (filter.toTimestamp !== undefined) {
722
+ const to = filter.toTimestamp;
723
+ results = results.filter((e) => e.timestamp <= to);
724
+ }
725
+ const offset = filter.offset ?? 0;
726
+ const limit = filter.limit ?? results.length;
727
+ return results.slice(offset, offset + limit);
728
+ }
729
+ count() {
730
+ return this.events.length;
731
+ }
732
+ verifyIntegrity() {
733
+ if (this.events.length === 0) {
734
+ return { valid: true, totalEvents: 0, chainComplete: true };
735
+ }
736
+ for (let i = 0;i < this.events.length; i++) {
737
+ const event = this.events[i];
738
+ const expectedHash = computeEventHash(event, event.previousHash);
739
+ if (event.hash !== expectedHash) {
740
+ return { valid: false, totalEvents: this.events.length, brokenAt: i };
741
+ }
742
+ if (i > 0 && event.previousHash !== this.events[i - 1].hash) {
743
+ return { valid: false, totalEvents: this.events.length, brokenAt: i };
744
+ }
745
+ }
746
+ const chainComplete = this.events[0].previousHash === "0".repeat(64);
747
+ return { valid: true, totalEvents: this.events.length, chainComplete };
748
+ }
749
+ getLastHash() {
750
+ if (this.events.length === 0)
751
+ return "0".repeat(64);
752
+ return this.events[this.events.length - 1].hash;
753
+ }
754
+ }
755
+ function createAuditTrail(config) {
756
+ const useHashChain = config?.hashChain !== false;
757
+ const storage = config?.storage && typeof config.storage !== "string" ? config.storage : new InMemoryAuditStorage(config?.maxEvents);
758
+ let sequenceId = 0;
759
+ let idCounter = 0;
760
+ let previousHash = "0".repeat(64);
761
+ if (useHashChain && storage.getLastHash) {
762
+ const lastHash = storage.getLastHash();
763
+ if (typeof lastHash === "string") {
764
+ previousHash = lastHash;
765
+ } else if (lastHash && typeof lastHash.then === "function") {
766
+ lastHash.then((hash) => {
767
+ if (typeof hash === "string")
768
+ previousHash = hash;
769
+ });
770
+ }
771
+ }
772
+ return {
773
+ log(type, data, options) {
774
+ sequenceId++;
775
+ idCounter++;
776
+ const event = {
777
+ id: `audit_${idCounter.toString(36)}_${Date.now().toString(36)}`,
778
+ sequenceId,
779
+ type,
780
+ timestamp: Date.now(),
781
+ actor: options?.actor,
782
+ traceId: options?.traceId,
783
+ data,
784
+ previousHash: useHashChain ? previousHash : "0".repeat(64)
785
+ };
786
+ const hash = useHashChain ? computeEventHash(event, event.previousHash) : createHash("sha256").update(JSON.stringify(event)).digest("hex");
787
+ const finalEvent = { ...event, hash };
788
+ if (useHashChain) {
789
+ previousHash = hash;
790
+ }
791
+ const result = storage.append(finalEvent);
792
+ if (result && typeof result.catch === "function") {
793
+ result.catch((err2) => config?.onError?.(err2));
794
+ }
795
+ },
796
+ async query(filter) {
797
+ return storage.query(filter);
798
+ },
799
+ async verifyIntegrity() {
800
+ return storage.verifyIntegrity();
801
+ },
802
+ get count() {
803
+ const result = storage.count();
804
+ return typeof result === "number" ? result : 0;
805
+ }
806
+ };
807
+ }
808
+ function auditMiddleware(auditTrail) {
809
+ return async (ctx, next) => {
810
+ const startTime = performance.now();
811
+ try {
812
+ const response = await next(ctx);
813
+ const latencyMs = Math.round(performance.now() - startTime);
814
+ auditTrail.log("llm_call", {
815
+ provider: ctx.provider,
816
+ model: ctx.model,
817
+ inputTokens: response.usage.inputTokens,
818
+ outputTokens: response.usage.outputTokens,
819
+ totalTokens: response.usage.totalTokens,
820
+ cost: response.cost.totalCost,
821
+ latencyMs,
822
+ stopReason: response.stopReason
823
+ }, { traceId: ctx.traceId });
824
+ return response;
825
+ } catch (error) {
826
+ const latencyMs = Math.round(performance.now() - startTime);
827
+ auditTrail.log("llm_call", {
828
+ provider: ctx.provider,
829
+ model: ctx.model,
830
+ error: error instanceof Error ? error.message : String(error),
831
+ latencyMs,
832
+ success: false
833
+ }, { traceId: ctx.traceId });
834
+ throw error;
835
+ }
836
+ };
837
+ }
838
+ // src/provenance.ts
839
+ import { createHash as createHash2 } from "crypto";
840
+ function sha256(input) {
841
+ return createHash2("sha256").update(input).digest("hex");
842
+ }
843
+ function matchesFilter(record, filter) {
844
+ if (filter.outputHash && record.outputHash !== filter.outputHash)
845
+ return false;
846
+ if (filter.promptVersion && record.promptVersion !== filter.promptVersion)
847
+ return false;
848
+ if (filter.modelVersion && record.modelVersion !== filter.modelVersion)
849
+ return false;
850
+ if (filter.traceId && record.traceId !== filter.traceId)
851
+ return false;
852
+ return true;
853
+ }
854
+ function createProvenanceTracker(options) {
855
+ const maxRecords = options?.maxRecords ?? 1e4;
856
+ const records = [];
857
+ let idCounter = 0;
858
+ return {
859
+ record(data) {
860
+ idCounter++;
861
+ const record = {
862
+ id: `prov_${idCounter.toString(36)}_${Date.now().toString(36)}`,
863
+ outputHash: sha256(data.output),
864
+ promptVersion: sha256(data.prompt),
865
+ modelVersion: sha256(data.model),
866
+ configHash: sha256(JSON.stringify(data.config)),
867
+ inputHash: sha256(data.input),
868
+ timestamp: Date.now(),
869
+ traceId: data.traceId,
870
+ metadata: data.metadata
871
+ };
872
+ records.push(record);
873
+ if (records.length > maxRecords) {
874
+ records.shift();
875
+ }
876
+ return record;
877
+ },
878
+ query(filter) {
879
+ return records.filter((r) => matchesFilter(r, filter));
880
+ },
881
+ getLineage(outputHash) {
882
+ const target = records.find((r) => r.outputHash === outputHash);
883
+ if (!target?.traceId)
884
+ return target ? [target] : [];
885
+ return records.filter((r) => r.traceId === target.traceId).sort((a, b) => a.timestamp - b.timestamp);
886
+ },
887
+ get count() {
888
+ return records.length;
889
+ },
890
+ clear() {
891
+ records.length = 0;
892
+ }
893
+ };
894
+ }
895
+ // src/otel.ts
896
+ var log2 = createLogger();
897
+ var SPAN_KIND_MAP = {
898
+ llm: 3,
899
+ tool: 1,
900
+ agent: 1,
901
+ workflow: 1,
902
+ custom: 0
903
+ };
904
+ function toNanoString(ms) {
905
+ return String(Math.round(ms * 1e6));
906
+ }
907
+ function toOTelAttribute(key, value) {
908
+ if (typeof value === "string") {
909
+ return { key, value: { stringValue: value } };
910
+ }
911
+ if (typeof value === "number") {
912
+ return Number.isInteger(value) ? { key, value: { intValue: value } } : { key, value: { doubleValue: value } };
913
+ }
914
+ if (typeof value === "boolean") {
915
+ return { key, value: { boolValue: value } };
916
+ }
917
+ return { key, value: { stringValue: JSON.stringify(value) } };
918
+ }
919
+ function toOTelSpan(span) {
920
+ const attributes = [toOTelAttribute("elsium.span.kind", span.kind)];
921
+ for (const [key, value] of Object.entries(span.metadata)) {
922
+ attributes.push(toOTelAttribute(`elsium.${key}`, value));
923
+ }
924
+ const events = span.events.map((e) => ({
925
+ name: e.name,
926
+ timeUnixNano: toNanoString(e.timestamp),
927
+ attributes: e.data ? Object.entries(e.data).map(([k, v]) => toOTelAttribute(k, v)) : []
928
+ }));
929
+ let statusCode = 0;
930
+ if (span.status === "ok")
931
+ statusCode = 1;
932
+ if (span.status === "error")
933
+ statusCode = 2;
934
+ return {
935
+ traceId: normalizeId(span.traceId, 32),
936
+ spanId: normalizeId(span.id, 16),
937
+ parentSpanId: span.parentId ? normalizeId(span.parentId, 16) : undefined,
938
+ name: span.name,
939
+ kind: SPAN_KIND_MAP[span.kind] ?? 0,
940
+ startTimeUnixNano: toNanoString(span.startTime),
941
+ endTimeUnixNano: span.endTime ? toNanoString(span.endTime) : toNanoString(span.startTime),
942
+ attributes,
943
+ events,
944
+ status: { code: statusCode }
945
+ };
946
+ }
947
+ function normalizeId(id, length) {
948
+ const clean = id.replace(/^[a-z]+_/, "");
949
+ const hex = Array.from(clean).map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("");
950
+ return hex.slice(0, length).padEnd(length, "0");
951
+ }
952
+ function toOTelExportRequest(spans, options = {}) {
953
+ const { serviceName = "elsium-ai", serviceVersion = "0.1.0" } = options;
954
+ return {
955
+ resourceSpans: [
956
+ {
957
+ resource: {
958
+ attributes: [
959
+ toOTelAttribute("service.name", serviceName),
960
+ toOTelAttribute("service.version", serviceVersion),
961
+ toOTelAttribute("telemetry.sdk.name", "elsium-ai"),
962
+ toOTelAttribute("telemetry.sdk.language", "typescript")
963
+ ]
964
+ },
965
+ scopeSpans: [
966
+ {
967
+ scope: {
968
+ name: "@elsium-ai/observe",
969
+ version: serviceVersion
970
+ },
971
+ spans: spans.map(toOTelSpan)
972
+ }
973
+ ]
974
+ }
975
+ ]
976
+ };
977
+ }
978
+ function toTraceparent(span) {
979
+ const version = "00";
980
+ const traceId = normalizeId(span.traceId, 32);
981
+ const spanId = normalizeId(span.id, 16);
982
+ const flags = "01";
983
+ return `${version}-${traceId}-${spanId}-${flags}`;
984
+ }
985
+ function parseTraceparent(header) {
986
+ const parts = header.trim().split("-");
987
+ if (parts.length < 4)
988
+ return null;
989
+ const [version, traceId, spanId, flags] = parts;
990
+ if (version !== "00")
991
+ return null;
992
+ if (traceId.length !== 32 || spanId.length !== 16)
993
+ return null;
994
+ return {
995
+ traceId,
996
+ spanId,
997
+ traceFlags: Number.parseInt(flags, 16)
998
+ };
999
+ }
1000
+ function injectTraceContext(span, headers = {}) {
1001
+ return {
1002
+ ...headers,
1003
+ traceparent: toTraceparent(span)
1004
+ };
1005
+ }
1006
+ function extractTraceContext(headers) {
1007
+ const traceparent = headers.traceparent ?? headers.Traceparent;
1008
+ if (!traceparent)
1009
+ return null;
1010
+ return parseTraceparent(traceparent);
1011
+ }
1012
+ function createOTLPExporter(config) {
1013
+ const {
1014
+ endpoint,
1015
+ headers = {},
1016
+ serviceName,
1017
+ serviceVersion,
1018
+ batchSize = 100,
1019
+ flushIntervalMs = 5000
1020
+ } = config;
1021
+ const buffer = [];
1022
+ let flushTimer = null;
1023
+ async function sendBatch(spans) {
1024
+ if (spans.length === 0)
1025
+ return;
1026
+ const payload = toOTelExportRequest(spans, { serviceName, serviceVersion });
1027
+ try {
1028
+ const response = await fetch(endpoint, {
1029
+ method: "POST",
1030
+ headers: {
1031
+ "Content-Type": "application/json",
1032
+ ...headers
1033
+ },
1034
+ body: JSON.stringify(payload)
1035
+ });
1036
+ if (!response.ok) {
1037
+ log2.error(`OTLP export failed: ${response.status} ${response.statusText}`);
1038
+ }
1039
+ } catch (err2) {
1040
+ log2.error("OTLP export error", { error: err2 instanceof Error ? err2.message : String(err2) });
1041
+ }
1042
+ }
1043
+ function startAutoFlush() {
1044
+ if (flushTimer)
1045
+ return;
1046
+ flushTimer = setInterval(async () => {
1047
+ if (buffer.length > 0) {
1048
+ const batch = buffer.splice(0, buffer.length);
1049
+ await sendBatch(batch);
1050
+ }
1051
+ }, flushIntervalMs);
1052
+ }
1053
+ return {
1054
+ name: "otlp",
1055
+ async export(spans) {
1056
+ buffer.push(...spans);
1057
+ if (buffer.length >= batchSize) {
1058
+ const batch = buffer.splice(0, batchSize);
1059
+ await sendBatch(batch);
1060
+ } else {
1061
+ startAutoFlush();
1062
+ }
1063
+ }
1064
+ };
1065
+ }
1066
+ export {
1067
+ toTraceparent,
1068
+ toOTelSpan,
1069
+ toOTelExportRequest,
1070
+ registerModelTier,
1071
+ parseTraceparent,
1072
+ observe,
1073
+ injectTraceContext,
1074
+ extractTraceContext,
1075
+ createSpan,
1076
+ createProvenanceTracker,
1077
+ createOTLPExporter,
1078
+ createMetrics,
1079
+ createCostEngine,
1080
+ createAuditTrail,
1081
+ auditMiddleware
1082
+ };