bloop-sdk 0.2.0 → 0.3.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/bloop.d.ts CHANGED
@@ -20,21 +20,153 @@ export interface ErrorEvent {
20
20
  userIdHash?: string;
21
21
  metadata?: Record<string, unknown>;
22
22
  }
23
+ export type SpanType = "generation" | "tool" | "retrieval" | "custom";
24
+ export type SpanStatus = "ok" | "error";
25
+ export type TraceStatus = "running" | "completed" | "error";
26
+ export interface TraceOptions {
27
+ name: string;
28
+ sessionId?: string;
29
+ userId?: string;
30
+ input?: string;
31
+ metadata?: Record<string, unknown>;
32
+ promptName?: string;
33
+ promptVersion?: string;
34
+ }
35
+ export interface SpanOptions {
36
+ spanType: SpanType;
37
+ name?: string;
38
+ model?: string;
39
+ provider?: string;
40
+ input?: string;
41
+ metadata?: Record<string, unknown>;
42
+ }
43
+ export interface SpanEndOptions {
44
+ inputTokens?: number;
45
+ outputTokens?: number;
46
+ cost?: number;
47
+ status: SpanStatus;
48
+ errorMessage?: string;
49
+ output?: string;
50
+ timeToFirstTokenMs?: number;
51
+ }
52
+ export interface TraceEndOptions {
53
+ status: TraceStatus;
54
+ output?: string;
55
+ }
56
+ /** Options for the traceGeneration convenience method. */
57
+ export interface TraceGenerationOptions {
58
+ name: string;
59
+ model?: string;
60
+ provider?: string;
61
+ input?: string;
62
+ sessionId?: string;
63
+ userId?: string;
64
+ metadata?: Record<string, unknown>;
65
+ }
66
+ export declare class Span {
67
+ id: string;
68
+ parentSpanId: string | null;
69
+ spanType: SpanType;
70
+ name: string;
71
+ model?: string;
72
+ provider?: string;
73
+ startedAt: number;
74
+ input?: string;
75
+ metadata?: Record<string, unknown>;
76
+ inputTokens?: number;
77
+ outputTokens?: number;
78
+ cost?: number;
79
+ latencyMs?: number;
80
+ timeToFirstTokenMs?: number;
81
+ status?: SpanStatus;
82
+ errorMessage?: string;
83
+ output?: string;
84
+ constructor(opts: SpanOptions & {
85
+ parentSpanId?: string | null;
86
+ });
87
+ /** End this span and record metrics. */
88
+ end(opts: SpanEndOptions): void;
89
+ /** Serialize to server-expected format with snake_case keys. */
90
+ toJSON(): object;
91
+ }
92
+ export declare class Trace {
93
+ id: string;
94
+ name: string;
95
+ sessionId?: string;
96
+ userId?: string;
97
+ status: TraceStatus;
98
+ input?: string;
99
+ output?: string;
100
+ metadata?: Record<string, unknown>;
101
+ promptName?: string;
102
+ promptVersion?: string;
103
+ startedAt: number;
104
+ endedAt?: number;
105
+ spans: Span[];
106
+ private _client;
107
+ constructor(opts: TraceOptions, client: BloopClient);
108
+ /** Create a new span within this trace. */
109
+ startSpan(opts: SpanOptions): Span;
110
+ /** End this trace and push it to the client's trace buffer. */
111
+ end(opts: TraceEndOptions): void;
112
+ /** Serialize to server-expected format. */
113
+ private _toPayload;
114
+ }
115
+ export declare const MODEL_COSTS: Record<string, {
116
+ input: number;
117
+ output: number;
118
+ }>;
23
119
  export declare class BloopClient {
24
120
  private config;
25
121
  private buffer;
122
+ private traceBuffer;
26
123
  private timer;
27
124
  private signingKey;
28
125
  private keyReady;
126
+ private globalHandlersInstalled;
127
+ private _modelCosts;
29
128
  constructor(config: BloopConfig);
30
129
  /** Import the HMAC key using Web Crypto API. */
31
130
  private importKey;
131
+ /**
132
+ * Install global error handlers for uncaught exceptions and unhandled rejections.
133
+ * Detects runtime (Node.js vs browser) and installs appropriate handlers.
134
+ * Uses addEventListener (not assignment) to avoid clobbering existing handlers.
135
+ */
136
+ installGlobalHandlers(): void;
32
137
  /** Capture an Error object. */
33
138
  captureError(error: Error, context?: Partial<ErrorEvent>): void;
34
139
  /** Capture a structured error event. */
35
140
  capture(event: ErrorEvent): void;
141
+ /** Start a new trace. Returns a Trace object for adding spans. */
142
+ startTrace(opts: TraceOptions): Trace;
143
+ /**
144
+ * Convenience method: creates a trace with a single generation span,
145
+ * calls the provided function, and ends both span and trace.
146
+ * Returns the value returned by the callback.
147
+ */
148
+ traceGeneration<T>(opts: TraceGenerationOptions, fn: (span: Span) => Promise<T>): Promise<T>;
149
+ /** Set custom cost rates for a model (per-token in dollars). */
150
+ setModelCosts(model: string, costs: {
151
+ input: number;
152
+ output: number;
153
+ }): void;
154
+ /** Wrap an OpenAI-compatible client instance to automatically trace all LLM calls. */
155
+ wrapOpenAI<T extends object>(client: T): T;
156
+ /** Wrap an Anthropic client instance to automatically trace all messages.create calls. */
157
+ wrapAnthropic<T extends object>(client: T): T;
158
+ /** Internal: trace an OpenAI-style API call. */
159
+ private _traceOpenAICall;
160
+ /** Internal: trace an Anthropic messages.create call. */
161
+ private _traceAnthropicCall;
162
+ /** Push a completed trace payload to the buffer. Called by Trace.end(). */
163
+ private _pushTrace;
36
164
  /** Flush buffered events to the server. */
37
165
  flush(): Promise<void>;
166
+ /** Flush error events to the ingest endpoint. */
167
+ private _flushErrors;
168
+ /** Flush trace payloads to the traces endpoint. */
169
+ private _flushTraces;
38
170
  /** Express/Koa error middleware. */
39
171
  errorMiddleware(): (err: Error, req: any, _res: any, next: any) => void;
40
172
  /** Stop the flush timer. Call on process shutdown. */
package/dist/bloop.js CHANGED
@@ -1,8 +1,146 @@
1
+ // ---- Span Class ----
2
+ export class Span {
3
+ constructor(opts) {
4
+ this.id = crypto.randomUUID();
5
+ this.parentSpanId = opts.parentSpanId ?? null;
6
+ this.spanType = opts.spanType;
7
+ this.name = opts.name ?? opts.spanType;
8
+ this.model = opts.model;
9
+ this.provider = opts.provider;
10
+ this.startedAt = Date.now();
11
+ this.input = opts.input;
12
+ this.metadata = opts.metadata;
13
+ }
14
+ /** End this span and record metrics. */
15
+ end(opts) {
16
+ this.latencyMs = Date.now() - this.startedAt;
17
+ this.status = opts.status;
18
+ this.inputTokens = opts.inputTokens;
19
+ this.outputTokens = opts.outputTokens;
20
+ this.cost = opts.cost;
21
+ this.errorMessage = opts.errorMessage;
22
+ this.output = opts.output;
23
+ this.timeToFirstTokenMs = opts.timeToFirstTokenMs;
24
+ }
25
+ /** Serialize to server-expected format with snake_case keys. */
26
+ toJSON() {
27
+ return {
28
+ id: this.id,
29
+ parent_span_id: this.parentSpanId,
30
+ span_type: this.spanType,
31
+ name: this.name,
32
+ model: this.model ?? null,
33
+ provider: this.provider ?? null,
34
+ input: this.input ?? null,
35
+ output: this.output ?? null,
36
+ metadata: this.metadata ?? null,
37
+ started_at: this.startedAt,
38
+ input_tokens: this.inputTokens ?? null,
39
+ output_tokens: this.outputTokens ?? null,
40
+ cost: this.cost ?? null,
41
+ latency_ms: this.latencyMs ?? null,
42
+ time_to_first_token_ms: this.timeToFirstTokenMs ?? null,
43
+ status: this.status ?? null,
44
+ error_message: this.errorMessage ?? null,
45
+ };
46
+ }
47
+ }
48
+ export class Trace {
49
+ constructor(opts, client) {
50
+ this.status = "running";
51
+ this.spans = [];
52
+ this.id = crypto.randomUUID();
53
+ this.name = opts.name;
54
+ this.sessionId = opts.sessionId;
55
+ this.userId = opts.userId;
56
+ this.input = opts.input;
57
+ this.metadata = opts.metadata;
58
+ this.promptName = opts.promptName;
59
+ this.promptVersion = opts.promptVersion;
60
+ this.startedAt = Date.now();
61
+ this._client = client;
62
+ }
63
+ /** Create a new span within this trace. */
64
+ startSpan(opts) {
65
+ const span = new Span({
66
+ ...opts,
67
+ parentSpanId: null,
68
+ });
69
+ this.spans.push(span);
70
+ return span;
71
+ }
72
+ /** End this trace and push it to the client's trace buffer. */
73
+ end(opts) {
74
+ this.endedAt = Date.now();
75
+ this.status = opts.status;
76
+ if (opts.output !== undefined) {
77
+ this.output = opts.output;
78
+ }
79
+ this._client["_pushTrace"](this._toPayload());
80
+ }
81
+ /** Serialize to server-expected format. */
82
+ _toPayload() {
83
+ return {
84
+ id: this.id,
85
+ session_id: this.sessionId ?? null,
86
+ user_id: this.userId ?? null,
87
+ name: this.name,
88
+ status: this.status,
89
+ input: this.input ?? null,
90
+ output: this.output ?? null,
91
+ metadata: this.metadata ?? null,
92
+ prompt_name: this.promptName ?? null,
93
+ prompt_version: this.promptVersion ?? null,
94
+ started_at: this.startedAt,
95
+ ended_at: this.endedAt ?? null,
96
+ spans: this.spans.map((s) => s.toJSON()),
97
+ };
98
+ }
99
+ }
100
+ // ---- Auto-Instrumentation: Provider Detection ----
101
+ const PROVIDER_MAP = {
102
+ "api.openai.com": "openai",
103
+ "api.anthropic.com": "anthropic",
104
+ "api.minimax.io": "minimax",
105
+ "api.minimaxi.com": "minimax",
106
+ "api.moonshot.ai": "kimi",
107
+ "generativelanguage.googleapis.com": "google",
108
+ };
109
+ function detectProvider(client) {
110
+ try {
111
+ const baseURL = client?.baseURL || client?._options?.baseURL || "";
112
+ const hostname = new URL(baseURL).hostname;
113
+ return PROVIDER_MAP[hostname] || hostname;
114
+ }
115
+ catch {
116
+ return "unknown";
117
+ }
118
+ }
119
+ // ---- Auto-Instrumentation: Model Pricing ----
120
+ export const MODEL_COSTS = {
121
+ // OpenAI (per token in dollars)
122
+ "gpt-4o": { input: 2.50 / 1000000, output: 10.00 / 1000000 },
123
+ "gpt-4o-mini": { input: 0.15 / 1000000, output: 0.60 / 1000000 },
124
+ "gpt-4-turbo": { input: 10.00 / 1000000, output: 30.00 / 1000000 },
125
+ // Anthropic
126
+ "claude-sonnet-4-5-20250929": { input: 3.00 / 1000000, output: 15.00 / 1000000 },
127
+ "claude-haiku-4-5-20251001": { input: 0.80 / 1000000, output: 4.00 / 1000000 },
128
+ // Minimax
129
+ "MiniMax-M1": { input: 0.40 / 1000000, output: 2.20 / 1000000 },
130
+ "MiniMax-Text-01": { input: 0.20 / 1000000, output: 1.10 / 1000000 },
131
+ // Kimi
132
+ "kimi-k2": { input: 0.60 / 1000000, output: 2.50 / 1000000 },
133
+ "moonshot-v1-8k": { input: 0.20 / 1000000, output: 2.00 / 1000000 },
134
+ };
135
+ // ---- BloopClient ----
1
136
  export class BloopClient {
2
137
  constructor(config) {
3
138
  this.buffer = [];
139
+ this.traceBuffer = [];
4
140
  this.timer = null;
5
141
  this.signingKey = null;
142
+ this.globalHandlersInstalled = false;
143
+ this._modelCosts = {};
6
144
  this.config = {
7
145
  source: "api",
8
146
  maxBufferSize: 20,
@@ -19,6 +157,49 @@ export class BloopClient {
19
157
  const encoder = new TextEncoder();
20
158
  this.signingKey = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
21
159
  }
160
+ /**
161
+ * Install global error handlers for uncaught exceptions and unhandled rejections.
162
+ * Detects runtime (Node.js vs browser) and installs appropriate handlers.
163
+ * Uses addEventListener (not assignment) to avoid clobbering existing handlers.
164
+ */
165
+ installGlobalHandlers() {
166
+ if (this.globalHandlersInstalled)
167
+ return;
168
+ this.globalHandlersInstalled = true;
169
+ const isNode = typeof globalThis !== "undefined" &&
170
+ typeof globalThis.process?.on === "function";
171
+ if (isNode) {
172
+ const proc = globalThis.process;
173
+ proc.on("uncaughtException", (error) => {
174
+ this.captureError(error, {
175
+ metadata: { unhandled: true, mechanism: "uncaughtException" },
176
+ });
177
+ this.flush();
178
+ });
179
+ proc.on("unhandledRejection", (reason) => {
180
+ const error = reason instanceof Error ? reason : new Error(String(reason));
181
+ this.captureError(error, {
182
+ metadata: { unhandled: true, mechanism: "unhandledRejection" },
183
+ });
184
+ });
185
+ }
186
+ else if (typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function") {
187
+ globalThis.addEventListener("error", (event) => {
188
+ const error = event.error instanceof Error ? event.error : new Error(event.message || "Unknown error");
189
+ this.captureError(error, {
190
+ metadata: { unhandled: true, mechanism: "onerror" },
191
+ });
192
+ });
193
+ globalThis.addEventListener("unhandledrejection", (event) => {
194
+ const error = event.reason instanceof Error
195
+ ? event.reason
196
+ : new Error(String(event.reason));
197
+ this.captureError(error, {
198
+ metadata: { unhandled: true, mechanism: "onunhandledrejection" },
199
+ });
200
+ });
201
+ }
202
+ }
22
203
  /** Capture an Error object. */
23
204
  captureError(error, context) {
24
205
  this.capture({
@@ -55,8 +236,208 @@ export class BloopClient {
55
236
  this.flush();
56
237
  }
57
238
  }
239
+ // ---- LLM Tracing ----
240
+ /** Start a new trace. Returns a Trace object for adding spans. */
241
+ startTrace(opts) {
242
+ return new Trace(opts, this);
243
+ }
244
+ /**
245
+ * Convenience method: creates a trace with a single generation span,
246
+ * calls the provided function, and ends both span and trace.
247
+ * Returns the value returned by the callback.
248
+ */
249
+ async traceGeneration(opts, fn) {
250
+ const trace = this.startTrace({
251
+ name: opts.name,
252
+ sessionId: opts.sessionId,
253
+ userId: opts.userId,
254
+ input: opts.input,
255
+ metadata: opts.metadata,
256
+ });
257
+ const span = trace.startSpan({
258
+ spanType: "generation",
259
+ name: opts.name,
260
+ model: opts.model,
261
+ provider: opts.provider,
262
+ input: opts.input,
263
+ });
264
+ try {
265
+ const result = await fn(span);
266
+ trace.end({ status: "completed", output: span.output });
267
+ return result;
268
+ }
269
+ catch (err) {
270
+ if (!span.status) {
271
+ span.end({
272
+ status: "error",
273
+ errorMessage: err instanceof Error ? err.message : String(err),
274
+ });
275
+ }
276
+ trace.end({ status: "error" });
277
+ throw err;
278
+ }
279
+ }
280
+ // ---- Auto-Instrumentation ----
281
+ /** Set custom cost rates for a model (per-token in dollars). */
282
+ setModelCosts(model, costs) {
283
+ this._modelCosts[model] = costs;
284
+ }
285
+ /** Wrap an OpenAI-compatible client instance to automatically trace all LLM calls. */
286
+ wrapOpenAI(client) {
287
+ const bloop = this;
288
+ const provider = detectProvider(client);
289
+ return new Proxy(client, {
290
+ get(target, prop, receiver) {
291
+ const val = Reflect.get(target, prop, receiver);
292
+ if (prop === "chat") {
293
+ return new Proxy(val, {
294
+ get(chatTarget, chatProp, chatReceiver) {
295
+ const chatVal = Reflect.get(chatTarget, chatProp, chatReceiver);
296
+ if (chatProp === "completions") {
297
+ return new Proxy(chatVal, {
298
+ get(compTarget, compProp, compReceiver) {
299
+ const compVal = Reflect.get(compTarget, compProp, compReceiver);
300
+ if (compProp === "create" && typeof compVal === "function") {
301
+ return async function (...args) {
302
+ return bloop._traceOpenAICall(provider, "chat.completions.create", compVal.bind(compTarget), args);
303
+ };
304
+ }
305
+ return compVal;
306
+ },
307
+ });
308
+ }
309
+ return chatVal;
310
+ },
311
+ });
312
+ }
313
+ if (prop === "embeddings") {
314
+ return new Proxy(val, {
315
+ get(embTarget, embProp, embReceiver) {
316
+ const embVal = Reflect.get(embTarget, embProp, embReceiver);
317
+ if (embProp === "create" && typeof embVal === "function") {
318
+ return async function (...args) {
319
+ return bloop._traceOpenAICall(provider, "embeddings.create", embVal.bind(embTarget), args);
320
+ };
321
+ }
322
+ return embVal;
323
+ },
324
+ });
325
+ }
326
+ return val;
327
+ },
328
+ });
329
+ }
330
+ /** Wrap an Anthropic client instance to automatically trace all messages.create calls. */
331
+ wrapAnthropic(client) {
332
+ const bloop = this;
333
+ return new Proxy(client, {
334
+ get(target, prop, receiver) {
335
+ const val = Reflect.get(target, prop, receiver);
336
+ if (prop === "messages") {
337
+ return new Proxy(val, {
338
+ get(msgTarget, msgProp, msgReceiver) {
339
+ const msgVal = Reflect.get(msgTarget, msgProp, msgReceiver);
340
+ if (msgProp === "create" && typeof msgVal === "function") {
341
+ return async function (...args) {
342
+ return bloop._traceAnthropicCall(msgVal.bind(msgTarget), args);
343
+ };
344
+ }
345
+ return msgVal;
346
+ },
347
+ });
348
+ }
349
+ return val;
350
+ },
351
+ });
352
+ }
353
+ /** Internal: trace an OpenAI-style API call. */
354
+ async _traceOpenAICall(provider, method, fn, args) {
355
+ const params = args[0] || {};
356
+ const model = params.model || "unknown";
357
+ const traceName = `${provider}.${method}`;
358
+ const trace = this.startTrace({ name: traceName });
359
+ const span = trace.startSpan({
360
+ spanType: "generation",
361
+ name: traceName,
362
+ model,
363
+ provider,
364
+ input: params.messages
365
+ ? JSON.stringify(params.messages.slice(-1))
366
+ : undefined,
367
+ });
368
+ try {
369
+ const response = await fn(...args);
370
+ // Extract usage from response
371
+ const usage = response?.usage;
372
+ const inputTokens = usage?.prompt_tokens || 0;
373
+ const outputTokens = usage?.completion_tokens || 0;
374
+ // Compute cost
375
+ const rates = this._modelCosts[model] || MODEL_COSTS[model];
376
+ let cost = 0;
377
+ if (rates) {
378
+ cost = inputTokens * rates.input + outputTokens * rates.output;
379
+ }
380
+ // Extract output
381
+ const output = response?.choices?.[0]?.message?.content;
382
+ span.end({ inputTokens, outputTokens, cost, status: "ok", output });
383
+ trace.end({ status: "completed", output });
384
+ return response;
385
+ }
386
+ catch (err) {
387
+ span.end({
388
+ status: "error",
389
+ errorMessage: err?.message || String(err),
390
+ });
391
+ trace.end({ status: "error" });
392
+ throw err;
393
+ }
394
+ }
395
+ /** Internal: trace an Anthropic messages.create call. */
396
+ async _traceAnthropicCall(fn, args) {
397
+ const params = args[0] || {};
398
+ const model = params.model || "unknown";
399
+ const trace = this.startTrace({ name: "anthropic.messages.create" });
400
+ const span = trace.startSpan({
401
+ spanType: "generation",
402
+ name: "anthropic.messages.create",
403
+ model,
404
+ provider: "anthropic",
405
+ });
406
+ try {
407
+ const response = await fn(...args);
408
+ const inputTokens = response?.usage?.input_tokens || 0;
409
+ const outputTokens = response?.usage?.output_tokens || 0;
410
+ const rates = this._modelCosts[model] || MODEL_COSTS[model];
411
+ let cost = 0;
412
+ if (rates) {
413
+ cost = inputTokens * rates.input + outputTokens * rates.output;
414
+ }
415
+ const output = response?.content?.[0]?.text;
416
+ span.end({ inputTokens, outputTokens, cost, status: "ok", output });
417
+ trace.end({ status: "completed", output });
418
+ return response;
419
+ }
420
+ catch (err) {
421
+ span.end({
422
+ status: "error",
423
+ errorMessage: err?.message || String(err),
424
+ });
425
+ trace.end({ status: "error" });
426
+ throw err;
427
+ }
428
+ }
429
+ /** Push a completed trace payload to the buffer. Called by Trace.end(). */
430
+ _pushTrace(payload) {
431
+ this.traceBuffer.push(payload);
432
+ }
58
433
  /** Flush buffered events to the server. */
59
434
  async flush() {
435
+ const flushErrors = this._flushErrors();
436
+ const flushTraces = this._flushTraces();
437
+ await Promise.all([flushErrors, flushTraces]);
438
+ }
439
+ /** Flush error events to the ingest endpoint. */
440
+ async _flushErrors() {
60
441
  if (this.buffer.length === 0)
61
442
  return;
62
443
  // Ensure key is ready
@@ -90,6 +471,39 @@ export class BloopClient {
90
471
  }
91
472
  }
92
473
  }
474
+ /** Flush trace payloads to the traces endpoint. */
475
+ async _flushTraces() {
476
+ if (this.traceBuffer.length === 0)
477
+ return;
478
+ // Ensure key is ready
479
+ await this.keyReady;
480
+ const traces = this.traceBuffer.splice(0);
481
+ const body = JSON.stringify({ traces });
482
+ const signature = await this.sign(body);
483
+ const headers = {
484
+ "Content-Type": "application/json",
485
+ "X-Signature": signature,
486
+ };
487
+ if (this.config.projectKey) {
488
+ headers["X-Project-Key"] = this.config.projectKey;
489
+ }
490
+ try {
491
+ const resp = await fetch(`${this.config.endpoint}/v1/traces/batch`, {
492
+ method: "POST",
493
+ headers,
494
+ body,
495
+ });
496
+ if (!resp.ok) {
497
+ console.warn(`[bloop] trace flush failed: ${resp.status}`);
498
+ }
499
+ }
500
+ catch (err) {
501
+ if (typeof globalThis !== "undefined" &&
502
+ globalThis.process?.env?.NODE_ENV !== "production") {
503
+ console.warn("[bloop] trace flush error:", err);
504
+ }
505
+ }
506
+ }
93
507
  /** Express/Koa error middleware. */
94
508
  errorMiddleware() {
95
509
  return (err, req, _res, next) => {
@@ -135,9 +549,29 @@ class HttpError extends Error {
135
549
  // release: "1.0.0",
136
550
  // });
137
551
  //
552
+ // // Install global handlers (Node.js or browser)
553
+ // client.installGlobalHandlers();
554
+ //
138
555
  // // Capture errors
139
556
  // client.captureError(new Error("something broke"), { route: "/api/users" });
140
557
  //
558
+ // // LLM Tracing
559
+ // const trace = client.startTrace({ name: "chat-completion", input: "Hello" });
560
+ // const span = trace.startSpan({ spanType: "generation", model: "gpt-4o", provider: "openai" });
561
+ // // ... call LLM ...
562
+ // span.end({ inputTokens: 100, outputTokens: 50, cost: 0.0025, status: "ok", output: "Hi!" });
563
+ // trace.end({ status: "completed", output: "Hi!" });
564
+ //
565
+ // // Convenience: traceGeneration
566
+ // const result = await client.traceGeneration(
567
+ // { name: "quick-gen", model: "gpt-4o", provider: "openai", input: "Hello" },
568
+ // async (span) => {
569
+ // const response = "Hi!"; // call LLM here
570
+ // span.end({ inputTokens: 10, outputTokens: 5, cost: 0.001, status: "ok", output: response });
571
+ // return response;
572
+ // }
573
+ // );
574
+ //
141
575
  // // Express middleware
142
576
  // app.use(client.errorMiddleware());
143
577
  //
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Type-level and behavioral tests for auto-instrumentation.
3
+ * `npx tsc --noEmit` must pass with this file included.
4
+ *
5
+ * Tests cover:
6
+ * 1. MODEL_COSTS export exists and has correct shape
7
+ * 2. detectProvider() helper works (tested indirectly via wrapOpenAI)
8
+ * 3. BloopClient.wrapOpenAI() exists, accepts an object, returns same type
9
+ * 4. BloopClient.wrapAnthropic() exists, accepts an object, returns same type
10
+ * 5. BloopClient.setModelCosts() exists with correct signature
11
+ * 6. Wrapped client preserves original type
12
+ * 7. Backward compatibility: all existing APIs still work
13
+ */
14
+ export {};
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Type-level and behavioral tests for auto-instrumentation.
3
+ * `npx tsc --noEmit` must pass with this file included.
4
+ *
5
+ * Tests cover:
6
+ * 1. MODEL_COSTS export exists and has correct shape
7
+ * 2. detectProvider() helper works (tested indirectly via wrapOpenAI)
8
+ * 3. BloopClient.wrapOpenAI() exists, accepts an object, returns same type
9
+ * 4. BloopClient.wrapAnthropic() exists, accepts an object, returns same type
10
+ * 5. BloopClient.setModelCosts() exists with correct signature
11
+ * 6. Wrapped client preserves original type
12
+ * 7. Backward compatibility: all existing APIs still work
13
+ */
14
+ import { BloopClient, MODEL_COSTS, } from "./bloop.js";
15
+ // ---- Test: MODEL_COSTS export exists and has correct shape ----
16
+ const costs = MODEL_COSTS;
17
+ // Verify specific models exist
18
+ const gpt4oCost = MODEL_COSTS["gpt-4o"];
19
+ const gpt4oMiniCost = MODEL_COSTS["gpt-4o-mini"];
20
+ // Values should be numbers (compile-time check)
21
+ const inputCost = MODEL_COSTS["gpt-4o"].input;
22
+ const outputCost = MODEL_COSTS["gpt-4o"].output;
23
+ // ---- Test: BloopClient construction (unchanged) ----
24
+ const client = new BloopClient({
25
+ endpoint: "https://errors.test.com",
26
+ projectKey: "test-key",
27
+ environment: "test",
28
+ release: "1.0.0",
29
+ });
30
+ // ---- Test: setModelCosts() method exists ----
31
+ client.setModelCosts("my-custom-model", { input: 0.001, output: 0.002 });
32
+ client.setModelCosts("gpt-4o", { input: 0.003, output: 0.01 }); // Override built-in
33
+ // wrapOpenAI should accept any object and return the same type
34
+ const mockOAI = {};
35
+ const wrappedOAI = client.wrapOpenAI(mockOAI);
36
+ // The returned type must be the same as the input type
37
+ function assertTypePreserved(input) {
38
+ return client.wrapOpenAI(input);
39
+ }
40
+ const mockAnthropic = {};
41
+ const wrappedAnthropic = client.wrapAnthropic(mockAnthropic);
42
+ // ---- Test: Backward compatibility - existing APIs still work ----
43
+ async function testBackwardCompat() {
44
+ // startTrace still works
45
+ const trace = client.startTrace({ name: "compat-test" });
46
+ const span = trace.startSpan({ spanType: "generation", model: "gpt-4o" });
47
+ span.end({ status: "ok", inputTokens: 10, outputTokens: 5 });
48
+ trace.end({ status: "completed" });
49
+ // traceGeneration still works
50
+ const result = await client.traceGeneration({ name: "compat-gen", model: "gpt-4o", provider: "openai" }, async (span) => {
51
+ span.end({ status: "ok" });
52
+ return "result";
53
+ });
54
+ const s = result;
55
+ // flush/close still work
56
+ await client.flush();
57
+ await client.close();
58
+ }
59
+ const extClient = {};
60
+ const wrappedExt = client.wrapOpenAI(extClient);
61
+ // customProp and someMethod should still be accessible (type-level)
62
+ const _cp = wrappedExt.customProp;
63
+ const _sm = wrappedExt.someMethod;
64
+ // ---- Test: wrapOpenAI and wrapAnthropic can be called with plain objects ----
65
+ const plainWrapped = client.wrapOpenAI({
66
+ chat: {
67
+ completions: {
68
+ create: async () => ({ choices: [], usage: { prompt_tokens: 0, completion_tokens: 0 } }),
69
+ },
70
+ },
71
+ });
72
+ const anthropicPlain = client.wrapAnthropic({
73
+ messages: {
74
+ create: async () => ({
75
+ content: [{ type: "text", text: "hello" }],
76
+ usage: { input_tokens: 0, output_tokens: 0 },
77
+ }),
78
+ },
79
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Type-level tests for LLM tracing support.
3
+ * This file exercises all new types and APIs.
4
+ * `npx tsc --noEmit` must pass for these tests to be "green."
5
+ */
6
+ export {};
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Type-level tests for LLM tracing support.
3
+ * This file exercises all new types and APIs.
4
+ * `npx tsc --noEmit` must pass for these tests to be "green."
5
+ */
6
+ import { BloopClient, } from "./bloop.js";
7
+ // ---- Test: Types exist and are correct ----
8
+ const spanType = "generation";
9
+ const spanType2 = "tool";
10
+ const spanType3 = "retrieval";
11
+ const spanType4 = "custom";
12
+ const spanStatus = "ok";
13
+ const spanStatus2 = "error";
14
+ const traceStatus = "running";
15
+ const traceStatus2 = "completed";
16
+ const traceStatus3 = "error";
17
+ // ---- Test: TraceOptions interface ----
18
+ const traceOpts = {
19
+ name: "chat-completion",
20
+ sessionId: "sess-123",
21
+ userId: "user-456",
22
+ input: "Hello, world!",
23
+ metadata: { key: "value" },
24
+ promptName: "my-prompt",
25
+ promptVersion: "1.0",
26
+ };
27
+ // Minimal TraceOptions (only name required)
28
+ const traceOptsMinimal = {
29
+ name: "minimal-trace",
30
+ };
31
+ // ---- Test: SpanOptions interface ----
32
+ const spanOpts = {
33
+ spanType: "generation",
34
+ name: "gpt-4o call",
35
+ model: "gpt-4o",
36
+ provider: "openai",
37
+ input: "prompt text",
38
+ metadata: { temperature: 0.7 },
39
+ };
40
+ // Minimal SpanOptions (only spanType required)
41
+ const spanOptsMinimal = {
42
+ spanType: "tool",
43
+ };
44
+ // ---- Test: SpanEndOptions interface ----
45
+ const spanEndOpts = {
46
+ inputTokens: 100,
47
+ outputTokens: 50,
48
+ cost: 0.0025,
49
+ status: "ok",
50
+ errorMessage: undefined,
51
+ output: "Generated text",
52
+ timeToFirstTokenMs: 300,
53
+ };
54
+ // Minimal SpanEndOptions (only status required)
55
+ const spanEndOptsMinimal = {
56
+ status: "error",
57
+ errorMessage: "Something went wrong",
58
+ };
59
+ // ---- Test: TraceEndOptions interface ----
60
+ const traceEndOpts = {
61
+ status: "completed",
62
+ output: "Final response text",
63
+ };
64
+ const traceEndOptsMinimal = {
65
+ status: "error",
66
+ };
67
+ // ---- Test: BloopClient.startTrace() returns a Trace ----
68
+ const client = new BloopClient({
69
+ endpoint: "https://errors.test.com",
70
+ projectKey: "test-key",
71
+ environment: "test",
72
+ release: "1.0.0",
73
+ });
74
+ const trace = client.startTrace({
75
+ name: "chat-completion",
76
+ sessionId: "sess-123",
77
+ userId: "user-456",
78
+ input: "What is the weather?",
79
+ metadata: { source: "api" },
80
+ promptName: "weather-prompt",
81
+ promptVersion: "2.1",
82
+ });
83
+ // ---- Test: Trace properties ----
84
+ const traceId = trace.id;
85
+ const traceName = trace.name;
86
+ const traceSessionId = trace.sessionId;
87
+ const traceUserId = trace.userId;
88
+ const traceStatusProp = trace.status;
89
+ const traceInput = trace.input;
90
+ const traceOutput = trace.output;
91
+ const traceMetadata = trace.metadata;
92
+ const tracePromptName = trace.promptName;
93
+ const tracePromptVersion = trace.promptVersion;
94
+ const traceStartedAt = trace.startedAt;
95
+ const traceEndedAt = trace.endedAt;
96
+ const traceSpans = trace.spans;
97
+ // ---- Test: Trace.startSpan() returns a Span ----
98
+ const span = trace.startSpan({
99
+ spanType: "generation",
100
+ name: "gpt-4o call",
101
+ model: "gpt-4o",
102
+ provider: "openai",
103
+ input: "prompt",
104
+ metadata: { key: "val" },
105
+ });
106
+ // ---- Test: Span properties ----
107
+ const spanId = span.id;
108
+ const spanParentId = span.parentSpanId;
109
+ const spanSpanType = span.spanType;
110
+ const spanName = span.name;
111
+ const spanModel = span.model;
112
+ const spanProvider = span.provider;
113
+ const spanStartedAt = span.startedAt;
114
+ const spanInput = span.input;
115
+ const spanMetadata = span.metadata;
116
+ // ---- Test: Span.end() ----
117
+ span.end({
118
+ inputTokens: 100,
119
+ outputTokens: 50,
120
+ cost: 0.0025,
121
+ status: "ok",
122
+ output: "generated response",
123
+ timeToFirstTokenMs: 300,
124
+ });
125
+ // After end(), these should be set
126
+ const spanInputTokens = span.inputTokens;
127
+ const spanOutputTokens = span.outputTokens;
128
+ const spanCost = span.cost;
129
+ const spanLatencyMs = span.latencyMs;
130
+ const spanTtft = span.timeToFirstTokenMs;
131
+ const spanStatusProp = span.status;
132
+ const spanErrorMessage = span.errorMessage;
133
+ const spanOutput = span.output;
134
+ // ---- Test: Span.toJSON() returns object ----
135
+ const spanJson = span.toJSON();
136
+ // ---- Test: Trace.end() ----
137
+ trace.end({
138
+ status: "completed",
139
+ output: "Final answer about weather",
140
+ });
141
+ // ---- Test: traceGeneration convenience method ----
142
+ async function testTraceGeneration() {
143
+ const result = await client.traceGeneration({
144
+ name: "simple-generation",
145
+ model: "gpt-4o",
146
+ provider: "openai",
147
+ input: "Hello",
148
+ }, async (span) => {
149
+ // Do LLM call
150
+ span.end({
151
+ inputTokens: 10,
152
+ outputTokens: 5,
153
+ cost: 0.001,
154
+ status: "ok",
155
+ output: "Hi!",
156
+ });
157
+ return "Hi!";
158
+ });
159
+ // result should be the return value of the callback
160
+ const output = result;
161
+ }
162
+ // ---- Test: TraceGenerationOptions has correct shape ----
163
+ async function testTraceGenerationMinimal() {
164
+ const result = await client.traceGeneration({
165
+ name: "minimal-gen",
166
+ input: "test",
167
+ }, async (span) => {
168
+ span.end({ status: "ok" });
169
+ return 42;
170
+ });
171
+ const num = result;
172
+ }
173
+ // ---- Test: flush() still works (backward compat) ----
174
+ async function testFlush() {
175
+ await client.flush();
176
+ }
177
+ // ---- Test: close() still works (backward compat) ----
178
+ async function testClose() {
179
+ await client.close();
180
+ }
181
+ // ---- Test: Error tracking still works (backward compat) ----
182
+ function testErrorTracking() {
183
+ client.captureError(new Error("test error"), { route: "/api/test" });
184
+ client.capture({
185
+ errorType: "TestError",
186
+ message: "test message",
187
+ });
188
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bloop-sdk",
3
- "version": "0.2.0",
4
- "description": "Lightweight error reporting SDK for bloop",
3
+ "version": "0.3.0",
4
+ "description": "Lightweight error reporting and LLM tracing SDK for bloop",
5
5
  "main": "dist/bloop.js",
6
6
  "types": "dist/bloop.d.ts",
7
7
  "files": ["dist"],
@@ -9,7 +9,7 @@
9
9
  "build": "tsc",
10
10
  "prepublishOnly": "npm run build"
11
11
  },
12
- "keywords": ["error-tracking", "observability", "bloop"],
12
+ "keywords": ["error-tracking", "observability", "llm-tracing", "bloop"],
13
13
  "license": "MIT",
14
14
  "repository": {
15
15
  "type": "git",