adaptive-memory-multi-model-router 2.8.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ import { EventEmitter } from 'events';
2
+ import { Span, RouteTrace } from './types';
3
+ interface LangfuseConfig {
4
+ publicKey?: string;
5
+ secretKey?: string;
6
+ baseUrl?: string;
7
+ }
8
+ declare class Tracer extends EventEmitter {
9
+ private traces;
10
+ private routeTraces;
11
+ private langfuseClient?;
12
+ private langfuseConfig?;
13
+ constructor();
14
+ /**
15
+ * Initialize Langfuse if configured
16
+ */
17
+ initLangfuse(config: LangfuseConfig): void;
18
+ /**
19
+ * Create a new trace span
20
+ */
21
+ startSpan(operationName: string, parentSpanId?: string): Span;
22
+ /**
23
+ * End a span
24
+ */
25
+ endSpan(spanId: string, attributes?: Record<string, any>): void;
26
+ /**
27
+ * Record an error on a span
28
+ */
29
+ errorSpan(spanId: string, error: string, attributes?: Record<string, any>): void;
30
+ /**
31
+ * Record a route decision
32
+ */
33
+ recordRoute(trace: RouteTrace): void;
34
+ /**
35
+ * Generate a unique trace ID
36
+ */
37
+ generateTraceId(): string;
38
+ /**
39
+ * Get all route traces
40
+ */
41
+ getTraces(limit?: number): RouteTrace[];
42
+ /**
43
+ * Get all spans for a trace
44
+ */
45
+ getSpans(traceId: string): Span[];
46
+ /**
47
+ * Export to Langfuse (async, non-blocking)
48
+ */
49
+ flushToLangfuse(): Promise<void>;
50
+ /**
51
+ * Clear all traces (for testing)
52
+ */
53
+ reset(): void;
54
+ }
55
+ export declare function getTracer(): Tracer;
56
+ export declare function createTracer(): Tracer;
57
+ export {};
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTracer = getTracer;
4
+ exports.createTracer = createTracer;
5
+ const events_1 = require("events");
6
+ function generateId() {
7
+ return Math.random().toString(36).substring(2, 15) +
8
+ Math.random().toString(36).substring(2, 15);
9
+ }
10
+ class Tracer extends events_1.EventEmitter {
11
+ traces = new Map();
12
+ routeTraces = [];
13
+ langfuseClient;
14
+ langfuseConfig;
15
+ constructor() {
16
+ super();
17
+ }
18
+ /**
19
+ * Initialize Langfuse if configured
20
+ */
21
+ initLangfuse(config) {
22
+ if (!config.publicKey || !config.secretKey) {
23
+ console.warn('[Tracer] Langfuse keys not provided, running without Langfuse');
24
+ return;
25
+ }
26
+ this.langfuseConfig = config;
27
+ // Simple Langfuse REST client
28
+ this.langfuseClient = {
29
+ trace: async (data) => {
30
+ try {
31
+ const response = await fetch(`${config.baseUrl || 'https://cloud.langfuse.com'}/api/public/ingestion`, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Authorization': `Bearer ${config.publicKey}`,
36
+ },
37
+ body: JSON.stringify(data),
38
+ });
39
+ if (!response.ok) {
40
+ console.warn('[Tracer] Langfuse ingestion failed:', response.status);
41
+ }
42
+ }
43
+ catch (err) {
44
+ console.warn('[Tracer] Langfuse error:', err);
45
+ }
46
+ },
47
+ flush: async () => {
48
+ // Langfuse uses async ingestion, noop here
49
+ },
50
+ };
51
+ console.log('[Tracer] Langfuse initialized');
52
+ }
53
+ /**
54
+ * Create a new trace span
55
+ */
56
+ startSpan(operationName, parentSpanId) {
57
+ const traceId = parentSpanId
58
+ ? this.traces.get(parentSpanId)?.traceId || this.generateTraceId()
59
+ : this.generateTraceId();
60
+ const span = {
61
+ traceId,
62
+ spanId: this.generateTraceId(),
63
+ parentSpanId,
64
+ operationName,
65
+ startTime: Date.now(),
66
+ attributes: {},
67
+ status: 'started',
68
+ };
69
+ this.traces.set(span.spanId, span);
70
+ this.emit('span_started', span);
71
+ this.emit('observability_event', { type: 'span_started', span });
72
+ return span;
73
+ }
74
+ /**
75
+ * End a span
76
+ */
77
+ endSpan(spanId, attributes) {
78
+ const span = this.traces.get(spanId);
79
+ if (!span) {
80
+ console.warn(`[Tracer] span ${spanId} not found`);
81
+ return;
82
+ }
83
+ span.endTime = Date.now();
84
+ span.duration = span.endTime - span.startTime;
85
+ span.status = 'completed';
86
+ if (attributes) {
87
+ span.attributes = { ...span.attributes, ...attributes };
88
+ }
89
+ this.emit('span_completed', span);
90
+ this.emit('observability_event', { type: 'span_completed', span });
91
+ }
92
+ /**
93
+ * Record an error on a span
94
+ */
95
+ errorSpan(spanId, error, attributes) {
96
+ const span = this.traces.get(spanId);
97
+ if (!span) {
98
+ console.warn(`[Tracer] span ${spanId} not found`);
99
+ return;
100
+ }
101
+ span.endTime = Date.now();
102
+ span.duration = span.endTime - span.startTime;
103
+ span.status = 'error';
104
+ span.error = error;
105
+ if (attributes) {
106
+ span.attributes = { ...span.attributes, ...attributes };
107
+ }
108
+ this.emit('error', spanId, error);
109
+ this.emit('observability_event', { type: 'error', spanId, error });
110
+ }
111
+ /**
112
+ * Record a route decision
113
+ */
114
+ recordRoute(trace) {
115
+ this.routeTraces.push(trace);
116
+ // Emit event
117
+ this.emit('route_complete', trace);
118
+ this.emit('observability_event', { type: 'route_complete', trace });
119
+ // Export to Langfuse async
120
+ if (this.langfuseClient) {
121
+ this.langfuseClient.trace({
122
+ name: 'route_decision',
123
+ traceId: trace.traceId,
124
+ timestamp: new Date(trace.timestamp).toISOString(),
125
+ input: { query: trace.query },
126
+ output: {
127
+ model: trace.model,
128
+ provider: trace.provider,
129
+ tokens: trace.queryTokens + trace.responseTokens,
130
+ latencyMs: trace.latencyMs,
131
+ cost: trace.cost,
132
+ },
133
+ metadata: {
134
+ queryTokens: trace.queryTokens,
135
+ responseTokens: trace.responseTokens,
136
+ cacheHit: trace.cacheHit,
137
+ complexity: trace.complexity,
138
+ tier: trace.tier,
139
+ },
140
+ }).catch(() => { }); // Fire and forget
141
+ }
142
+ }
143
+ /**
144
+ * Generate a unique trace ID
145
+ */
146
+ generateTraceId() {
147
+ return generateId();
148
+ }
149
+ /**
150
+ * Get all route traces
151
+ */
152
+ getTraces(limit) {
153
+ if (limit) {
154
+ return this.routeTraces.slice(-limit);
155
+ }
156
+ return [...this.routeTraces];
157
+ }
158
+ /**
159
+ * Get all spans for a trace
160
+ */
161
+ getSpans(traceId) {
162
+ return Array.from(this.traces.values()).filter(s => s.traceId === traceId);
163
+ }
164
+ /**
165
+ * Export to Langfuse (async, non-blocking)
166
+ */
167
+ async flushToLangfuse() {
168
+ if (this.langfuseClient) {
169
+ await this.langfuseClient.flush();
170
+ }
171
+ }
172
+ /**
173
+ * Clear all traces (for testing)
174
+ */
175
+ reset() {
176
+ this.traces.clear();
177
+ this.routeTraces = [];
178
+ }
179
+ }
180
+ // Singleton instance
181
+ let tracerInstance = null;
182
+ function getTracer() {
183
+ if (!tracerInstance) {
184
+ tracerInstance = new Tracer();
185
+ }
186
+ return tracerInstance;
187
+ }
188
+ function createTracer() {
189
+ tracerInstance = new Tracer();
190
+ return tracerInstance;
191
+ }
192
+ //# sourceMappingURL=tracer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracer.js","sourceRoot":"","sources":["../../src/observability/tracer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ export interface Span {
2
+ traceId: string;
3
+ spanId: string;
4
+ parentSpanId?: string;
5
+ operationName: string;
6
+ startTime: number;
7
+ endTime?: number;
8
+ duration?: number;
9
+ attributes: Record<string, string | number | boolean>;
10
+ status: 'started' | 'completed' | 'error';
11
+ error?: string;
12
+ }
13
+ export interface Metric {
14
+ name: string;
15
+ value: number;
16
+ timestamp: number;
17
+ labels: Record<string, string>;
18
+ type: 'counter' | 'gauge' | 'histogram';
19
+ }
20
+ export interface RouteTrace {
21
+ traceId: string;
22
+ timestamp: number;
23
+ query: string;
24
+ queryTokens: number;
25
+ model: string;
26
+ provider: string;
27
+ responseTokens: number;
28
+ latencyMs: number;
29
+ cost: number;
30
+ cacheHit: boolean;
31
+ complexity: number;
32
+ tier: 'budget' | 'standard' | 'premium';
33
+ }
34
+ export type ObservabilityEvent = {
35
+ type: 'route_complete';
36
+ trace: RouteTrace;
37
+ } | {
38
+ type: 'span_started';
39
+ span: Span;
40
+ } | {
41
+ type: 'span_completed';
42
+ span: Span;
43
+ } | {
44
+ type: 'budget_warning';
45
+ apiKey: string;
46
+ usedCents: number;
47
+ thresholdCents: number;
48
+ } | {
49
+ type: 'error';
50
+ spanId: string;
51
+ error: string;
52
+ };
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Observability types for A3M Router
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/observability/types.ts"],"names":[],"mappings":";AAAA,qCAAqC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adaptive-memory-multi-model-router",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "shortName": "A3M Router",
5
5
  "displayName": "A3M Router - Adaptive Memory Multi-Model Router",
