@lelemondev/sdk 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -6,10 +6,26 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
6
6
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
7
7
 
8
8
  // src/transport.ts
9
+ var DEFAULT_BATCH_SIZE = 10;
10
+ var DEFAULT_FLUSH_INTERVAL_MS = 1e3;
11
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
9
12
  var Transport = class {
10
13
  constructor(config) {
11
14
  __publicField(this, "config");
12
- this.config = config;
15
+ __publicField(this, "queue", []);
16
+ __publicField(this, "flushPromise", null);
17
+ __publicField(this, "flushTimer", null);
18
+ __publicField(this, "pendingResolvers", /* @__PURE__ */ new Map());
19
+ __publicField(this, "idCounter", 0);
20
+ this.config = {
21
+ apiKey: config.apiKey,
22
+ endpoint: config.endpoint,
23
+ debug: config.debug,
24
+ disabled: config.disabled,
25
+ batchSize: config.batchSize ?? DEFAULT_BATCH_SIZE,
26
+ flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
27
+ requestTimeoutMs: config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
28
+ };
13
29
  }
14
30
  /**
15
31
  * Check if transport is enabled
@@ -18,53 +34,171 @@ var Transport = class {
18
34
  return !this.config.disabled && !!this.config.apiKey;
19
35
  }
20
36
  /**
21
- * Create a new trace
37
+ * Enqueue trace creation (returns promise that resolves to trace ID)
22
38
  */
23
- async createTrace(data) {
24
- return this.request("POST", "/api/v1/traces", data);
39
+ enqueueCreate(data) {
40
+ if (this.config.disabled) {
41
+ return Promise.resolve(null);
42
+ }
43
+ const tempId = this.generateTempId();
44
+ return new Promise((resolve) => {
45
+ this.pendingResolvers.set(tempId, resolve);
46
+ this.enqueue({ type: "create", tempId, data });
47
+ });
25
48
  }
26
49
  /**
27
- * Complete a trace (success or error)
50
+ * Enqueue trace completion (fire-and-forget)
28
51
  */
29
- async completeTrace(traceId, data) {
30
- await this.request("PATCH", `/api/v1/traces/${traceId}`, data);
52
+ enqueueComplete(traceId, data) {
53
+ if (this.config.disabled || !traceId) {
54
+ return;
55
+ }
56
+ this.enqueue({ type: "complete", traceId, data });
31
57
  }
32
58
  /**
33
- * Make HTTP request to API
59
+ * Flush all pending items
60
+ * Safe to call multiple times (deduplicates)
34
61
  */
35
- async request(method, path, body) {
36
- if (this.config.disabled) {
37
- return {};
62
+ async flush() {
63
+ if (this.flushPromise) {
64
+ return this.flushPromise;
38
65
  }
39
- const url = `${this.config.endpoint}${path}`;
40
- if (this.config.debug) {
41
- console.log(`[Lelemon] ${method} ${url}`, body);
66
+ if (this.queue.length === 0) {
67
+ return;
68
+ }
69
+ this.cancelScheduledFlush();
70
+ const items = this.queue;
71
+ this.queue = [];
72
+ this.flushPromise = this.sendBatch(items).finally(() => {
73
+ this.flushPromise = null;
74
+ });
75
+ return this.flushPromise;
76
+ }
77
+ /**
78
+ * Get pending item count (for testing/debugging)
79
+ */
80
+ getPendingCount() {
81
+ return this.queue.length;
82
+ }
83
+ // ─────────────────────────────────────────────────────────────
84
+ // Private methods
85
+ // ─────────────────────────────────────────────────────────────
86
+ generateTempId() {
87
+ return `temp_${++this.idCounter}_${Date.now()}`;
88
+ }
89
+ enqueue(item) {
90
+ this.queue.push(item);
91
+ if (this.queue.length >= this.config.batchSize) {
92
+ this.flush();
93
+ } else {
94
+ this.scheduleFlush();
95
+ }
96
+ }
97
+ scheduleFlush() {
98
+ if (this.flushTimer !== null) {
99
+ return;
100
+ }
101
+ this.flushTimer = setTimeout(() => {
102
+ this.flushTimer = null;
103
+ this.flush();
104
+ }, this.config.flushIntervalMs);
105
+ }
106
+ cancelScheduledFlush() {
107
+ if (this.flushTimer !== null) {
108
+ clearTimeout(this.flushTimer);
109
+ this.flushTimer = null;
110
+ }
111
+ }
112
+ async sendBatch(items) {
113
+ const payload = {
114
+ creates: [],
115
+ completes: []
116
+ };
117
+ for (const item of items) {
118
+ if (item.type === "create") {
119
+ payload.creates.push({ tempId: item.tempId, data: item.data });
120
+ } else {
121
+ payload.completes.push({ traceId: item.traceId, data: item.data });
122
+ }
123
+ }
124
+ if (payload.creates.length === 0 && payload.completes.length === 0) {
125
+ return;
126
+ }
127
+ this.log("Sending batch", {
128
+ creates: payload.creates.length,
129
+ completes: payload.completes.length
130
+ });
131
+ try {
132
+ const response = await this.request(
133
+ "POST",
134
+ "/api/v1/traces/batch",
135
+ payload
136
+ );
137
+ if (response.created) {
138
+ for (const [tempId, realId] of Object.entries(response.created)) {
139
+ const resolver = this.pendingResolvers.get(tempId);
140
+ if (resolver) {
141
+ resolver(realId);
142
+ this.pendingResolvers.delete(tempId);
143
+ }
144
+ }
145
+ }
146
+ if (response.errors?.length && this.config.debug) {
147
+ console.warn("[Lelemon] Batch errors:", response.errors);
148
+ }
149
+ } catch (error) {
150
+ for (const item of items) {
151
+ if (item.type === "create") {
152
+ const resolver = this.pendingResolvers.get(item.tempId);
153
+ if (resolver) {
154
+ resolver(null);
155
+ this.pendingResolvers.delete(item.tempId);
156
+ }
157
+ }
158
+ }
159
+ this.log("Batch failed", error);
42
160
  }
161
+ }
162
+ async request(method, path, body) {
163
+ const url = `${this.config.endpoint}${path}`;
164
+ const controller = new AbortController();
165
+ const timeoutId = setTimeout(() => {
166
+ controller.abort();
167
+ }, this.config.requestTimeoutMs);
43
168
  try {
44
169
  const response = await fetch(url, {
45
170
  method,
46
171
  headers: {
47
172
  "Content-Type": "application/json",
48
- Authorization: `Bearer ${this.config.apiKey}`
173
+ "Authorization": `Bearer ${this.config.apiKey}`
49
174
  },
50
- body: body ? JSON.stringify(body) : void 0
175
+ body: body ? JSON.stringify(body) : void 0,
176
+ signal: controller.signal
51
177
  });
178
+ clearTimeout(timeoutId);
52
179
  if (!response.ok) {
53
- const error = await response.text();
54
- throw new Error(`Lelemon API error: ${response.status} ${error}`);
180
+ const errorText = await response.text().catch(() => "Unknown error");
181
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
55
182
  }
56
183
  const text = await response.text();
57
- if (!text) {
58
- return {};
59
- }
60
- return JSON.parse(text);
184
+ return text ? JSON.parse(text) : {};
61
185
  } catch (error) {
62
- if (this.config.debug) {
63
- console.error("[Lelemon] Request failed:", error);
186
+ clearTimeout(timeoutId);
187
+ if (error instanceof Error && error.name === "AbortError") {
188
+ throw new Error(`Request timeout after ${this.config.requestTimeoutMs}ms`);
64
189
  }
65
190
  throw error;
66
191
  }
67
192
  }
193
+ log(message, data) {
194
+ if (this.config.debug) {
195
+ if (data !== void 0) {
196
+ console.log(`[Lelemon] ${message}`, data);
197
+ } else {
198
+ console.log(`[Lelemon] ${message}`);
199
+ }
200
+ }
201
+ }
68
202
  };
69
203
 
70
204
  // src/parser.ts
@@ -373,176 +507,185 @@ function init(config = {}) {
373
507
  globalConfig = config;
374
508
  globalTransport = createTransport(config);
375
509
  }
376
- function createTransport(config) {
377
- const apiKey = config.apiKey ?? getEnvVar("LELEMON_API_KEY");
378
- if (!apiKey && !config.disabled) {
379
- console.warn(
380
- "[Lelemon] No API key provided. Set apiKey in config or LELEMON_API_KEY env var. Tracing disabled."
381
- );
382
- }
383
- return new Transport({
384
- apiKey: apiKey ?? "",
385
- endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
386
- debug: config.debug ?? false,
387
- disabled: config.disabled ?? !apiKey
388
- });
510
+ function trace(options) {
511
+ const transport = getTransport();
512
+ const debug = globalConfig.debug ?? false;
513
+ const disabled = globalConfig.disabled ?? !transport.isEnabled();
514
+ return new Trace(options, transport, debug, disabled);
389
515
  }
390
- function getTransport() {
391
- if (!globalTransport) {
392
- globalTransport = createTransport(globalConfig);
516
+ async function flush() {
517
+ if (globalTransport) {
518
+ await globalTransport.flush();
393
519
  }
394
- return globalTransport;
395
520
  }
396
- function getEnvVar(name) {
397
- if (typeof process !== "undefined" && process.env) {
398
- return process.env[name];
399
- }
400
- return void 0;
521
+ function isEnabled() {
522
+ return getTransport().isEnabled();
401
523
  }
402
524
  var Trace = class {
403
525
  constructor(options, transport, debug, disabled) {
404
526
  __publicField(this, "id", null);
527
+ __publicField(this, "idPromise");
405
528
  __publicField(this, "transport");
406
- __publicField(this, "options");
407
529
  __publicField(this, "startTime");
408
- __publicField(this, "completed", false);
409
530
  __publicField(this, "debug");
410
531
  __publicField(this, "disabled");
532
+ __publicField(this, "completed", false);
411
533
  __publicField(this, "llmCalls", []);
412
- this.options = options;
413
534
  this.transport = transport;
414
535
  this.startTime = Date.now();
415
536
  this.debug = debug;
416
537
  this.disabled = disabled;
417
- }
418
- /**
419
- * Initialize trace on server (called internally)
420
- */
421
- async init() {
422
- if (this.disabled) return;
423
- try {
424
- const result = await this.transport.createTrace({
425
- name: this.options.name,
426
- sessionId: this.options.sessionId,
427
- userId: this.options.userId,
428
- input: this.options.input,
429
- metadata: this.options.metadata,
430
- tags: this.options.tags
538
+ if (disabled) {
539
+ this.idPromise = Promise.resolve(null);
540
+ } else {
541
+ this.idPromise = transport.enqueueCreate({
542
+ name: options.name,
543
+ sessionId: options.sessionId,
544
+ userId: options.userId,
545
+ input: options.input,
546
+ metadata: options.metadata,
547
+ tags: options.tags
548
+ });
549
+ this.idPromise.then((id) => {
550
+ this.id = id;
431
551
  });
432
- this.id = result.id;
433
- } catch (error) {
434
- if (this.debug) {
435
- console.error("[Lelemon] Failed to create trace:", error);
436
- }
437
552
  }
438
553
  }
439
554
  /**
440
- * Log an LLM response (optional - for tracking individual calls)
441
- * Use this if you want to track tokens per call, not just at the end
555
+ * Log an LLM response for token tracking
556
+ * Optional - use if you want per-call token counts
442
557
  */
443
558
  log(response) {
559
+ if (this.disabled || this.completed) return this;
444
560
  const parsed = parseResponse(response);
445
561
  if (parsed.model || parsed.inputTokens || parsed.outputTokens) {
446
562
  this.llmCalls.push(parsed);
447
563
  }
564
+ return this;
448
565
  }
449
566
  /**
450
- * Complete trace successfully
451
- * @param messages - The full message history (OpenAI/Anthropic format)
567
+ * Complete trace successfully (fire-and-forget)
568
+ *
569
+ * @param messages - Full message history (OpenAI/Anthropic format)
452
570
  */
453
- async success(messages) {
454
- if (this.completed) return;
571
+ success(messages) {
572
+ if (this.completed || this.disabled) return;
455
573
  this.completed = true;
456
- if (this.disabled || !this.id) return;
457
574
  const durationMs = Date.now() - this.startTime;
458
575
  const parsed = parseMessages(messages);
459
576
  const allLLMCalls = [...this.llmCalls, ...parsed.llmCalls];
460
- let totalInputTokens = 0;
461
- let totalOutputTokens = 0;
462
- const models = /* @__PURE__ */ new Set();
463
- for (const call of allLLMCalls) {
464
- if (call.inputTokens) totalInputTokens += call.inputTokens;
465
- if (call.outputTokens) totalOutputTokens += call.outputTokens;
466
- if (call.model) models.add(call.model);
467
- }
468
- try {
469
- await this.transport.completeTrace(this.id, {
577
+ const { totalInputTokens, totalOutputTokens, models } = this.aggregateCalls(allLLMCalls);
578
+ this.idPromise.then((id) => {
579
+ if (!id) return;
580
+ this.transport.enqueueComplete(id, {
470
581
  status: "completed",
471
582
  output: parsed.output,
472
583
  systemPrompt: parsed.systemPrompt,
473
584
  llmCalls: allLLMCalls,
474
585
  toolCalls: parsed.toolCalls,
475
- models: Array.from(models),
586
+ models,
476
587
  totalInputTokens,
477
588
  totalOutputTokens,
478
589
  durationMs
479
590
  });
480
- } catch (err) {
481
- if (this.debug) {
482
- console.error("[Lelemon] Failed to complete trace:", err);
483
- }
484
- }
591
+ });
485
592
  }
486
593
  /**
487
- * Complete trace with error
594
+ * Complete trace with error (fire-and-forget)
595
+ *
488
596
  * @param error - The error that occurred
489
- * @param messages - The message history up to the failure (optional)
597
+ * @param messages - Optional message history up to failure
490
598
  */
491
- async error(error, messages) {
492
- if (this.completed) return;
599
+ error(error, messages) {
600
+ if (this.completed || this.disabled) return;
493
601
  this.completed = true;
494
- if (this.disabled || !this.id) return;
495
602
  const durationMs = Date.now() - this.startTime;
496
603
  const parsed = messages ? parseMessages(messages) : null;
497
604
  const errorObj = error instanceof Error ? error : new Error(String(error));
498
605
  const allLLMCalls = parsed ? [...this.llmCalls, ...parsed.llmCalls] : this.llmCalls;
606
+ const { totalInputTokens, totalOutputTokens, models } = this.aggregateCalls(allLLMCalls);
607
+ this.idPromise.then((id) => {
608
+ if (!id) return;
609
+ this.transport.enqueueComplete(id, {
610
+ status: "error",
611
+ errorMessage: errorObj.message,
612
+ errorStack: errorObj.stack,
613
+ output: parsed?.output,
614
+ systemPrompt: parsed?.systemPrompt,
615
+ llmCalls: allLLMCalls.length > 0 ? allLLMCalls : void 0,
616
+ toolCalls: parsed?.toolCalls,
617
+ models: models.length > 0 ? models : void 0,
618
+ totalInputTokens,
619
+ totalOutputTokens,
620
+ durationMs
621
+ });
622
+ });
623
+ }
624
+ /**
625
+ * Get the trace ID (may be null if not yet created or failed)
626
+ */
627
+ getId() {
628
+ return this.id;
629
+ }
630
+ /**
631
+ * Wait for trace ID to be available
632
+ */
633
+ async waitForId() {
634
+ return this.idPromise;
635
+ }
636
+ // ─────────────────────────────────────────────────────────────
637
+ // Private methods
638
+ // ─────────────────────────────────────────────────────────────
639
+ aggregateCalls(calls) {
499
640
  let totalInputTokens = 0;
500
641
  let totalOutputTokens = 0;
501
- const models = /* @__PURE__ */ new Set();
502
- for (const call of allLLMCalls) {
642
+ const modelSet = /* @__PURE__ */ new Set();
643
+ for (const call of calls) {
503
644
  if (call.inputTokens) totalInputTokens += call.inputTokens;
504
645
  if (call.outputTokens) totalOutputTokens += call.outputTokens;
505
- if (call.model) models.add(call.model);
646
+ if (call.model) modelSet.add(call.model);
506
647
  }
507
- const request = {
508
- status: "error",
509
- errorMessage: errorObj.message,
510
- errorStack: errorObj.stack,
511
- durationMs,
648
+ return {
512
649
  totalInputTokens,
513
650
  totalOutputTokens,
514
- models: Array.from(models)
651
+ models: Array.from(modelSet)
515
652
  };
516
- if (parsed) {
517
- request.output = parsed.output;
518
- request.systemPrompt = parsed.systemPrompt;
519
- request.llmCalls = allLLMCalls;
520
- request.toolCalls = parsed.toolCalls;
521
- }
522
- try {
523
- await this.transport.completeTrace(this.id, request);
524
- } catch (err) {
525
- if (this.debug) {
526
- console.error("[Lelemon] Failed to complete trace:", err);
527
- }
528
- }
529
653
  }
530
654
  };
531
- function trace(options) {
532
- const transport = getTransport();
533
- const debug = globalConfig.debug ?? false;
534
- const disabled = globalConfig.disabled ?? !transport.isEnabled();
535
- const t = new Trace(options, transport, debug, disabled);
536
- t.init().catch((err) => {
537
- if (debug) {
538
- console.error("[Lelemon] Trace init failed:", err);
539
- }
655
+ function getTransport() {
656
+ if (!globalTransport) {
657
+ globalTransport = createTransport(globalConfig);
658
+ }
659
+ return globalTransport;
660
+ }
661
+ function createTransport(config) {
662
+ const apiKey = config.apiKey ?? getEnvVar("LELEMON_API_KEY");
663
+ if (!apiKey && !config.disabled) {
664
+ console.warn(
665
+ "[Lelemon] No API key provided. Set apiKey in config or LELEMON_API_KEY env var. Tracing disabled."
666
+ );
667
+ }
668
+ return new Transport({
669
+ apiKey: apiKey ?? "",
670
+ endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
671
+ debug: config.debug ?? false,
672
+ disabled: config.disabled ?? !apiKey,
673
+ batchSize: config.batchSize,
674
+ flushIntervalMs: config.flushIntervalMs,
675
+ requestTimeoutMs: config.requestTimeoutMs
540
676
  });
541
- return t;
677
+ }
678
+ function getEnvVar(name) {
679
+ if (typeof process !== "undefined" && process.env) {
680
+ return process.env[name];
681
+ }
682
+ return void 0;
542
683
  }
543
684
 
544
685
  exports.Trace = Trace;
686
+ exports.flush = flush;
545
687
  exports.init = init;
688
+ exports.isEnabled = isEnabled;
546
689
  exports.parseBedrockResponse = parseBedrockResponse;
547
690
  exports.parseMessages = parseMessages;
548
691
  exports.parseResponse = parseResponse;