@glassflow-ai/rius 0.4.0 → 0.6.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
@@ -40,7 +40,11 @@ function seconds(raw, fallback) {
40
40
  if (Number.isNaN(parsed)) return fallback;
41
41
  return parsed;
42
42
  }
43
- function clamp(name, value, min, max) {
43
+ function clamp(name, value, min, max, fallback) {
44
+ if (!Number.isFinite(value)) {
45
+ console.warn(`[rius] ${name}=${value} is not a finite number; using ${fallback}.`);
46
+ return fallback;
47
+ }
44
48
  if (value >= min && value <= max) return value;
45
49
  const clamped = Math.min(Math.max(value, min), max);
46
50
  console.warn(`[rius] ${name}=${value} is outside [${min}, ${max}]; clamped to ${clamped}.`);
@@ -53,13 +57,15 @@ function resolveConfig(options = {}, env = process.env) {
53
57
  "heartbeatInterval",
54
58
  options.heartbeatInterval ?? seconds(env.RIUS_HEARTBEAT_INTERVAL, DEFAULT_HEARTBEAT_INTERVAL),
55
59
  HEARTBEAT_INTERVAL_MIN,
56
- HEARTBEAT_INTERVAL_MAX
60
+ HEARTBEAT_INTERVAL_MAX,
61
+ DEFAULT_HEARTBEAT_INTERVAL
57
62
  );
58
63
  const partialSpansDelaySeconds = clamp(
59
64
  "partialSpansDelay",
60
65
  options.partialSpansDelay ?? seconds(env.RIUS_PARTIAL_SPANS_DELAY, DEFAULT_PARTIAL_SPANS_DELAY),
61
66
  PARTIAL_SPANS_DELAY_MIN,
62
- PARTIAL_SPANS_DELAY_MAX
67
+ PARTIAL_SPANS_DELAY_MAX,
68
+ DEFAULT_PARTIAL_SPANS_DELAY
63
69
  );
64
70
  return {
65
71
  endpoint,
@@ -208,8 +214,25 @@ var init_exportHealth = __esm({
208
214
  }
209
215
  });
210
216
 
211
- // src/heartbeat.ts
217
+ // src/version.ts
212
218
  import { createRequire } from "module";
219
+ var SDK_VERSION;
220
+ var init_version = __esm({
221
+ "src/version.ts"() {
222
+ "use strict";
223
+ init_esm_shims();
224
+ SDK_VERSION = (() => {
225
+ try {
226
+ const pkg = createRequire(import.meta.url)("../package.json");
227
+ return pkg.version ?? "0.0.0";
228
+ } catch {
229
+ return "0.0.0";
230
+ }
231
+ })();
232
+ }
233
+ });
234
+
235
+ // src/heartbeat.ts
213
236
  function httpTransport(url, headers) {
214
237
  return async (payload, timeoutMs) => {
215
238
  const response = await fetch(url, {
@@ -225,23 +248,16 @@ function httpTransport(url, headers) {
225
248
  }
226
249
  };
227
250
  }
228
- var PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, SDK_VERSION, OpenRootSpanTracker, HeartbeatSender;
251
+ var PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, OpenRootSpanTracker, HeartbeatSender;
229
252
  var init_heartbeat = __esm({
230
253
  "src/heartbeat.ts"() {
231
254
  "use strict";
232
255
  init_esm_shims();
256
+ init_version();
233
257
  PAYLOAD_VERSION = 1;
234
258
  OPEN_TRACES_CAP = 32;
235
259
  DEFAULT_PING_TIMEOUT_MS = 3e3;
236
260
  DEFAULT_FINAL_PING_TIMEOUT_MS = 1e3;
237
- SDK_VERSION = (() => {
238
- try {
239
- const pkg = createRequire(import.meta.url)("../package.json");
240
- return pkg.version ?? "0.0.0";
241
- } catch {
242
- return "0.0.0";
243
- }
244
- })();
245
261
  OpenRootSpanTracker = class {
246
262
  // trace id (32-hex) -> count of open root spans in that trace. A trace id
247
263
  // normally has one root, but the count guards against duplicates.
@@ -293,6 +309,7 @@ var init_heartbeat = __esm({
293
309
  }
294
310
  /** Starts pinging: an immediate first ping, then every `intervalMs`. */
295
311
  start() {
312
+ if (this.timer !== void 0) return;
296
313
  void this.ping(this.pingTimeoutMs, false);
297
314
  const timer = setInterval(() => void this.ping(this.pingTimeoutMs, false), this.intervalMs);
298
315
  if (typeof timer.unref === "function") timer.unref();
@@ -343,7 +360,7 @@ function kindAttributes(kind) {
343
360
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
344
361
  return attributes;
345
362
  }
346
- var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, WORKSPACE_ROUTE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
363
+ var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, USER_ID, WORKSPACE_ROUTE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_REQUEST_REASONING_LEVEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_TOOL_DEFINITIONS, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, LLM_INVOCATION_PARAMETERS, INVOCATION_PARAMETERS_CONTENT_MEMBERS, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
347
364
  var init_semconv = __esm({
348
365
  "src/semconv.ts"() {
349
366
  "use strict";
@@ -354,19 +371,23 @@ var init_semconv = __esm({
354
371
  INPUT_VALUE = "input.value";
355
372
  OUTPUT_VALUE = "output.value";
356
373
  SESSION_ID = "session.id";
374
+ USER_ID = "user.id";
357
375
  WORKSPACE_ROUTE = "rius.workspace";
358
376
  GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
359
377
  GEN_AI_PROVIDER_NAME = "gen_ai.provider.name";
360
378
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
379
+ GEN_AI_REQUEST_REASONING_LEVEL = "gen_ai.request.reasoning.level";
361
380
  GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
362
381
  GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
363
382
  GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
364
383
  GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens";
365
384
  GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS = "gen_ai.usage.cache_write.input_tokens";
385
+ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS = "gen_ai.usage.reasoning.output_tokens";
366
386
  GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages";
367
387
  GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages";
368
388
  GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons";
369
389
  GEN_AI_TOOL_NAME = "gen_ai.tool.name";
390
+ GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions";
370
391
  GEN_AI_REQUEST_PREFIX = "gen_ai.request.";
371
392
  MCP_RESULT_TYPE = "mcp.result_type";
372
393
  GEN_AI_FIRST_TOKEN_EVENT = "gen_ai.first_token";
@@ -395,7 +416,18 @@ var init_semconv = __esm({
395
416
  // or internal URLs in parameter defaults. gen_ai.tool.name stays: it is
396
417
  // identity, not content.
397
418
  "gen_ai.tool.description",
398
- "gen_ai.tool.definitions",
419
+ GEN_AI_TOOL_DEFINITIONS,
420
+ // GenAI semconv keys emitted by OTel-native instrumentations (@ai-sdk/otel,
421
+ // which init() registers for ai v7, among them; pinned 2026-09-14): the
422
+ // system prompt and each tool call's input and output are content in the
423
+ // same sense messages are. gen_ai.tool.call.id stays: identity.
424
+ "gen_ai.system_instructions",
425
+ "gen_ai.tool.call.arguments",
426
+ "gen_ai.tool.call.result",
427
+ // OpenInference TOOL-kind spans carry the definition under these two bare
428
+ // keys, sensitive for the reason gen_ai.tool.description is.
429
+ "tool.description",
430
+ "tool.parameters",
399
431
  "gen_ai.prompt",
400
432
  "gen_ai.completion",
401
433
  "llm.input_messages",
@@ -409,15 +441,44 @@ var init_semconv = __esm({
409
441
  "mlflow.spanInputs",
410
442
  "mlflow.spanOutputs",
411
443
  "traceloop.entity.input",
412
- "traceloop.entity.output"
444
+ "traceloop.entity.output",
445
+ // Every bundled OpenInference instrumentation emits tool definitions as
446
+ // llm.tools.{i}.tool.json_schema (pinned empirically 2026-09-14); covered
447
+ // by prefix below, bare key listed per the bare-key rule.
448
+ "llm.tools",
449
+ // The Vercel AI SDK's own telemetry keys survive the OpenInference
450
+ // transform untouched, and they carry the full prompt (messages AND tool
451
+ // definitions), response content, and tool-call I/O. Names, ids and models
452
+ // (ai.toolCall.name, ai.response.model, ...) are identity and stay.
453
+ "ai.prompt",
454
+ "ai.response.text",
455
+ "ai.response.object",
456
+ "ai.toolCall.args",
457
+ "ai.toolCall.result",
458
+ // Same family, keys enumerated from the ai v7 telemetry surface
459
+ // (2026-09-14): model reasoning, tool calls in the response, response
460
+ // files, embedding inputs, rerank documents, and the generateObject schema
461
+ // (a tool definition by another name).
462
+ "ai.response.reasoning",
463
+ "ai.response.toolCalls",
464
+ "ai.response.files",
465
+ "ai.value",
466
+ "ai.values",
467
+ "ai.documents",
468
+ "ai.schema",
469
+ "ai.schema.description"
413
470
  ]);
471
+ LLM_INVOCATION_PARAMETERS = "llm.invocation_parameters";
472
+ INVOCATION_PARAMETERS_CONTENT_MEMBERS = ["tools", "functions"];
414
473
  CONTENT_ATTRIBUTE_PREFIXES = [
415
474
  "llm.input_messages.",
416
475
  "llm.output_messages.",
417
476
  "gen_ai.prompt.",
418
477
  "gen_ai.completion.",
419
478
  "llm.prompts.",
420
- "llm.prompt_template."
479
+ "llm.prompt_template.",
480
+ "llm.tools.",
481
+ "ai.prompt."
421
482
  ];
422
483
  CONTENT_ATTRIBUTE_SUFFIXES = [
423
484
  ".document.content",
@@ -432,6 +493,9 @@ var init_semconv = __esm({
432
493
  // Identity, not content: a pending span must be groupable into its
433
494
  // session while still running, that is the live view's whole point.
434
495
  SESSION_ID,
496
+ // Same reason: a crashed run's partial spans must still be attributable
497
+ // to the user they served.
498
+ USER_ID,
435
499
  // Routing, not content: a crashed run's snapshot must land in the same
436
500
  // workspace its final span would have. Stripped at export either way.
437
501
  WORKSPACE_ROUTE
@@ -441,31 +505,77 @@ var init_semconv = __esm({
441
505
  });
442
506
 
443
507
  // src/serde.ts
444
- function toAttributeValue(value) {
445
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
446
- return value;
508
+ function truncate(text) {
509
+ return text.length > MAX_ATTR_CHARS ? text.slice(0, MAX_ATTR_CHARS) + TRUNCATION_MARKER : text;
510
+ }
511
+ function renderable(raw, value) {
512
+ if (typeof raw === "bigint") return String(raw);
513
+ if (raw instanceof Map) return Object.fromEntries(raw);
514
+ if (raw instanceof Set) return [...raw];
515
+ if (ArrayBuffer.isView(raw)) {
516
+ return `<${raw.constructor.name} ${raw.byteLength} bytes>`;
447
517
  }
518
+ if (raw instanceof ArrayBuffer) return `<ArrayBuffer ${raw.byteLength} bytes>`;
519
+ return value;
520
+ }
521
+ function toAttributeValue(value) {
522
+ if (typeof value === "string") return truncate(value);
523
+ if (typeof value === "number" || typeof value === "boolean") return value;
448
524
  if (value === null || value === void 0) return "";
525
+ if (typeof value === "bigint") return String(value);
449
526
  const ancestors = [];
450
527
  try {
451
- return JSON.stringify(value, function(_key, v) {
452
- if (typeof v === "object" && v !== null) {
528
+ const text = JSON.stringify({ "": value }[""], function(key, v) {
529
+ const raw = this?.[key];
530
+ const rendered = renderable(raw, v);
531
+ if (typeof rendered === "object" && rendered !== null) {
453
532
  while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
454
533
  ancestors.pop();
455
534
  }
456
- if (ancestors.includes(v)) return "[Circular]";
457
- ancestors.push(v);
535
+ if (ancestors.includes(rendered)) return "[Circular]";
536
+ ancestors.push(rendered);
458
537
  }
459
- return v;
538
+ return rendered;
460
539
  }) ?? String(value);
540
+ return truncate(text);
461
541
  } catch {
462
542
  return "[unserializable]";
463
543
  }
464
544
  }
545
+ var MAX_ATTR_CHARS, TRUNCATION_MARKER;
465
546
  var init_serde = __esm({
466
547
  "src/serde.ts"() {
467
548
  "use strict";
468
549
  init_esm_shims();
550
+ MAX_ATTR_CHARS = 32 * 1024;
551
+ TRUNCATION_MARKER = "\u2026(truncated)";
552
+ }
553
+ });
554
+
555
+ // src/user.ts
556
+ import { context as apiContext, createContextKey } from "@opentelemetry/api";
557
+ function withUser(userId, fn) {
558
+ return apiContext.with(apiContext.active().setValue(USER_KEY, userId), () => fn(userId));
559
+ }
560
+ var USER_KEY, UserSpanProcessor;
561
+ var init_user = __esm({
562
+ "src/user.ts"() {
563
+ "use strict";
564
+ init_esm_shims();
565
+ init_semconv();
566
+ USER_KEY = createContextKey("rius-user-id");
567
+ UserSpanProcessor = class {
568
+ onStart(span, parentContext) {
569
+ const value = parentContext.getValue(USER_KEY);
570
+ if (typeof value === "string") span.setAttribute(USER_ID, value);
571
+ }
572
+ onEnd(_span) {
573
+ }
574
+ async forceFlush() {
575
+ }
576
+ async shutdown() {
577
+ }
578
+ };
469
579
  }
470
580
  });
471
581
 
@@ -475,30 +585,39 @@ function configure(observation, options) {
475
585
  if (options.input !== void 0) observation.setInput(options.input);
476
586
  return observation;
477
587
  }
588
+ function creationAttributes(options) {
589
+ const attributes = { ...kindAttributes(options.kind ?? "CHAIN" /* CHAIN */), ...options.attributes };
590
+ if (options.userId !== void 0) attributes[USER_ID] = options.userId;
591
+ return attributes;
592
+ }
478
593
  function startSpan(name, options = {}) {
479
- const span = getTracer().startSpan(name, {
480
- attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */)
481
- });
594
+ const span = getTracer().startSpan(name, { attributes: creationAttributes(options) });
482
595
  return configure(new Observation(span), options);
483
596
  }
484
597
  function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
485
598
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
486
- return getTracer().startActiveSpan(
599
+ return runActive(
487
600
  name,
488
- { attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */) },
489
- async (span) => {
490
- const observation = configure(new Observation(span), options);
491
- try {
492
- return await fn(observation);
493
- } catch (error) {
494
- observation.recordException(error);
495
- throw error;
496
- } finally {
497
- observation.end();
498
- }
499
- }
601
+ creationAttributes(options),
602
+ options.userId,
603
+ (span) => configure(new Observation(span), options),
604
+ fn
500
605
  );
501
606
  }
607
+ function runActive(name, attributes, userId, makeHandle, fn) {
608
+ const run = () => getTracer().startActiveSpan(name, { attributes }, async (span) => {
609
+ const handle = makeHandle(span);
610
+ try {
611
+ return await fn(handle);
612
+ } catch (error) {
613
+ handle.recordException(error);
614
+ throw error;
615
+ } finally {
616
+ handle.end();
617
+ }
618
+ });
619
+ return userId !== void 0 ? withUser(userId, run) : run();
620
+ }
502
621
  var Observation;
503
622
  var init_spans = __esm({
504
623
  "src/spans.ts"() {
@@ -507,6 +626,8 @@ var init_spans = __esm({
507
626
  init_client();
508
627
  init_semconv();
509
628
  init_serde();
629
+ init_user();
630
+ Symbol.dispose ??= /* @__PURE__ */ Symbol.for("Symbol.dispose");
510
631
  Observation = class {
511
632
  constructor(span) {
512
633
  this.span = span;
@@ -521,7 +642,26 @@ var init_spans = __esm({
521
642
  this.span.setAttribute(OUTPUT_VALUE, toAttributeValue(value));
522
643
  return this;
523
644
  }
645
+ /**
646
+ * Set an arbitrary attribute. Primitives and homogeneous primitive arrays
647
+ * are passed through as the OTel values they are; objects are JSON-encoded
648
+ * and bounded; `undefined` and `null` set nothing, since "no value" is not
649
+ * an empty string.
650
+ */
524
651
  setAttribute(key, value) {
652
+ if (value === void 0 || value === null) return this;
653
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
654
+ this.span.setAttribute(key, value);
655
+ return this;
656
+ }
657
+ if (Array.isArray(value) && value.every((v) => typeof v === "number")) {
658
+ this.span.setAttribute(key, value);
659
+ return this;
660
+ }
661
+ if (Array.isArray(value) && value.every((v) => typeof v === "boolean")) {
662
+ this.span.setAttribute(key, value);
663
+ return this;
664
+ }
525
665
  this.span.setAttribute(key, toAttributeValue(value));
526
666
  return this;
527
667
  }
@@ -558,6 +698,21 @@ __export(instrumentationMcp_exports, {
558
698
  instrumentMcpClient: () => instrumentMcpClient
559
699
  });
560
700
  import { SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
701
+ function serializeResult(result) {
702
+ if (typeof result !== "object" || result === null) return toAttributeValue(result);
703
+ const record = result;
704
+ const structured = record.structured_content ?? record.structuredContent;
705
+ if (structured !== void 0 && structured !== null) return toAttributeValue(structured);
706
+ const content = record.content;
707
+ if (Array.isArray(content)) {
708
+ const texts = content.map(
709
+ (block) => typeof block === "object" && block !== null ? block.text : void 0
710
+ ).filter((text) => typeof text === "string");
711
+ if (texts.length === 1) return truncate(texts[0]);
712
+ if (texts.length > 1) return toAttributeValue(texts);
713
+ }
714
+ return toAttributeValue(result);
715
+ }
561
716
  function resultIsError(result) {
562
717
  if (typeof result !== "object" || result === null) return false;
563
718
  const record = result;
@@ -569,7 +724,7 @@ function recordResult(observation, result) {
569
724
  observation.setAttribute(MCP_RESULT_TYPE, INPUT_REQUIRED);
570
725
  return;
571
726
  }
572
- observation.setOutput(result);
727
+ observation.setAttribute(OUTPUT_VALUE, serializeResult(result));
573
728
  if (resultIsError(result)) {
574
729
  observation.span.setStatus({
575
730
  code: SpanStatusCode2.ERROR,
@@ -589,9 +744,14 @@ function instrumentMcpClient(ClientClass) {
589
744
  const instrumented = function instrumentedCallTool(params, ...rest) {
590
745
  return startAsCurrentSpan(
591
746
  `execute_tool ${params.name}`,
592
- { kind: "TOOL" /* TOOL */, input: params.arguments },
747
+ // Tool name at CREATION: pending snapshots are built at start, so an
748
+ // attribute set inside the callback never reaches them.
749
+ {
750
+ kind: "TOOL" /* TOOL */,
751
+ input: params.arguments,
752
+ attributes: { [GEN_AI_TOOL_NAME]: params.name }
753
+ },
593
754
  async (observation) => {
594
- observation.setAttribute(GEN_AI_TOOL_NAME, params.name);
595
755
  const result = await original.call(this, params, ...rest);
596
756
  recordResult(observation, result);
597
757
  return result;
@@ -610,6 +770,7 @@ var init_instrumentationMcp = __esm({
610
770
  "use strict";
611
771
  init_esm_shims();
612
772
  init_semconv();
773
+ init_serde();
613
774
  init_spans();
614
775
  INPUT_REQUIRED = "input_required";
615
776
  }
@@ -704,21 +865,55 @@ function mcpClientPatchable(exports) {
704
865
  const cls = exports.Client;
705
866
  return typeof cls?.prototype?.callTool === "function" ? { Client: cls } : void 0;
706
867
  }
707
- async function enableInstrumentations(sink, tracerProvider, names) {
868
+ async function registerVercelTelemetry(tracerProvider, teardown) {
869
+ const ai = await optional("ai");
870
+ const register = ai?.registerTelemetry;
871
+ if (register === void 0) return false;
872
+ const otel = await optional("@ai-sdk/otel");
873
+ const OpenTelemetryIntegration = otel?.OpenTelemetry;
874
+ if (OpenTelemetryIntegration === void 0) {
875
+ console.warn(
876
+ "[rius] Vercel AI SDK v7+ found, but its spans require the @ai-sdk/otel package, which is not installed \u2014 `ai` calls will not be traced. Install it (npm i @ai-sdk/otel) and rius registers it automatically."
877
+ );
878
+ return false;
879
+ }
880
+ const bag = globalThis;
881
+ if (vercelTelemetryIntegration !== void 0 && bag.AI_SDK_TELEMETRY_INTEGRATIONS) {
882
+ bag.AI_SDK_TELEMETRY_INTEGRATIONS = bag.AI_SDK_TELEMETRY_INTEGRATIONS.filter(
883
+ (integration2) => integration2 !== vercelTelemetryIntegration
884
+ );
885
+ }
886
+ const integration = new OpenTelemetryIntegration({
887
+ tracer: tracerProvider.getTracer(TRACER_NAME, SDK_VERSION)
888
+ });
889
+ vercelTelemetryIntegration = integration;
890
+ register(integration);
891
+ teardown?.push(() => {
892
+ if (bag.AI_SDK_TELEMETRY_INTEGRATIONS) {
893
+ bag.AI_SDK_TELEMETRY_INTEGRATIONS = bag.AI_SDK_TELEMETRY_INTEGRATIONS.filter(
894
+ (candidate) => candidate !== integration
895
+ );
896
+ }
897
+ if (vercelTelemetryIntegration === integration) vercelTelemetryIntegration = void 0;
898
+ });
899
+ return true;
900
+ }
901
+ async function enableInstrumentations(sink, tracerProvider, names, teardown) {
708
902
  const wanted = names ? REGISTRY.filter((e) => names.includes(e.name)) : REGISTRY;
709
903
  const enabled = [];
710
904
  for (const entry of wanted) {
711
905
  try {
712
- const loaded = await entry.load();
906
+ const loaded = await entry.load(tracerProvider, teardown);
713
907
  if (loaded === void 0) continue;
714
908
  if (entry.kind === "processor") {
715
909
  if (entry.insert === "first") sink.addFirst(loaded);
716
910
  else sink.add(loaded);
717
911
  } else if (entry.kind === "instrumentation") {
718
- registerInstrumentations({
912
+ const disable = registerInstrumentations({
719
913
  instrumentations: [loaded],
720
914
  tracerProvider
721
915
  });
916
+ teardown?.push(disable);
722
917
  }
723
918
  enabled.push(entry.name);
724
919
  } catch (error) {
@@ -727,11 +922,13 @@ async function enableInstrumentations(sink, tracerProvider, names) {
727
922
  }
728
923
  return enabled;
729
924
  }
730
- var dynamicImport, REGISTRY;
925
+ var dynamicImport, vercelTelemetryIntegration, REGISTRY;
731
926
  var init_instrumentation = __esm({
732
927
  "src/instrumentation.ts"() {
733
928
  "use strict";
734
929
  init_esm_shims();
930
+ init_semconv();
931
+ init_version();
735
932
  dynamicImport = new Function("s", "return import(s)");
736
933
  REGISTRY = [
737
934
  {
@@ -742,15 +939,16 @@ var init_instrumentation = __esm({
742
939
  // transform has to run before the exporting processor sees the span or the
743
940
  // attributes it added would never be sanitised.
744
941
  insert: "first",
745
- async load() {
942
+ async load(tracerProvider, teardown) {
943
+ const registered = tracerProvider === void 0 ? false : await registerVercelTelemetry(tracerProvider, teardown);
746
944
  const utils = await optional("@arizeai/openinference-vercel/utils");
747
945
  const add = utils?.addOpenInferenceAttributesToSpan;
748
- if (add === void 0) return void 0;
946
+ if (add === void 0 && !registered) return void 0;
749
947
  return {
750
948
  onStart() {
751
949
  },
752
950
  onEnd(span) {
753
- add(span);
951
+ add?.(span);
754
952
  },
755
953
  async forceFlush() {
756
954
  },
@@ -800,14 +998,18 @@ var init_instrumentation = __esm({
800
998
  {
801
999
  name: "mcp",
802
1000
  kind: "self-applying",
803
- async load() {
1001
+ async load(_tracerProvider, teardown) {
804
1002
  const mod = await optional("@modelcontextprotocol/sdk/client/index.js");
805
1003
  const ClientClass = mod?.Client;
806
1004
  if (ClientClass === void 0) return void 0;
807
1005
  const { instrumentMcpClient: instrumentMcpClient2 } = await Promise.resolve().then(() => (init_instrumentationMcp(), instrumentationMcp_exports));
808
- instrumentMcpClient2(ClientClass);
1006
+ const uninstallEsm = instrumentMcpClient2(ClientClass);
1007
+ teardown?.push(uninstallEsm);
809
1008
  const cjs = cachedCjsExports("@modelcontextprotocol/sdk", mcpClientPatchable);
810
- if (cjs !== void 0) instrumentMcpClient2(cjs.Client);
1009
+ if (cjs !== void 0) {
1010
+ const uninstallCjs = instrumentMcpClient2(cjs.Client);
1011
+ teardown?.push(uninstallCjs);
1012
+ }
811
1013
  return true;
812
1014
  }
813
1015
  }
@@ -822,7 +1024,25 @@ function isContentKey(key) {
822
1024
  if (CONTENT_ATTRIBUTE_PREFIXES.some((p) => key.startsWith(p))) return true;
823
1025
  return CONTENT_ATTRIBUTE_SUFFIXES.some((s) => key.endsWith(s));
824
1026
  }
825
- var EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, MaskingSpanExporter;
1027
+ function redactInvocationParameters(value) {
1028
+ if (typeof value !== "string") return void 0;
1029
+ let parameters;
1030
+ try {
1031
+ parameters = JSON.parse(value);
1032
+ } catch {
1033
+ return void 0;
1034
+ }
1035
+ if (parameters === null || typeof parameters !== "object" || Array.isArray(parameters)) {
1036
+ return void 0;
1037
+ }
1038
+ const bag = parameters;
1039
+ if (!INVOCATION_PARAMETERS_CONTENT_MEMBERS.some((member) => member in bag)) {
1040
+ return value;
1041
+ }
1042
+ for (const member of INVOCATION_PARAMETERS_CONTENT_MEMBERS) delete bag[member];
1043
+ return JSON.stringify(bag);
1044
+ }
1045
+ var EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, STATUS_DESCRIPTION_KEY, MaskingSpanExporter;
826
1046
  var init_masking = __esm({
827
1047
  "src/masking.ts"() {
828
1048
  "use strict";
@@ -831,6 +1051,7 @@ var init_masking = __esm({
831
1051
  init_serde();
832
1052
  EXCEPTION_EVENT_NAME = "exception";
833
1053
  EXCEPTION_CONTENT_KEYS = ["exception.message", "exception.stacktrace"];
1054
+ STATUS_DESCRIPTION_KEY = "status.description";
834
1055
  MaskingSpanExporter = class {
835
1056
  constructor(inner, opts) {
836
1057
  this.inner = inner;
@@ -856,14 +1077,42 @@ var init_masking = __esm({
856
1077
  for (const link of span.links ?? []) {
857
1078
  this.sanitizeAttributes(link.attributes);
858
1079
  }
859
- if (!this.opts.captureContent && span.status?.code === SpanStatusCode3.ERROR) {
860
- span.status.message = void 0;
861
- }
1080
+ this.sanitizeStatus(span);
862
1081
  return span;
863
1082
  }
1083
+ /**
1084
+ * Drops or masks the status description, in place. `status` is mutable and
1085
+ * exposed by reference on the SDK span, same as `attributes`, so mutating it
1086
+ * here reaches the span about to be handed to the inner exporter. The ERROR
1087
+ * code itself is left alone so the failure stays visible and classifiable,
1088
+ * the same policy as `exception.type`.
1089
+ */
1090
+ sanitizeStatus(span) {
1091
+ const status = span.status;
1092
+ if (status?.code !== SpanStatusCode3.ERROR || !status.message) return;
1093
+ if (!this.opts.captureContent) {
1094
+ status.message = void 0;
1095
+ return;
1096
+ }
1097
+ if (this.opts.mask === void 0) return;
1098
+ try {
1099
+ const masked = toAttributeValue(
1100
+ this.opts.mask(status.message, { key: STATUS_DESCRIPTION_KEY })
1101
+ );
1102
+ status.message = typeof masked === "string" ? masked : String(masked);
1103
+ } catch {
1104
+ status.message = "[mask error]";
1105
+ }
1106
+ }
864
1107
  /** Strips or masks the content keys of one attribute bag, in place. */
865
1108
  sanitizeAttributes(attributes) {
866
1109
  if (attributes === void 0) return;
1110
+ const sanitizing = !this.opts.captureContent || this.opts.mask !== void 0;
1111
+ if (sanitizing && LLM_INVOCATION_PARAMETERS in attributes) {
1112
+ const redacted = redactInvocationParameters(attributes[LLM_INVOCATION_PARAMETERS]);
1113
+ if (redacted === void 0) delete attributes[LLM_INVOCATION_PARAMETERS];
1114
+ else attributes[LLM_INVOCATION_PARAMETERS] = redacted;
1115
+ }
867
1116
  for (const key of Object.keys(attributes)) {
868
1117
  if (!isContentKey(key)) continue;
869
1118
  if (!this.opts.captureContent) {
@@ -923,8 +1172,8 @@ function buildSnapshot(span) {
923
1172
  ended: true
924
1173
  };
925
1174
  }
926
- function spanKey(context) {
927
- return `${context.traceId}:${context.spanId}`;
1175
+ function spanKey(context2) {
1176
+ return `${context2.traceId}:${context2.spanId}`;
928
1177
  }
929
1178
  var PendingSpanProcessor;
930
1179
  var init_pending = __esm({
@@ -977,10 +1226,10 @@ var init_pending = __esm({
977
1226
 
978
1227
  // src/session.ts
979
1228
  import { randomUUID } from "crypto";
980
- import { context as apiContext, createContextKey } from "@opentelemetry/api";
1229
+ import { context as apiContext2, createContextKey as createContextKey2, trace } from "@opentelemetry/api";
981
1230
  function withSession(sessionIdOrFn, maybeFn) {
982
1231
  const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [randomUUID(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
983
- return apiContext.with(apiContext.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
1232
+ return apiContext2.with(apiContext2.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
984
1233
  }
985
1234
  var SESSION_KEY, SessionSpanProcessor;
986
1235
  var init_session = __esm({
@@ -988,7 +1237,7 @@ var init_session = __esm({
988
1237
  "use strict";
989
1238
  init_esm_shims();
990
1239
  init_semconv();
991
- SESSION_KEY = createContextKey("rius-session-id");
1240
+ SESSION_KEY = createContextKey2("rius-session-id");
992
1241
  SessionSpanProcessor = class {
993
1242
  constructor(defaultSessionId) {
994
1243
  this.defaultSessionId = defaultSessionId;
@@ -996,7 +1245,9 @@ var init_session = __esm({
996
1245
  defaultSessionId;
997
1246
  onStart(span, parentContext) {
998
1247
  const value = parentContext.getValue(SESSION_KEY);
999
- const sessionId = typeof value === "string" ? value : this.defaultSessionId;
1248
+ const parent = trace.getSpan(parentContext);
1249
+ const inherited = parent?.attributes?.[SESSION_ID];
1250
+ const sessionId = typeof value === "string" ? value : typeof inherited === "string" ? inherited : this.defaultSessionId;
1000
1251
  if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
1001
1252
  }
1002
1253
  onEnd(_span) {
@@ -1010,10 +1261,10 @@ var init_session = __esm({
1010
1261
  });
1011
1262
 
1012
1263
  // src/workspace.ts
1013
- import { context as apiContext2, createContextKey as createContextKey2, trace } from "@opentelemetry/api";
1264
+ import { context as apiContext3, createContextKey as createContextKey3, trace as trace2 } from "@opentelemetry/api";
1014
1265
  function withWorkspace(alias, fn) {
1015
1266
  if (!alias) throw new Error("workspace alias must be a non-empty string");
1016
- return apiContext2.with(apiContext2.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1267
+ return apiContext3.with(apiContext3.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1017
1268
  }
1018
1269
  function setGlobalRouting(routing) {
1019
1270
  globalRouting = routing;
@@ -1033,17 +1284,24 @@ var init_workspace = __esm({
1033
1284
  init_esm_shims();
1034
1285
  init_esm();
1035
1286
  init_semconv();
1036
- WORKSPACE_KEY = createContextKey2("rius-workspace-alias");
1287
+ WORKSPACE_KEY = createContextKey3("rius-workspace-alias");
1037
1288
  WorkspaceSpanProcessor = class {
1289
+ /** Straddles already warned about, as "parentAlias->alias"; one warning each, not one per span. */
1290
+ warnedStraddles = /* @__PURE__ */ new Set();
1038
1291
  onStart(span, parentContext) {
1039
- const alias = parentContext.getValue(WORKSPACE_KEY);
1040
- if (typeof alias !== "string") return;
1041
- const parent = trace.getSpan(parentContext);
1292
+ const parent = trace2.getSpan(parentContext);
1042
1293
  const parentAlias = parent?.attributes?.[WORKSPACE_ROUTE];
1294
+ const scoped = parentContext.getValue(WORKSPACE_KEY);
1295
+ const alias = typeof scoped === "string" ? scoped : parentAlias;
1296
+ if (typeof alias !== "string") return;
1043
1297
  if (parentAlias !== void 0 && parentAlias !== alias) {
1044
- console.warn(
1045
- `[rius] span "${span.name}" starts under workspace "${alias}" but its parent is stamped "${String(parentAlias)}"; a trace cannot straddle two workspaces (the backend derives the workspace from the API key). Switch workspaces at request boundaries, before the root span starts.`
1046
- );
1298
+ const straddle = `${String(parentAlias)}->${alias}`;
1299
+ if (!this.warnedStraddles.has(straddle)) {
1300
+ this.warnedStraddles.add(straddle);
1301
+ console.warn(
1302
+ `[rius] span "${span.name}" starts under workspace "${alias}" but its parent is stamped "${String(parentAlias)}"; a trace cannot straddle two workspaces (the backend derives the workspace from the API key). Switch workspaces at request boundaries, before the root span starts. Further spans of this straddle are not reported.`
1303
+ );
1304
+ }
1047
1305
  }
1048
1306
  span.setAttribute(WORKSPACE_ROUTE, alias);
1049
1307
  }
@@ -1133,7 +1391,7 @@ var init_workspace = __esm({
1133
1391
 
1134
1392
  // src/client.ts
1135
1393
  import { randomUUID as randomUUID2 } from "crypto";
1136
- import { trace as trace2 } from "@opentelemetry/api";
1394
+ import { context, propagation, trace as trace3 } from "@opentelemetry/api";
1137
1395
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
1138
1396
  import { resourceFromAttributes } from "@opentelemetry/resources";
1139
1397
  import {
@@ -1175,6 +1433,7 @@ function init(options = {}) {
1175
1433
  }) : health;
1176
1434
  const batch = new BatchSpanProcessor(exporter);
1177
1435
  processors.add(new SessionSpanProcessor(config.sessionId));
1436
+ processors.add(new UserSpanProcessor());
1178
1437
  if (routing !== void 0) {
1179
1438
  processors.add(new WorkspaceSpanProcessor());
1180
1439
  }
@@ -1185,9 +1444,13 @@ function init(options = {}) {
1185
1444
  }
1186
1445
  const instanceId = randomUUID2();
1187
1446
  const provider = new NodeTracerProvider({
1447
+ // telemetry.sdk.* is reserved for the OTel SDK itself; we identify as a
1448
+ // distribution via telemetry.distro.*, the same two keys Python stamps.
1188
1449
  resource: resourceFromAttributes({
1189
1450
  [ATTR_SERVICE_NAME]: config.serviceName,
1190
- [SERVICE_INSTANCE_ID]: instanceId
1451
+ [SERVICE_INSTANCE_ID]: instanceId,
1452
+ "telemetry.distro.name": "glassflow-rius",
1453
+ "telemetry.distro.version": SDK_VERSION
1191
1454
  }),
1192
1455
  // Always ParentBased, with no AlwaysOn shortcut at rate 1. They are not
1193
1456
  // equivalent: ParentBased honours a remote UNSAMPLED parent and drops,
@@ -1197,10 +1460,12 @@ function init(options = {}) {
1197
1460
  sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(config.sampleRate) }),
1198
1461
  spanProcessors: [processors]
1199
1462
  });
1200
- const ready = config.disabled ? Promise.resolve([]) : enableInstrumentations(processors, provider).catch(() => []);
1463
+ const teardown = [];
1464
+ const ready = config.disabled ? Promise.resolve([]) : enableInstrumentations(processors, provider, void 0, teardown);
1201
1465
  provider.register();
1202
1466
  let heartbeat;
1203
- if (config.heartbeat && !config.disabled) {
1467
+ const heartbeatCanAuthenticate = config.apiKey !== void 0 || options.heartbeatTransport !== void 0;
1468
+ if (config.heartbeat && !config.disabled && heartbeatCanAuthenticate) {
1204
1469
  const tracker = new OpenRootSpanTracker();
1205
1470
  processors.add(tracker);
1206
1471
  const sender = new HeartbeatSender({
@@ -1219,12 +1484,12 @@ function init(options = {}) {
1219
1484
  process.once("beforeExit", beforeExitHandler);
1220
1485
  heartbeat = { sender, beforeExitHandler };
1221
1486
  }
1222
- globalClient = createClient({ provider, processors, health, ready, heartbeat });
1487
+ globalClient = createClient({ provider, processors, health, ready, teardown, heartbeat });
1223
1488
  setGlobalRouting(routing);
1224
1489
  return globalClient;
1225
1490
  }
1226
1491
  function getTracer() {
1227
- return trace2.getTracer(TRACER_NAME);
1492
+ return trace3.getTracer(TRACER_NAME, SDK_VERSION);
1228
1493
  }
1229
1494
  var sinks, heartbeats, createClient, RiusClient, globalClient;
1230
1495
  var init_client = __esm({
@@ -1240,18 +1505,22 @@ var init_client = __esm({
1240
1505
  init_pending();
1241
1506
  init_semconv();
1242
1507
  init_session();
1508
+ init_user();
1509
+ init_version();
1243
1510
  init_workspace();
1244
1511
  sinks = /* @__PURE__ */ new WeakMap();
1245
1512
  heartbeats = /* @__PURE__ */ new WeakMap();
1246
1513
  RiusClient = class _RiusClient {
1247
1514
  provider;
1248
1515
  health;
1516
+ teardown;
1249
1517
  /** Resolves with the names of the auto-instrumentations that attached. */
1250
1518
  ready;
1251
1519
  constructor(parts) {
1252
1520
  this.provider = parts.provider;
1253
1521
  this.health = parts.health;
1254
1522
  this.ready = parts.ready;
1523
+ this.teardown = parts.teardown;
1255
1524
  sinks.set(this, parts.processors);
1256
1525
  if (parts.heartbeat) heartbeats.set(this, parts.heartbeat);
1257
1526
  }
@@ -1279,13 +1548,22 @@ var init_client = __esm({
1279
1548
  process.removeListener("beforeExit", heartbeat.beforeExitHandler);
1280
1549
  await heartbeat.sender.stop();
1281
1550
  }
1551
+ await this.ready;
1552
+ for (const disable of this.teardown.splice(0)) {
1553
+ try {
1554
+ disable();
1555
+ } catch {
1556
+ }
1557
+ }
1282
1558
  try {
1283
1559
  await this.provider.shutdown();
1284
1560
  } finally {
1285
1561
  if (globalClient === this) {
1286
1562
  globalClient = void 0;
1287
1563
  setGlobalRouting(void 0);
1288
- trace2.disable();
1564
+ trace3.disable();
1565
+ context.disable();
1566
+ propagation.disable();
1289
1567
  }
1290
1568
  }
1291
1569
  }
@@ -1300,17 +1578,106 @@ init_client();
1300
1578
  // src/generation.ts
1301
1579
  init_esm_shims();
1302
1580
  init_client();
1581
+
1582
+ // src/messages.ts
1583
+ init_esm_shims();
1584
+ init_serde();
1585
+ function isRecord(value) {
1586
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1587
+ }
1588
+ function asText(value) {
1589
+ if (typeof value === "string") return value;
1590
+ const text = toAttributeValue(value);
1591
+ return typeof text === "string" ? text : String(text);
1592
+ }
1593
+ function normalizePart(item) {
1594
+ if (isRecord(item)) {
1595
+ if (item.type === "text" && "text" in item) return { type: "text", content: item.text };
1596
+ if ("type" in item) return item;
1597
+ }
1598
+ return { type: "text", content: asText(item) };
1599
+ }
1600
+ function normalizeMessage(message, defaultRole) {
1601
+ if (typeof message === "string") {
1602
+ return { role: defaultRole, parts: [{ type: "text", content: message }] };
1603
+ }
1604
+ if (!isRecord(message)) {
1605
+ return { role: defaultRole, parts: [{ type: "text", content: asText(message) }] };
1606
+ }
1607
+ if ("parts" in message) {
1608
+ return { ...message, role: message.role ?? defaultRole };
1609
+ }
1610
+ const role = message.role ?? defaultRole;
1611
+ if (role === "tool" && "tool_call_id" in message) {
1612
+ return {
1613
+ role: "tool",
1614
+ parts: [{ type: "tool_call_response", id: message.tool_call_id, response: message.content }]
1615
+ };
1616
+ }
1617
+ const parts = [];
1618
+ const content = message.content;
1619
+ if (typeof content === "string") {
1620
+ parts.push({ type: "text", content });
1621
+ } else if (Array.isArray(content)) {
1622
+ for (const item of content) parts.push(normalizePart(item));
1623
+ } else if (content !== null && content !== void 0) {
1624
+ parts.push({ type: "text", content: asText(content) });
1625
+ }
1626
+ const toolCalls = message.tool_calls;
1627
+ if (Array.isArray(toolCalls)) {
1628
+ for (const call of toolCalls) {
1629
+ if (!isRecord(call)) continue;
1630
+ const fn = isRecord(call.function) ? call.function : {};
1631
+ parts.push({ type: "tool_call", id: call.id, name: fn.name, arguments: fn.arguments });
1632
+ }
1633
+ }
1634
+ return { role, parts };
1635
+ }
1636
+ function serializeMessages(messages, defaultRole) {
1637
+ const list = Array.isArray(messages) ? messages : [messages];
1638
+ return toAttributeValue(list.map((message) => normalizeMessage(message, defaultRole)));
1639
+ }
1640
+
1641
+ // src/generation.ts
1303
1642
  init_semconv();
1304
1643
  init_serde();
1305
1644
  init_spans();
1306
1645
  var Generation = class extends Observation {
1646
+ /**
1647
+ * The provider passed at creation; drives the Anthropic input-token summing
1648
+ * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
1649
+ */
1650
+ constructor(span, provider) {
1651
+ super(span);
1652
+ this.provider = provider;
1653
+ }
1654
+ provider;
1307
1655
  firstTokenRecorded = false;
1656
+ /**
1657
+ * Record the request messages (`gen_ai.input.messages`), normalised to the
1658
+ * GenAI `{role, parts}` shape like the Python SDK does: bare strings, OpenAI
1659
+ * dicts (including `tool_calls` and tool responses) and multimodal content
1660
+ * lists are all accepted. Bare strings default to the `user` role.
1661
+ */
1308
1662
  setInput(value) {
1309
- this.span.setAttribute(GEN_AI_INPUT_MESSAGES, toAttributeValue(value));
1663
+ this.span.setAttribute(GEN_AI_INPUT_MESSAGES, serializeMessages(value, "user"));
1310
1664
  return this;
1311
1665
  }
1666
+ /** Record the response messages (`gen_ai.output.messages`); bare strings default to `assistant`. */
1312
1667
  setOutput(value) {
1313
- this.span.setAttribute(GEN_AI_OUTPUT_MESSAGES, toAttributeValue(value));
1668
+ this.span.setAttribute(GEN_AI_OUTPUT_MESSAGES, serializeMessages(value, "assistant"));
1669
+ return this;
1670
+ }
1671
+ /**
1672
+ * The request's tool/function definitions (`gen_ai.tool.definitions`).
1673
+ * Serialized verbatim, in whatever shape the provider request used (OpenAI
1674
+ * nests each tool under `function`, Anthropic uses top-level
1675
+ * `name`/`input_schema`) — no normalization, so what is recorded is exactly
1676
+ * what the model was shown. Definitions are content, not identity: they are
1677
+ * masked/stripped under `captureContent: false` like messages are.
1678
+ */
1679
+ setToolDefinitions(tools) {
1680
+ this.span.setAttribute(GEN_AI_TOOL_DEFINITIONS, toAttributeValue(tools));
1314
1681
  return this;
1315
1682
  }
1316
1683
  setModel(model) {
@@ -1318,15 +1685,20 @@ var Generation = class extends Observation {
1318
1685
  return this;
1319
1686
  }
1320
1687
  /**
1321
- * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is.
1322
- * Per the GenAI conventions, `inputTokens` is the total including cached
1323
- * tokens (the cache counts are subsets of it); providers that report an
1324
- * exclusive `inputTokens` (e.g. Anthropic) are detected and normalized by
1325
- * the backend, so no client-side arithmetic is needed.
1688
+ * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is;
1689
+ * never pre-add anything. Per the GenAI conventions, `inputTokens` is the
1690
+ * total including cached tokens (the cache counts are subsets of it).
1691
+ * Anthropic's API reports `input_tokens` excluding the cache counts, and
1692
+ * the conventions require the instrumentation to do the summing, so when
1693
+ * the generation's provider is `"anthropic"` the emitted total is
1694
+ * `inputTokens` plus both cache counts. Every other provider is recorded
1695
+ * verbatim.
1326
1696
  */
1327
1697
  setUsage(usage) {
1328
1698
  if (usage.inputTokens !== void 0) {
1329
- this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, usage.inputTokens);
1699
+ const sums = this.provider?.toLowerCase() === "anthropic";
1700
+ const total = sums ? usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) : usage.inputTokens;
1701
+ this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, total);
1330
1702
  }
1331
1703
  if (usage.outputTokens !== void 0) {
1332
1704
  this.span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage.outputTokens);
@@ -1337,6 +1709,9 @@ var Generation = class extends Observation {
1337
1709
  if (usage.cacheWriteInputTokens !== void 0) {
1338
1710
  this.span.setAttribute(GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, usage.cacheWriteInputTokens);
1339
1711
  }
1712
+ if (usage.reasoningOutputTokens !== void 0) {
1713
+ this.span.setAttribute(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.reasoningOutputTokens);
1714
+ }
1340
1715
  return this;
1341
1716
  }
1342
1717
  /**
@@ -1368,34 +1743,36 @@ var Generation = class extends Observation {
1368
1743
  };
1369
1744
  function attributesFor(options) {
1370
1745
  const attributes = { ...kindAttributes("LLM" /* LLM */) };
1746
+ if (options.operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = options.operation;
1371
1747
  if (options.model !== void 0) attributes[GEN_AI_REQUEST_MODEL] = options.model;
1372
1748
  if (options.provider !== void 0) attributes[GEN_AI_PROVIDER_NAME] = options.provider;
1749
+ if (options.userId !== void 0) attributes[USER_ID] = options.userId;
1373
1750
  return attributes;
1374
1751
  }
1375
1752
  function configure2(generation, options) {
1376
1753
  for (const [key, value] of Object.entries(options.modelParameters ?? {})) {
1377
1754
  generation.setAttribute(`${GEN_AI_REQUEST_PREFIX}${key}`, value);
1378
1755
  }
1756
+ if (options.reasoningLevel !== void 0) {
1757
+ generation.setAttribute(GEN_AI_REQUEST_REASONING_LEVEL, options.reasoningLevel);
1758
+ }
1759
+ if (options.tools !== void 0) generation.setToolDefinitions(options.tools);
1379
1760
  if (options.input !== void 0) generation.setInput(options.input);
1380
1761
  return generation;
1381
1762
  }
1382
1763
  function startGeneration(name, options = {}) {
1383
1764
  const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1384
- return configure2(new Generation(span), options);
1765
+ return configure2(new Generation(span, options.provider), options);
1385
1766
  }
1386
1767
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1387
1768
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1388
- return getTracer().startActiveSpan(name, { attributes: attributesFor(options) }, async (span) => {
1389
- const generation = configure2(new Generation(span), options);
1390
- try {
1391
- return await fn(generation);
1392
- } catch (error) {
1393
- generation.recordException(error);
1394
- throw error;
1395
- } finally {
1396
- generation.end();
1397
- }
1398
- });
1769
+ return runActive(
1770
+ name,
1771
+ attributesFor(options),
1772
+ options.userId,
1773
+ (span) => configure2(new Generation(span, options.provider), options),
1774
+ fn
1775
+ );
1399
1776
  }
1400
1777
 
1401
1778
  // src/observe.ts
@@ -1407,7 +1784,10 @@ function observe(fn, options = {}) {
1407
1784
  const captureOutput = options.captureOutput ?? true;
1408
1785
  const wrapped = (...args) => startAsCurrentSpan(
1409
1786
  name,
1410
- { kind: options.kind, input: captureInput ? args : void 0 },
1787
+ // {args, kwargs} is the Python SDK's shape; JavaScript has no keyword
1788
+ // arguments, so kwargs is always empty, but the key stays so a saved
1789
+ // search or a console view reads both SDKs' input.value the same way.
1790
+ { kind: options.kind, input: captureInput ? { args, kwargs: {} } : void 0 },
1411
1791
  async (observation) => {
1412
1792
  const result = await fn(...args);
1413
1793
  if (captureOutput && result !== void 0) observation.setOutput(result);
@@ -1421,9 +1801,10 @@ function observe(fn, options = {}) {
1421
1801
  // src/index.ts
1422
1802
  init_semconv();
1423
1803
  init_session();
1804
+ init_user();
1424
1805
  init_spans();
1425
1806
  init_workspace();
1426
- var VERSION = "0.4.0";
1807
+ var VERSION = "0.6.0";
1427
1808
  export {
1428
1809
  Generation,
1429
1810
  Observation,
@@ -1439,5 +1820,6 @@ export {
1439
1820
  startGeneration,
1440
1821
  startSpan,
1441
1822
  withSession,
1823
+ withUser,
1442
1824
  withWorkspace
1443
1825
  };