6
6
  "description": "LLM router & AI gateway — 99.5% routing accuracy, 47 providers (DeepSeek, Kimi/Moonshot, Qwen, Zhipu GLM, Yi + more). Semantic cache, guardrails, cost analytics. Built on 30+ arXiv papers (SGLang, Medusa, MemoRAG). Zero ML, 19.5KB. TypeScript + Python SDK. MIT.",
package/src/index.ts CHANGED
@@ -84,6 +84,22 @@ export { GuardrailEngine } from './security/guardrails';
84
84
  export { CostAnalytics } from './analytics/costAnalytics';
85
85
  export { createProxyServer } from './server/proxyServer';
86
86
 
87
+ // ============================================================
88
+ // OBSERVABILITY
89
+ // ============================================================
90
+ export {
91
+ Tracer,
92
+ getTracer,
93
+ createTracer,
94
+ MetricsCollector,
95
+ getMetrics,
96
+ createMetricsCollector,
97
+ observabilityMiddleware,
98
+ observabilityPlugin,
99
+ budgetAlertMiddleware,
100
+ } from './observability';
101
+ export type { Span, Metric, RouteTrace, ObservabilityEvent } from './observability';
102
+
87
103
  // ============================================================
88
104
  // CONVENIENCE: Create a router instance
89
105
  // ============================================================
