@lmnr-ai/client 0.8.25 → 0.8.27

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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Datapoint, Dataset, EvaluationDatapoint, GetDatapointsResponse, InitEvaluationResponse, PushDatapointsResponse, RolloutParam, SpanType, StringUUID } from "@lmnr-ai/types";
1
+ import { Datapoint, Dataset, EvaluationDatapoint, GetDatapointsResponse, InitEvaluationResponse, PushDatapointsResponse, StringUUID } from "@lmnr-ai/types";
2
2
 
3
3
  //#region src/resources/index.d.ts
4
4
  declare class BaseResource {
@@ -260,46 +260,37 @@ declare class EvaluatorsResource extends BaseResource {
260
260
  declare class RolloutSessionsResource extends BaseResource {
261
261
  constructor(baseHttpUrl: string, projectApiKey: string);
262
262
  /**
263
- * Connects to the SSE stream for rollout debugging sessions
264
- * Returns the Response object for streaming SSE events
263
+ * Idempotently register (upsert) a debug session on the backend, keyed on the
264
+ * SDK-supplied session id. The backend stores the row so the session is
265
+ * visible in the UI; a null/omitted name never clobbers a name set elsewhere.
266
+ *
267
+ * Returns the backend-resolved `projectId` (derived from the API key) so the
268
+ * caller can build the debugger URL; null if the body can't be parsed.
265
269
  */
266
- connect({
270
+ register({
267
271
  sessionId,
268
- name,
269
- params,
270
- signal
272
+ name
271
273
  }: {
272
274
  sessionId: string;
273
- params: RolloutParam[];
274
- name: string;
275
- signal?: AbortSignal;
276
- }): Promise<Response>;
277
- delete({
278
- sessionId
279
- }: {
280
- sessionId: string;
281
- }): Promise<void>;
282
- setStatus({
275
+ name?: string;
276
+ }): Promise<string | null>;
277
+ /**
278
+ * Rename an existing debug session. Update-only: the backend returns 404 (and
279
+ * this throws) when the session id is unknown for the project, so a mistyped
280
+ * id surfaces as an error rather than silently creating a session. Creation
281
+ * stays the SDK's job via {@link register}.
282
+ */
283
+ setName({
283
284
  sessionId,
284
- status
285
+ name
285
286
  }: {
286
287
  sessionId: string;
287
- status: 'PENDING' | 'RUNNING' | 'FINISHED' | 'STOPPED';
288
+ name: string;
288
289
  }): Promise<void>;
289
- sendSpanUpdate({
290
- sessionId,
291
- span
290
+ delete({
291
+ sessionId
292
292
  }: {
293
293
  sessionId: string;
294
- span: {
295
- name: string;
296
- startTime: string;
297
- spanId: string;
298
- traceId: string;
299
- parentSpanId: string | undefined;
300
- attributes: Record<string, any>;
301
- spanType: SpanType;
302
- };
303
294
  }): Promise<void>;
304
295
  }
305
296
  //#endregion
@@ -348,6 +339,65 @@ declare class TagsResource extends BaseResource {
348
339
  tag(trace_id: string, tags: string[] | string): Promise<any>;
349
340
  }
350
341
  //#endregion
342
+ //#region src/resources/traces.d.ts
343
+ declare class TracesResource extends BaseResource {
344
+ /** Resource for post-factum operations on existing traces. */
345
+ constructor(baseHttpUrl: string, projectApiKey: string);
346
+ /**
347
+ * Push a metadata patch to an existing trace.
348
+ *
349
+ * The patch is shallow-merged server-side into the trace's existing metadata
350
+ * (`existing || patch`, last-write-wins per top-level key). Useful for
351
+ * attaching post-factum signals — quality scores, human edits, triage labels —
352
+ * to a trace that has already finished. The patch does NOT extend `endTime`
353
+ * or change tokens / cost / top span / tags / span names. `numSpans` is
354
+ * incremented by 1 (paid by the virtual span that carried the patch through
355
+ * the ingestion queue) so the new ClickHouse row beats the prior version on
356
+ * `ReplacingMergeTree(numSpans)`. No row is added to the `spans` table.
357
+ *
358
+ * Compared to `Laminar.setTraceMetadata` (which sets metadata on the
359
+ * currently in-flight trace via OpenTelemetry attributes), this method
360
+ * operates on a finished trace by trace id, so it must be called after the
361
+ * trace has been flushed.
362
+ *
363
+ * A 404 response (the trace was not found in the project — typically because
364
+ * it has not been flushed yet) is logged as a warning and the call returns
365
+ * without throwing, since the 404 may be expected when pushing too soon
366
+ * after the trace run. Pass `failOnNotFound: true` to throw instead (e.g.
367
+ * CLI callers that must report the failure). Any other non-OK status throws.
368
+ *
369
+ * @param traceId - The trace id to push metadata to. Accepts a UUID string
370
+ * or a 32-char OTel hex trace id.
371
+ * @param metadata - The metadata patch. Top-level keys are merged into the
372
+ * trace's existing metadata. Must be non-empty (the server rejects empty
373
+ * patches with 400).
374
+ * @param options - `failOnNotFound`: throw on 404 instead of warn-and-return.
375
+ * @example
376
+ * ```typescript
377
+ * import { Laminar, observe, LaminarClient } from "@lmnr-ai/lmnr";
378
+ * Laminar.initialize();
379
+ * const client = new LaminarClient();
380
+ *
381
+ * let traceId: string | null = null;
382
+ * await observe({ name: "generate" }, async () => {
383
+ * traceId = await Laminar.getTraceId();
384
+ * });
385
+ * await Laminar.flush();
386
+ *
387
+ * if (traceId) {
388
+ * await client.traces.pushMetadata(traceId, {
389
+ * score: 0.85,
390
+ * reviewer: "alice",
391
+ * needsReview: false,
392
+ * });
393
+ * }
394
+ * ```
395
+ */
396
+ pushMetadata(traceId: string, metadata: Record<string, unknown>, options?: {
397
+ failOnNotFound?: boolean;
398
+ }): Promise<void>;
399
+ }
400
+ //#endregion
351
401
  //#region src/index.d.ts
352
402
  declare class LaminarClient {
353
403
  private baseUrl;
@@ -359,6 +409,7 @@ declare class LaminarClient {
359
409
  private _rolloutSessions;
360
410
  private _sql;
361
411
  private _tags;
412
+ private _traces;
362
413
  constructor({
363
414
  baseUrl,
364
415
  projectApiKey,
@@ -375,6 +426,7 @@ declare class LaminarClient {
375
426
  get rolloutSessions(): RolloutSessionsResource;
376
427
  get sql(): SqlResource;
377
428
  get tags(): TagsResource;
429
+ get traces(): TracesResource;
378
430
  }
379
431
  //#endregion
380
432
  export { LaminarClient };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Datapoint, Dataset, EvaluationDatapoint, GetDatapointsResponse, InitEvaluationResponse, PushDatapointsResponse, RolloutParam, SpanType, StringUUID } from "@lmnr-ai/types";
1
+ import { Datapoint, Dataset, EvaluationDatapoint, GetDatapointsResponse, InitEvaluationResponse, PushDatapointsResponse, StringUUID } from "@lmnr-ai/types";
2
2
 
3
3
  //#region src/resources/index.d.ts
4
4
  declare class BaseResource {
@@ -260,46 +260,37 @@ declare class EvaluatorsResource extends BaseResource {
260
260
  declare class RolloutSessionsResource extends BaseResource {
261
261
  constructor(baseHttpUrl: string, projectApiKey: string);
262
262
  /**
263
- * Connects to the SSE stream for rollout debugging sessions
264
- * Returns the Response object for streaming SSE events
263
+ * Idempotently register (upsert) a debug session on the backend, keyed on the
264
+ * SDK-supplied session id. The backend stores the row so the session is
265
+ * visible in the UI; a null/omitted name never clobbers a name set elsewhere.
266
+ *
267
+ * Returns the backend-resolved `projectId` (derived from the API key) so the
268
+ * caller can build the debugger URL; null if the body can't be parsed.
265
269
  */
266
- connect({
270
+ register({
267
271
  sessionId,
268
- name,
269
- params,
270
- signal
272
+ name
271
273
  }: {
272
274
  sessionId: string;
273
- params: RolloutParam[];
274
- name: string;
275
- signal?: AbortSignal;
276
- }): Promise<Response>;
277
- delete({
278
- sessionId
279
- }: {
280
- sessionId: string;
281
- }): Promise<void>;
282
- setStatus({
275
+ name?: string;
276
+ }): Promise<string | null>;
277
+ /**
278
+ * Rename an existing debug session. Update-only: the backend returns 404 (and
279
+ * this throws) when the session id is unknown for the project, so a mistyped
280
+ * id surfaces as an error rather than silently creating a session. Creation
281
+ * stays the SDK's job via {@link register}.
282
+ */
283
+ setName({
283
284
  sessionId,
284
- status
285
+ name
285
286
  }: {
286
287
  sessionId: string;
287
- status: 'PENDING' | 'RUNNING' | 'FINISHED' | 'STOPPED';
288
+ name: string;
288
289
  }): Promise<void>;
289
- sendSpanUpdate({
290
- sessionId,
291
- span
290
+ delete({
291
+ sessionId
292
292
  }: {
293
293
  sessionId: string;
294
- span: {
295
- name: string;
296
- startTime: string;
297
- spanId: string;
298
- traceId: string;
299
- parentSpanId: string | undefined;
300
- attributes: Record<string, any>;
301
- spanType: SpanType;
302
- };
303
294
  }): Promise<void>;
304
295
  }
305
296
  //#endregion
@@ -348,6 +339,65 @@ declare class TagsResource extends BaseResource {
348
339
  tag(trace_id: string, tags: string[] | string): Promise<any>;
349
340
  }
350
341
  //#endregion
342
+ //#region src/resources/traces.d.ts
343
+ declare class TracesResource extends BaseResource {
344
+ /** Resource for post-factum operations on existing traces. */
345
+ constructor(baseHttpUrl: string, projectApiKey: string);
346
+ /**
347
+ * Push a metadata patch to an existing trace.
348
+ *
349
+ * The patch is shallow-merged server-side into the trace's existing metadata
350
+ * (`existing || patch`, last-write-wins per top-level key). Useful for
351
+ * attaching post-factum signals — quality scores, human edits, triage labels —
352
+ * to a trace that has already finished. The patch does NOT extend `endTime`
353
+ * or change tokens / cost / top span / tags / span names. `numSpans` is
354
+ * incremented by 1 (paid by the virtual span that carried the patch through
355
+ * the ingestion queue) so the new ClickHouse row beats the prior version on
356
+ * `ReplacingMergeTree(numSpans)`. No row is added to the `spans` table.
357
+ *
358
+ * Compared to `Laminar.setTraceMetadata` (which sets metadata on the
359
+ * currently in-flight trace via OpenTelemetry attributes), this method
360
+ * operates on a finished trace by trace id, so it must be called after the
361
+ * trace has been flushed.
362
+ *
363
+ * A 404 response (the trace was not found in the project — typically because
364
+ * it has not been flushed yet) is logged as a warning and the call returns
365
+ * without throwing, since the 404 may be expected when pushing too soon
366
+ * after the trace run. Pass `failOnNotFound: true` to throw instead (e.g.
367
+ * CLI callers that must report the failure). Any other non-OK status throws.
368
+ *
369
+ * @param traceId - The trace id to push metadata to. Accepts a UUID string
370
+ * or a 32-char OTel hex trace id.
371
+ * @param metadata - The metadata patch. Top-level keys are merged into the
372
+ * trace's existing metadata. Must be non-empty (the server rejects empty
373
+ * patches with 400).
374
+ * @param options - `failOnNotFound`: throw on 404 instead of warn-and-return.
375
+ * @example
376
+ * ```typescript
377
+ * import { Laminar, observe, LaminarClient } from "@lmnr-ai/lmnr";
378
+ * Laminar.initialize();
379
+ * const client = new LaminarClient();
380
+ *
381
+ * let traceId: string | null = null;
382
+ * await observe({ name: "generate" }, async () => {
383
+ * traceId = await Laminar.getTraceId();
384
+ * });
385
+ * await Laminar.flush();
386
+ *
387
+ * if (traceId) {
388
+ * await client.traces.pushMetadata(traceId, {
389
+ * score: 0.85,
390
+ * reviewer: "alice",
391
+ * needsReview: false,
392
+ * });
393
+ * }
394
+ * ```
395
+ */
396
+ pushMetadata(traceId: string, metadata: Record<string, unknown>, options?: {
397
+ failOnNotFound?: boolean;
398
+ }): Promise<void>;
399
+ }
400
+ //#endregion
351
401
  //#region src/index.d.ts
352
402
  declare class LaminarClient {
353
403
  private baseUrl;
@@ -359,6 +409,7 @@ declare class LaminarClient {
359
409
  private _rolloutSessions;
360
410
  private _sql;
361
411
  private _tags;
412
+ private _traces;
362
413
  constructor({
363
414
  baseUrl,
364
415
  projectApiKey,
@@ -375,6 +426,7 @@ declare class LaminarClient {
375
426
  get rolloutSessions(): RolloutSessionsResource;
376
427
  get sql(): SqlResource;
377
428
  get tags(): TagsResource;
429
+ get traces(): TracesResource;
378
430
  }
379
431
  //#endregion
380
432
  export { LaminarClient };
package/dist/index.mjs CHANGED
@@ -3,8 +3,9 @@ import * as path from "path";
3
3
  import pino from "pino";
4
4
  import { PinoPretty } from "pino-pretty";
5
5
  import { v4 } from "uuid";
6
+ import { errorMessage } from "@lmnr-ai/types";
6
7
  //#region package.json
7
- var version = "0.8.25";
8
+ var version = "0.8.27";
8
9
  //#endregion
9
10
  //#region src/version.ts
10
11
  function getLangVersion() {
@@ -69,7 +70,7 @@ function initializeLogger(options) {
69
70
  minimumLevel: level
70
71
  }));
71
72
  }
72
- const logger$2 = initializeLogger();
73
+ const logger$4 = initializeLogger();
73
74
  const isStringUUID = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id);
74
75
  const newUUID = () => {
75
76
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
@@ -78,9 +79,9 @@ const newUUID = () => {
78
79
  const otelSpanIdToUUID = (spanId) => {
79
80
  let id = spanId.toLowerCase();
80
81
  if (id.startsWith("0x")) id = id.slice(2);
81
- if (id.length !== 16) logger$2.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
82
+ if (id.length !== 16) logger$4.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
82
83
  if (!/^[0-9a-f]+$/.test(id)) {
83
- logger$2.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
84
+ logger$4.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
84
85
  return newUUID();
85
86
  }
86
87
  return id.padStart(32, "0").replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
@@ -88,9 +89,9 @@ const otelSpanIdToUUID = (spanId) => {
88
89
  const otelTraceIdToUUID = (traceId) => {
89
90
  let id = traceId.toLowerCase();
90
91
  if (id.startsWith("0x")) id = id.slice(2);
91
- if (id.length !== 32) logger$2.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
92
+ if (id.length !== 32) logger$4.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
92
93
  if (!/^[0-9a-f]+$/.test(id)) {
93
- logger$2.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
94
+ logger$4.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
94
95
  return newUUID();
95
96
  }
96
97
  return id.replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
@@ -120,7 +121,7 @@ const loadEnv = (options) => {
120
121
  };
121
122
  //#endregion
122
123
  //#region src/resources/datasets.ts
123
- const logger$1 = initializeLogger();
124
+ const logger$3 = initializeLogger();
124
125
  const DEFAULT_DATASET_PULL_LIMIT = 100;
125
126
  const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
126
127
  var DatasetsResource = class extends BaseResource {
@@ -175,7 +176,7 @@ var DatasetsResource = class extends BaseResource {
175
176
  let response;
176
177
  for (let i = 0; i < points.length; i += batchSize) {
177
178
  const batchNum = Math.floor(i / batchSize) + 1;
178
- logger$1.debug(`Pushing batch ${batchNum} of ${totalBatches}`);
179
+ logger$3.debug(`Pushing batch ${batchNum} of ${totalBatches}`);
179
180
  const batch = points.slice(i, i + batchSize);
180
181
  const fetchResponse = await fetch(this.baseHttpUrl + "/v1/datasets/datapoints", {
181
182
  method: "POST",
@@ -225,7 +226,7 @@ var DatasetsResource = class extends BaseResource {
225
226
  };
226
227
  //#endregion
227
228
  //#region src/resources/evals.ts
228
- const logger = initializeLogger();
229
+ const logger$2 = initializeLogger();
229
230
  const INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
230
231
  var EvalsResource = class extends BaseResource {
231
232
  constructor(baseHttpUrl, projectApiKey) {
@@ -361,7 +362,7 @@ var EvalsResource = class extends BaseResource {
361
362
  * @returns {Promise<GetDatapointsResponse>} Response from the datapoint retrieval
362
363
  */
363
364
  async getDatapoints({ datasetName, offset, limit }) {
364
- logger.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
365
+ logger$2.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
365
366
  const params = new URLSearchParams({
366
367
  name: datasetName,
367
368
  offset: offset.toString(),
@@ -378,7 +379,7 @@ var EvalsResource = class extends BaseResource {
378
379
  let length = initialLength;
379
380
  let lastResponse = null;
380
381
  for (let i = 0; i < maxRetries; i++) {
381
- logger.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
382
+ logger$2.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
382
383
  const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
383
384
  method: "POST",
384
385
  headers: this.headers(),
@@ -464,60 +465,51 @@ var EvaluatorsResource = class extends BaseResource {
464
465
  };
465
466
  //#endregion
466
467
  //#region src/resources/rollout-sessions.ts
468
+ const logger$1 = initializeLogger();
467
469
  var RolloutSessionsResource = class extends BaseResource {
468
470
  constructor(baseHttpUrl, projectApiKey) {
469
471
  super(baseHttpUrl, projectApiKey);
470
472
  }
471
473
  /**
472
- * Connects to the SSE stream for rollout debugging sessions
473
- * Returns the Response object for streaming SSE events
474
+ * Idempotently register (upsert) a debug session on the backend, keyed on the
475
+ * SDK-supplied session id. The backend stores the row so the session is
476
+ * visible in the UI; a null/omitted name never clobbers a name set elsewhere.
477
+ *
478
+ * Returns the backend-resolved `projectId` (derived from the API key) so the
479
+ * caller can build the debugger URL; null if the body can't be parsed.
474
480
  */
475
- async connect({ sessionId, name, params, signal }) {
481
+ async register({ sessionId, name }) {
476
482
  const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
477
483
  method: "POST",
478
- headers: {
479
- ...this.headers(),
480
- "Accept": "text/event-stream"
481
- },
482
- body: JSON.stringify({
483
- name,
484
- params
485
- }),
486
- signal
487
- });
488
- if (!response.ok) throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
489
- if (!response.body) throw new Error("No response body");
490
- return response;
491
- }
492
- async delete({ sessionId }) {
493
- const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
494
- method: "DELETE",
495
- headers: this.headers()
484
+ headers: this.headers(),
485
+ body: JSON.stringify({ name })
496
486
  });
497
487
  if (!response.ok) await this.handleError(response);
488
+ try {
489
+ return (await response.json()).projectId ?? null;
490
+ } catch (e) {
491
+ logger$1.warn(`Failed to parse rollout register response: ${errorMessage(e)}`);
492
+ return null;
493
+ }
498
494
  }
499
- async setStatus({ sessionId, status }) {
500
- const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/status`, {
495
+ /**
496
+ * Rename an existing debug session. Update-only: the backend returns 404 (and
497
+ * this throws) when the session id is unknown for the project, so a mistyped
498
+ * id surfaces as an error rather than silently creating a session. Creation
499
+ * stays the SDK's job via {@link register}.
500
+ */
501
+ async setName({ sessionId, name }) {
502
+ const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/name`, {
501
503
  method: "PATCH",
502
504
  headers: this.headers(),
503
- body: JSON.stringify({ status })
505
+ body: JSON.stringify({ name })
504
506
  });
505
507
  if (!response.ok) await this.handleError(response);
506
508
  }
507
- async sendSpanUpdate({ sessionId, span }) {
508
- const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/update`, {
509
- method: "PATCH",
510
- headers: this.headers(),
511
- body: JSON.stringify({
512
- type: "spanStart",
513
- spanId: otelSpanIdToUUID(span.spanId),
514
- traceId: otelTraceIdToUUID(span.traceId),
515
- parentSpanId: span.parentSpanId ? otelSpanIdToUUID(span.parentSpanId) : void 0,
516
- attributes: span.attributes,
517
- startTime: span.startTime,
518
- name: span.name,
519
- spanType: span.spanType
520
- })
509
+ async delete({ sessionId }) {
510
+ const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
511
+ method: "DELETE",
512
+ headers: this.headers()
521
513
  });
522
514
  if (!response.ok) await this.handleError(response);
523
515
  }
@@ -599,6 +591,86 @@ var TagsResource = class extends BaseResource {
599
591
  }
600
592
  };
601
593
  //#endregion
594
+ //#region src/resources/traces.ts
595
+ /** Resource for post-factum operations on existing traces. */
596
+ const logger = initializeLogger();
597
+ var TracesResource = class extends BaseResource {
598
+ /** Resource for post-factum operations on existing traces. */
599
+ constructor(baseHttpUrl, projectApiKey) {
600
+ super(baseHttpUrl, projectApiKey);
601
+ }
602
+ /**
603
+ * Push a metadata patch to an existing trace.
604
+ *
605
+ * The patch is shallow-merged server-side into the trace's existing metadata
606
+ * (`existing || patch`, last-write-wins per top-level key). Useful for
607
+ * attaching post-factum signals — quality scores, human edits, triage labels —
608
+ * to a trace that has already finished. The patch does NOT extend `endTime`
609
+ * or change tokens / cost / top span / tags / span names. `numSpans` is
610
+ * incremented by 1 (paid by the virtual span that carried the patch through
611
+ * the ingestion queue) so the new ClickHouse row beats the prior version on
612
+ * `ReplacingMergeTree(numSpans)`. No row is added to the `spans` table.
613
+ *
614
+ * Compared to `Laminar.setTraceMetadata` (which sets metadata on the
615
+ * currently in-flight trace via OpenTelemetry attributes), this method
616
+ * operates on a finished trace by trace id, so it must be called after the
617
+ * trace has been flushed.
618
+ *
619
+ * A 404 response (the trace was not found in the project — typically because
620
+ * it has not been flushed yet) is logged as a warning and the call returns
621
+ * without throwing, since the 404 may be expected when pushing too soon
622
+ * after the trace run. Pass `failOnNotFound: true` to throw instead (e.g.
623
+ * CLI callers that must report the failure). Any other non-OK status throws.
624
+ *
625
+ * @param traceId - The trace id to push metadata to. Accepts a UUID string
626
+ * or a 32-char OTel hex trace id.
627
+ * @param metadata - The metadata patch. Top-level keys are merged into the
628
+ * trace's existing metadata. Must be non-empty (the server rejects empty
629
+ * patches with 400).
630
+ * @param options - `failOnNotFound`: throw on 404 instead of warn-and-return.
631
+ * @example
632
+ * ```typescript
633
+ * import { Laminar, observe, LaminarClient } from "@lmnr-ai/lmnr";
634
+ * Laminar.initialize();
635
+ * const client = new LaminarClient();
636
+ *
637
+ * let traceId: string | null = null;
638
+ * await observe({ name: "generate" }, async () => {
639
+ * traceId = await Laminar.getTraceId();
640
+ * });
641
+ * await Laminar.flush();
642
+ *
643
+ * if (traceId) {
644
+ * await client.traces.pushMetadata(traceId, {
645
+ * score: 0.85,
646
+ * reviewer: "alice",
647
+ * needsReview: false,
648
+ * });
649
+ * }
650
+ * ```
651
+ */
652
+ async pushMetadata(traceId, metadata, options) {
653
+ if (!metadata || Object.keys(metadata).length === 0) throw new Error("metadata must be a non-empty object");
654
+ const formattedTraceId = isStringUUID(traceId) ? traceId : otelTraceIdToUUID(traceId);
655
+ const url = this.baseHttpUrl + "/v1/traces/metadata";
656
+ const response = await fetch(url, {
657
+ method: "POST",
658
+ headers: this.headers(),
659
+ body: JSON.stringify({
660
+ traceId: formattedTraceId,
661
+ metadata
662
+ })
663
+ });
664
+ if (response.status === 404) {
665
+ const message = `Trace ${formattedTraceId} not found. The trace may not have been flushed yet — call await Laminar.flush() and retry.`;
666
+ if (options?.failOnNotFound) throw new Error(message);
667
+ logger.warn(message);
668
+ return;
669
+ }
670
+ if (!response.ok) await this.handleError(response);
671
+ }
672
+ };
673
+ //#endregion
602
674
  //#region src/index.ts
603
675
  var LaminarClient = class {
604
676
  constructor({ baseUrl, projectApiKey, port } = {}) {
@@ -614,6 +686,7 @@ var LaminarClient = class {
614
686
  this._rolloutSessions = new RolloutSessionsResource(this.baseUrl, this.projectApiKey);
615
687
  this._sql = new SqlResource(this.baseUrl, this.projectApiKey);
616
688
  this._tags = new TagsResource(this.baseUrl, this.projectApiKey);
689
+ this._traces = new TracesResource(this.baseUrl, this.projectApiKey);
617
690
  }
618
691
  get browserEvents() {
619
692
  return this._browserEvents;
@@ -636,6 +709,9 @@ var LaminarClient = class {
636
709
  get tags() {
637
710
  return this._tags;
638
711
  }
712
+ get traces() {
713
+ return this._traces;
714
+ }
639
715
  };
640
716
  //#endregion
641
717
  export { LaminarClient };