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