@@ -0,0 +1,10 @@
1
+ // Observability module exports
2
+
3
+ export * from './types';
4
+ export { Tracer, getTracer, createTracer } from './tracer';
5
+ export { MetricsCollector, getMetrics, createMetricsCollector } from './metrics';
6
+ export {
7
+ observabilityMiddleware,
8
+ observabilityPlugin,
9
+ budgetAlertMiddleware,
10
+ } from './middleware';
@@ -0,0 +1,215 @@
1
+ import { Metric } from './types';
2
+
3
+ interface HistogramBucket {
4
+ count: number;
5
+ sum: number;
6
+ min: number;
7
+ max: number;
8
+ }
9
+
10
+ function formatLabels(labels: Record<string, string>): string {
11
+ const parts: string[] = [];
12
+ for (const [key, value] of Object.entries(labels)) {
13
+ parts.push(`${key}="${value}"`);
14
+ }
15
+ return parts.join(',');
16
+ }
17
+
18
+ function metricKey(name: string, labels?: Record<string, string>): string {
19
+ if (!labels) return name;
20
+ return `${name}{${formatLabels(labels)}}`;
21
+ }
22
+
23
+ class MetricsCollector {
24
+ private counters: Map<string, number> = new Map();
25
+ private gauges: Map<string, number> = new Map();
26
+ private histograms: Map<string, Map<string, HistogramBucket>> = new Map();
27
+ private createdAt: number = Date.now();
28
+
29
+ /**
30
+ * Increment counter
31
+ */
32
+ incrementCounter(name: string, labels?: Record<string, string>, value: number = 1): void {
33
+ const key = metricKey(name, labels);
34
+ this.counters.set(key, (this.counters.get(key) || 0) + value);
35
+ }
36
+
37
+ /**
38
+ * Set gauge
39
+ */
40
+ setGauge(name: string, value: number, labels?: Record<string, string>): void {
41
+ const key = metricKey(name, labels);
42
+ this.gauges.set(key, value);
43
+ }
44
+
45
+ /**
46
+ * Record histogram value
47
+ */
48
+ recordHistogram(name: string, value: number, labels?: Record<string, string>): void {
49
+ const key = metricKey(name, labels);
50
+
51
+ if (!this.histograms.has(name)) {
52
+ this.histograms.set(name, new Map());
53
+ }
54
+
55
+ const buckets = this.histograms.get(name)!;
56
+ const bucket = buckets.get(key) || { count: 0, sum: 0, min: Infinity, max: -Infinity };
57
+
58
+ bucket.count++;
59
+ bucket.sum += value;
60
+ bucket.min = Math.min(bucket.min, value);
61
+ bucket.max = Math.max(bucket.max, value);
62
+
63
+ buckets.set(key, bucket);
64
+ }
65
+
66
+ /**
67
+ * Get metrics in Prometheus format
68
+ */
69
+ getPrometheusMetrics(): string {
70
+ const lines: string[] = [];
71
+ const timestamp = Date.now();
72
+
73
+ lines.push('# HELP a3m_router_info A3M Router info');
74
+ lines.push('# TYPE a3m_router_info gauge');
75
+ lines.push('a3m_router_info{router_version="2.0.0"} 1');
76
+
77
+ // Counters
78
+ lines.push('');
79
+ lines.push('# HELP a3m_router_requests_total Total number of router requests');
80
+ lines.push('# TYPE a3m_router_requests_total counter');
81
+
82
+ for (const [key, value] of this.counters) {
83
+ if (key.startsWith('a3m_router_') || key.startsWith('a3m_')) {
84
+ lines.push(`${key} ${value} ${timestamp}`);
85
+ }
86
+ }
87
+
88
+ // Cache counters
89
+ for (const [key, value] of this.counters) {
90
+ if (key.startsWith('cache_')) {
91
+ lines.push(`${key} ${value} ${timestamp}`);
92
+ }
93
+ }
94
+
95
+ // Gauges
96
+ lines.push('');
97
+ lines.push('# HELP a3m_active_providers Number of active providers');
98
+ lines.push('# TYPE a3m_active_providers gauge');
99
+
100
+ for (const [key, value] of this.gauges) {
101
+ lines.push(`${key} ${value} ${timestamp}`);
102
+ }
103
+
104
+ // Histograms
105
+ lines.push('');
106
+ lines.push('# HELP a3m_request_latency_seconds Request latency in seconds');
107
+ lines.push('# TYPE a3m_request_latency_seconds histogram');
108
+
109
+ for (const [name, buckets] of this.histograms) {
110
+ if (name.includes('latency')) {
111
+ for (const [bucketKey, bucket] of buckets) {
112
+ const labelStr = bucketKey.includes('{') ? bucketKey.slice(name.length) : '';
113
+ lines.push(`${name}_sum${labelStr} ${bucket.sum} ${timestamp}`);
114
+ lines.push(`${name}_count${labelStr} ${bucket.count} ${timestamp}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ lines.push('');
120
+ lines.push('# HELP a3m_request_cost_cents Request cost in cents');
121
+ lines.push('# TYPE a3m_request_cost_cents histogram');
122
+
123
+ for (const [name, buckets] of this.histograms) {
124
+ if (name.includes('cost')) {
125
+ for (const [bucketKey, bucket] of buckets) {
126
+ const labelStr = bucketKey.includes('{') ? bucketKey.slice(name.length) : '';
127
+ lines.push(`${name}_sum${labelStr} ${bucket.sum} ${timestamp}`);
128
+ lines.push(`${name}_count${labelStr} ${bucket.count} ${timestamp}`);
129
+ }
130
+ }
131
+ }
132
+
133
+ return lines.join('\n');
134
+ }
135
+
136
+ /**
137
+ * Get all metrics as Metric objects
138
+ */
139
+ getMetrics(): Metric[] {
140
+ const metrics: Metric[] = [];
141
+ const timestamp = Date.now();
142
+
143
+ for (const [key, value] of this.counters) {
144
+ metrics.push({
145
+ name: key,
146
+ value,
147
+ timestamp,
148
+ labels: this.extractLabels(key),
149
+ type: 'counter',
150
+ });
151
+ }
152
+
153
+ for (const [key, value] of this.gauges) {
154
+ metrics.push({
155
+ name: key,
156
+ value,
157
+ timestamp,
158
+ labels: this.extractLabels(key),
159
+ type: 'gauge',
160
+ });
161
+ }
162
+
163
+ for (const [name, buckets] of this.histograms) {
164
+ for (const [bucketKey, bucket] of buckets) {
165
+ metrics.push({
166
+ name: bucketKey,
167
+ value: bucket.count,
168
+ timestamp,
169
+ labels: this.extractLabels(bucketKey),
170
+ type: 'histogram',
171
+ });
172
+ }
173
+ }
174
+
175
+ return metrics;
176
+ }
177
+
178
+ private extractLabels(metricKey: string): Record<string, string> {
179
+ const labels: Record<string, string> = {};
180
+ const match = metricKey.match(/\{(.+)\}/);
181
+ if (match) {
182
+ const pairs = match[1].split(',');
183
+ for (const pair of pairs) {
184
+ const [key, value] = pair.split('=');
185
+ labels[key] = value.replace(/"/g, '');
186
+ }
187
+ }
188
+ return labels;
189
+ }
190
+
191
+ /**
192
+ * Clear metrics
193
+ */
194
+ reset(): void {
195
+ this.counters.clear();
196
+ this.gauges.clear();
197
+ this.histograms.clear();
198
+ this.createdAt = Date.now();
199
+ }
200
+ }
201
+
202
+ // Singleton instance
203
+ let metricsInstance: MetricsCollector | null = null;
204
+
205
+ export function getMetrics(): MetricsCollector {
206
+ if (!metricsInstance) {
207
+ metricsInstance = new MetricsCollector();
208
+ }
209
+ return metricsInstance;
210
+ }
211
+
212
+ export function createMetricsCollector(): MetricsCollector {
213
+ metricsInstance = new MetricsCollector();
214
+ return metricsInstance;
215
+ }