@glassflow-ai/rius 0.3.1 → 0.5.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.cjs CHANGED
@@ -356,7 +356,7 @@ function kindAttributes(kind) {
356
356
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
357
357
  return attributes;
358
358
  }
359
- var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, 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_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;
359
+ 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_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_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;
360
360
  var init_semconv = __esm({
361
361
  "src/semconv.ts"() {
362
362
  "use strict";
@@ -367,12 +367,17 @@ var init_semconv = __esm({
367
367
  INPUT_VALUE = "input.value";
368
368
  OUTPUT_VALUE = "output.value";
369
369
  SESSION_ID = "session.id";
370
+ WORKSPACE_ROUTE = "rius.workspace";
370
371
  GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
371
372
  GEN_AI_PROVIDER_NAME = "gen_ai.provider.name";
372
373
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
374
+ GEN_AI_REQUEST_REASONING_LEVEL = "gen_ai.request.reasoning.level";
373
375
  GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
374
376
  GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
375
377
  GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
378
+ GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens";
379
+ GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS = "gen_ai.usage.cache_write.input_tokens";
380
+ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS = "gen_ai.usage.reasoning.output_tokens";
376
381
  GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages";
377
382
  GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages";
378
383
  GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons";
@@ -400,6 +405,12 @@ var init_semconv = __esm({
400
405
  OUTPUT_VALUE,
401
406
  GEN_AI_INPUT_MESSAGES,
402
407
  GEN_AI_OUTPUT_MESSAGES,
408
+ // Sensitive per the GenAI conventions (semconv-genai#431): tool definitions
409
+ // routinely embed proprietary prompt engineering, and sometimes credentials
410
+ // or internal URLs in parameter defaults. gen_ai.tool.name stays: it is
411
+ // identity, not content.
412
+ "gen_ai.tool.description",
413
+ "gen_ai.tool.definitions",
403
414
  "gen_ai.prompt",
404
415
  "gen_ai.completion",
405
416
  "llm.input_messages",
@@ -435,7 +446,10 @@ var init_semconv = __esm({
435
446
  GEN_AI_TOOL_NAME,
436
447
  // Identity, not content: a pending span must be groupable into its
437
448
  // session while still running, that is the live view's whole point.
438
- SESSION_ID
449
+ SESSION_ID,
450
+ // Routing, not content: a crashed run's snapshot must land in the same
451
+ // workspace its final span would have. Stripped at export either way.
452
+ WORKSPACE_ROUTE
439
453
  ]);
440
454
  PENDING_IDENTITY_PREFIXES = [GEN_AI_REQUEST_PREFIX];
441
455
  }
@@ -1010,6 +1024,128 @@ var init_session = __esm({
1010
1024
  }
1011
1025
  });
1012
1026
 
1027
+ // src/workspace.ts
1028
+ function withWorkspace(alias, fn) {
1029
+ if (!alias) throw new Error("workspace alias must be a non-empty string");
1030
+ return import_api6.context.with(import_api6.context.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1031
+ }
1032
+ function setGlobalRouting(routing) {
1033
+ globalRouting = routing;
1034
+ }
1035
+ function registerWorkspace(alias, apiKey) {
1036
+ if (globalRouting === void 0) {
1037
+ throw new Error(
1038
+ "[rius] workspace routing is not enabled: pass workspaces (an empty object is fine) to init() to opt in before registering destinations"
1039
+ );
1040
+ }
1041
+ globalRouting.register(alias, apiKey);
1042
+ }
1043
+ var import_api6, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1044
+ var init_workspace = __esm({
1045
+ "src/workspace.ts"() {
1046
+ "use strict";
1047
+ init_cjs_shims();
1048
+ import_api6 = require("@opentelemetry/api");
1049
+ init_esm();
1050
+ init_semconv();
1051
+ WORKSPACE_KEY = (0, import_api6.createContextKey)("rius-workspace-alias");
1052
+ WorkspaceSpanProcessor = class {
1053
+ onStart(span, parentContext) {
1054
+ const alias = parentContext.getValue(WORKSPACE_KEY);
1055
+ if (typeof alias !== "string") return;
1056
+ const parent = import_api6.trace.getSpan(parentContext);
1057
+ const parentAlias = parent?.attributes?.[WORKSPACE_ROUTE];
1058
+ if (parentAlias !== void 0 && parentAlias !== alias) {
1059
+ console.warn(
1060
+ `[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.`
1061
+ );
1062
+ }
1063
+ span.setAttribute(WORKSPACE_ROUTE, alias);
1064
+ }
1065
+ onEnd(_span) {
1066
+ }
1067
+ async forceFlush() {
1068
+ }
1069
+ async shutdown() {
1070
+ }
1071
+ };
1072
+ RoutingSpanExporter = class {
1073
+ constructor(defaultExporter, factory, routes = {}) {
1074
+ this.defaultExporter = defaultExporter;
1075
+ this.factory = factory;
1076
+ this.routes = new Map(Object.entries(routes));
1077
+ }
1078
+ defaultExporter;
1079
+ factory;
1080
+ routes;
1081
+ exporters = /* @__PURE__ */ new Map();
1082
+ warnedAliases = /* @__PURE__ */ new Set();
1083
+ /** Add or replace a route. Replacing supports key rotation. */
1084
+ register(alias, apiKey) {
1085
+ if (!alias || !apiKey) {
1086
+ throw new Error("workspace alias and apiKey must be non-empty strings");
1087
+ }
1088
+ this.routes.set(alias, apiKey);
1089
+ this.warnedAliases.delete(alias);
1090
+ }
1091
+ export(spans, resultCallback) {
1092
+ const groups = /* @__PURE__ */ new Map();
1093
+ for (const span of spans) {
1094
+ const attributes = span.attributes;
1095
+ const raw = attributes[WORKSPACE_ROUTE];
1096
+ let destination = this.defaultExporter;
1097
+ if (raw !== void 0) {
1098
+ delete attributes[WORKSPACE_ROUTE];
1099
+ destination = this.resolve(String(raw));
1100
+ }
1101
+ const group = groups.get(destination);
1102
+ if (group === void 0) groups.set(destination, [span]);
1103
+ else group.push(span);
1104
+ }
1105
+ let pending = groups.size;
1106
+ if (pending === 0) {
1107
+ resultCallback({ code: ExportResultCode.SUCCESS });
1108
+ return;
1109
+ }
1110
+ let failed;
1111
+ for (const [exporter, group] of groups) {
1112
+ exporter.export(group, (result) => {
1113
+ if (result.code !== ExportResultCode.SUCCESS) failed = result;
1114
+ pending -= 1;
1115
+ if (pending === 0) resultCallback(failed ?? { code: ExportResultCode.SUCCESS });
1116
+ });
1117
+ }
1118
+ }
1119
+ resolve(alias) {
1120
+ const apiKey = this.routes.get(alias);
1121
+ if (apiKey === void 0) {
1122
+ if (!this.warnedAliases.has(alias)) {
1123
+ this.warnedAliases.add(alias);
1124
+ console.warn(
1125
+ `[rius] no workspace registered for alias "${alias}"; its spans go to the default destination. Register it with registerWorkspace("${alias}", apiKey) or in init({ workspaces }).`
1126
+ );
1127
+ }
1128
+ return this.defaultExporter;
1129
+ }
1130
+ let exporter = this.exporters.get(apiKey);
1131
+ if (exporter === void 0) {
1132
+ exporter = this.factory(apiKey);
1133
+ this.exporters.set(apiKey, exporter);
1134
+ }
1135
+ return exporter;
1136
+ }
1137
+ async forceFlush() {
1138
+ await this.defaultExporter.forceFlush?.();
1139
+ await Promise.all([...this.exporters.values()].map((e) => e.forceFlush?.()));
1140
+ }
1141
+ async shutdown() {
1142
+ await this.defaultExporter.shutdown();
1143
+ await Promise.all([...this.exporters.values()].map((e) => e.shutdown()));
1144
+ }
1145
+ };
1146
+ }
1147
+ });
1148
+
1013
1149
  // src/client.ts
1014
1150
  function init(options = {}) {
1015
1151
  if (globalClient !== void 0) {
@@ -1022,11 +1158,20 @@ function init(options = {}) {
1022
1158
  const processors = new DelegatingSpanProcessor();
1023
1159
  const authHeaders = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {};
1024
1160
  let health;
1161
+ let routing;
1025
1162
  if (!config.disabled) {
1026
- const base = options.spanExporter ?? new import_exporter_trace_otlp_proto.OTLPTraceExporter({
1163
+ let base = options.spanExporter ?? new import_exporter_trace_otlp_proto.OTLPTraceExporter({
1027
1164
  url: `${config.endpoint}/v1/traces`,
1028
1165
  headers: config.apiKey ? authHeaders : void 0
1029
1166
  });
1167
+ if (options.workspaces !== void 0) {
1168
+ const factory = options.workspaceExporterFactory ?? ((apiKey) => new import_exporter_trace_otlp_proto.OTLPTraceExporter({
1169
+ url: `${config.endpoint}/v1/traces`,
1170
+ headers: { Authorization: `Bearer ${apiKey}` }
1171
+ }));
1172
+ routing = new RoutingSpanExporter(base, factory, options.workspaces);
1173
+ base = routing;
1174
+ }
1030
1175
  health = new ExportOutcomeExporter(base);
1031
1176
  const exporter = !config.captureContent || config.mask !== void 0 ? new MaskingSpanExporter(health, {
1032
1177
  captureContent: config.captureContent,
@@ -1034,6 +1179,9 @@ function init(options = {}) {
1034
1179
  }) : health;
1035
1180
  const batch = new import_sdk_trace_base.BatchSpanProcessor(exporter);
1036
1181
  processors.add(new SessionSpanProcessor(config.sessionId));
1182
+ if (routing !== void 0) {
1183
+ processors.add(new WorkspaceSpanProcessor());
1184
+ }
1037
1185
  if (config.partialSpans) {
1038
1186
  processors.add(new PendingSpanProcessor(batch, { delayMs: config.partialSpansDelayMs }));
1039
1187
  }
@@ -1076,18 +1224,19 @@ function init(options = {}) {
1076
1224
  heartbeat = { sender, beforeExitHandler };
1077
1225
  }
1078
1226
  globalClient = createClient({ provider, processors, health, ready, heartbeat });
1227
+ setGlobalRouting(routing);
1079
1228
  return globalClient;
1080
1229
  }
1081
1230
  function getTracer() {
1082
- return import_api6.trace.getTracer(TRACER_NAME);
1231
+ return import_api7.trace.getTracer(TRACER_NAME);
1083
1232
  }
1084
- var import_node_crypto2, import_api6, import_exporter_trace_otlp_proto, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
1233
+ var import_node_crypto2, import_api7, import_exporter_trace_otlp_proto, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
1085
1234
  var init_client = __esm({
1086
1235
  "src/client.ts"() {
1087
1236
  "use strict";
1088
1237
  init_cjs_shims();
1089
1238
  import_node_crypto2 = require("crypto");
1090
- import_api6 = require("@opentelemetry/api");
1239
+ import_api7 = require("@opentelemetry/api");
1091
1240
  import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
1092
1241
  import_resources = require("@opentelemetry/resources");
1093
1242
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -1102,6 +1251,7 @@ var init_client = __esm({
1102
1251
  init_pending();
1103
1252
  init_semconv();
1104
1253
  init_session();
1254
+ init_workspace();
1105
1255
  sinks = /* @__PURE__ */ new WeakMap();
1106
1256
  heartbeats = /* @__PURE__ */ new WeakMap();
1107
1257
  RiusClient = class _RiusClient {
@@ -1145,7 +1295,8 @@ var init_client = __esm({
1145
1295
  } finally {
1146
1296
  if (globalClient === this) {
1147
1297
  globalClient = void 0;
1148
- import_api6.trace.disable();
1298
+ setGlobalRouting(void 0);
1299
+ import_api7.trace.disable();
1149
1300
  }
1150
1301
  }
1151
1302
  }
@@ -1164,11 +1315,13 @@ __export(index_exports, {
1164
1315
  getTracer: () => getTracer,
1165
1316
  init: () => init,
1166
1317
  observe: () => observe,
1318
+ registerWorkspace: () => registerWorkspace,
1167
1319
  startAsCurrentGeneration: () => startAsCurrentGeneration,
1168
1320
  startAsCurrentSpan: () => startAsCurrentSpan,
1169
1321
  startGeneration: () => startGeneration,
1170
1322
  startSpan: () => startSpan,
1171
- withSession: () => withSession
1323
+ withSession: () => withSession,
1324
+ withWorkspace: () => withWorkspace
1172
1325
  });
1173
1326
  module.exports = __toCommonJS(index_exports);
1174
1327
  init_cjs_shims();
@@ -1181,6 +1334,15 @@ init_semconv();
1181
1334
  init_serde();
1182
1335
  init_spans();
1183
1336
  var Generation = class extends Observation {
1337
+ /**
1338
+ * The provider passed at creation; drives the Anthropic input-token summing
1339
+ * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
1340
+ */
1341
+ constructor(span, provider) {
1342
+ super(span);
1343
+ this.provider = provider;
1344
+ }
1345
+ provider;
1184
1346
  firstTokenRecorded = false;
1185
1347
  setInput(value) {
1186
1348
  this.span.setAttribute(GEN_AI_INPUT_MESSAGES, toAttributeValue(value));
@@ -1194,13 +1356,34 @@ var Generation = class extends Observation {
1194
1356
  this.span.setAttribute(GEN_AI_RESPONSE_MODEL, model);
1195
1357
  return this;
1196
1358
  }
1359
+ /**
1360
+ * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is;
1361
+ * never pre-add anything. Per the GenAI conventions, `inputTokens` is the
1362
+ * total including cached tokens (the cache counts are subsets of it).
1363
+ * Anthropic's API reports `input_tokens` excluding the cache counts, and
1364
+ * the conventions require the instrumentation to do the summing, so when
1365
+ * the generation's provider is `"anthropic"` the emitted total is
1366
+ * `inputTokens` plus both cache counts. Every other provider is recorded
1367
+ * verbatim.
1368
+ */
1197
1369
  setUsage(usage) {
1198
1370
  if (usage.inputTokens !== void 0) {
1199
- this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, usage.inputTokens);
1371
+ const sums = this.provider?.toLowerCase() === "anthropic";
1372
+ const total = sums ? usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) : usage.inputTokens;
1373
+ this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, total);
1200
1374
  }
1201
1375
  if (usage.outputTokens !== void 0) {
1202
1376
  this.span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage.outputTokens);
1203
1377
  }
1378
+ if (usage.cacheReadInputTokens !== void 0) {
1379
+ this.span.setAttribute(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, usage.cacheReadInputTokens);
1380
+ }
1381
+ if (usage.cacheWriteInputTokens !== void 0) {
1382
+ this.span.setAttribute(GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, usage.cacheWriteInputTokens);
1383
+ }
1384
+ if (usage.reasoningOutputTokens !== void 0) {
1385
+ this.span.setAttribute(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.reasoningOutputTokens);
1386
+ }
1204
1387
  return this;
1205
1388
  }
1206
1389
  /**
@@ -1240,17 +1423,20 @@ function configure2(generation, options) {
1240
1423
  for (const [key, value] of Object.entries(options.modelParameters ?? {})) {
1241
1424
  generation.setAttribute(`${GEN_AI_REQUEST_PREFIX}${key}`, value);
1242
1425
  }
1426
+ if (options.reasoningLevel !== void 0) {
1427
+ generation.setAttribute(GEN_AI_REQUEST_REASONING_LEVEL, options.reasoningLevel);
1428
+ }
1243
1429
  if (options.input !== void 0) generation.setInput(options.input);
1244
1430
  return generation;
1245
1431
  }
1246
1432
  function startGeneration(name, options = {}) {
1247
1433
  const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1248
- return configure2(new Generation(span), options);
1434
+ return configure2(new Generation(span, options.provider), options);
1249
1435
  }
1250
1436
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1251
1437
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1252
1438
  return getTracer().startActiveSpan(name, { attributes: attributesFor(options) }, async (span) => {
1253
- const generation = configure2(new Generation(span), options);
1439
+ const generation = configure2(new Generation(span, options.provider), options);
1254
1440
  try {
1255
1441
  return await fn(generation);
1256
1442
  } catch (error) {
@@ -1286,7 +1472,8 @@ function observe(fn, options = {}) {
1286
1472
  init_semconv();
1287
1473
  init_session();
1288
1474
  init_spans();
1289
- var VERSION = "0.3.1";
1475
+ init_workspace();
1476
+ var VERSION = "0.5.0";
1290
1477
  // Annotate the CommonJS export names for ESM import in node:
1291
1478
  0 && (module.exports = {
1292
1479
  Generation,
@@ -1297,9 +1484,11 @@ var VERSION = "0.3.1";
1297
1484
  getTracer,
1298
1485
  init,
1299
1486
  observe,
1487
+ registerWorkspace,
1300
1488
  startAsCurrentGeneration,
1301
1489
  startAsCurrentSpan,
1302
1490
  startGeneration,
1303
1491
  startSpan,
1304
- withSession
1492
+ withSession,
1493
+ withWorkspace
1305
1494
  });
package/dist/index.d.cts CHANGED
@@ -35,12 +35,80 @@ interface RiusOptions {
35
35
 
36
36
  type HeartbeatTransport = (payload: Record<string, unknown>, timeoutMs: number) => Promise<void>;
37
37
 
38
+ /**
39
+ * Workspaces: route spans from one process to per-customer destinations.
40
+ *
41
+ * One client, one provider, one batch pipeline; the *destination* is a
42
+ * context-scoped property. `withWorkspace(alias, fn)` sets an OTel context
43
+ * key (exactly like `withSession`), `WorkspaceSpanProcessor` stamps it as a
44
+ * transient attribute at span start, and `RoutingSpanExporter` partitions
45
+ * each export batch by that attribute, strips it, and forwards every
46
+ * partition to the exporter registered for its alias. Spans started outside
47
+ * any scope go to the default destination.
48
+ *
49
+ * Because the alias rides OTel context, everything started in scope routes
50
+ * together: `observe` wrappers, generations, sessions, and spans created by
51
+ * auto-instrumentation. That is the property a second client could never
52
+ * give, which is why this SDK has no scoped-client mode at all.
53
+ *
54
+ * Two rules the design enforces or warns about:
55
+ *
56
+ * - The routing attribute never reaches the wire. The destination's API key
57
+ * is what tells the backend which workspace a span belongs to; the alias
58
+ * is process-local configuration, so the exporter strips it before
59
+ * delegating.
60
+ * - One trace, one workspace. The backend derives the workspace from the API
61
+ * key per request, so a trace split across scopes would come apart.
62
+ * Starting a span under a different alias than its parent's logs a
63
+ * warning; switch workspaces at request boundaries, not inside a trace.
64
+ */
65
+
66
+ type WorkspaceExporterFactory = (apiKey: string) => SpanExporter;
67
+ /**
68
+ * Scope every span started inside `fn` to one workspace destination.
69
+ *
70
+ * `alias` names a workspace registered via `init({ workspaces })` or
71
+ * `registerWorkspace()`; the scope's spans are exported with that
72
+ * workspace's API key. Scopes nest and follow async continuations the way
73
+ * all OTel context does, but a trace must stay inside one workspace: enter
74
+ * the scope at a request boundary, before the root span starts.
75
+ *
76
+ * ```typescript
77
+ * await withWorkspace("acme", async () => {
78
+ * await handle(request) // every span of the request lands in acme's workspace
79
+ * })
80
+ * ```
81
+ */
82
+ declare function withWorkspace<T>(alias: string, fn: (alias: string) => T): T;
83
+ /**
84
+ * Add (or rotate the key of) a workspace destination on the global client.
85
+ *
86
+ * Requires `init({ workspaces })` to have opted into routing (an empty
87
+ * object opts in with no static routes). Spans started inside
88
+ * `withWorkspace(alias, ...)` are then exported with `apiKey`.
89
+ */
90
+ declare function registerWorkspace(alias: string, apiKey: string): void;
91
+
38
92
  /** Options accepted by {@link init}, extending the shared configuration. */
39
93
  interface InitOptions extends RiusOptions {
40
94
  /** Inject an exporter instead of OTLP. The test seam; prefer this to mocking. */
41
95
  spanExporter?: SpanExporter;
42
96
  /** Override the heartbeat HTTP transport. The test seam; prefer this to mocking fetch. */
43
97
  heartbeatTransport?: HeartbeatTransport;
98
+ /**
99
+ * Enable multi-workspace routing: a map of alias to API key. Spans started
100
+ * inside `withWorkspace(alias, fn)` are exported with that workspace's
101
+ * key; spans outside any scope use the default `apiKey`. Pass `{}` to opt
102
+ * in with no static routes and register destinations later via
103
+ * `registerWorkspace()`. One trace must stay inside one workspace.
104
+ */
105
+ workspaces?: Record<string, string>;
106
+ /**
107
+ * Override how per-workspace exporters are built from an API key. The
108
+ * test seam, like `spanExporter`; defaults to the standard OTLP exporter
109
+ * against the configured endpoint.
110
+ */
111
+ workspaceExporterFactory?: WorkspaceExporterFactory;
44
112
  }
45
113
  /**
46
114
  * Handle over a configured tracer pipeline, returned by {@link init}.
@@ -155,16 +223,54 @@ interface GenerationOptions {
155
223
  * so use the provider's own parameter names.
156
224
  */
157
225
  modelParameters?: Record<string, unknown>;
226
+ /**
227
+ * Requested reasoning/thinking effort level
228
+ * (`gen_ai.request.reasoning.level`), e.g. OpenAI's `reasoning.effort`
229
+ * values. Provider-defined string, recorded verbatim. A first-class option
230
+ * because the `modelParameters` pass-through would spell the key
231
+ * `gen_ai.request.reasoning_level`, which is not the convention's name.
232
+ */
233
+ reasoningLevel?: string;
158
234
  }
159
235
  /** An LLM call. Content uses gen_ai message keys, never input.value. */
160
236
  declare class Generation extends Observation {
237
+ private readonly provider?;
161
238
  private firstTokenRecorded;
239
+ /**
240
+ * The provider passed at creation; drives the Anthropic input-token summing
241
+ * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
242
+ */
243
+ constructor(span: ConstructorParameters<typeof Observation>[0], provider?: string | undefined);
162
244
  setInput(value: unknown): this;
163
245
  setOutput(value: unknown): this;
164
246
  setModel(model: string): this;
247
+ /**
248
+ * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is;
249
+ * never pre-add anything. Per the GenAI conventions, `inputTokens` is the
250
+ * total including cached tokens (the cache counts are subsets of it).
251
+ * Anthropic's API reports `input_tokens` excluding the cache counts, and
252
+ * the conventions require the instrumentation to do the summing, so when
253
+ * the generation's provider is `"anthropic"` the emitted total is
254
+ * `inputTokens` plus both cache counts. Every other provider is recorded
255
+ * verbatim.
256
+ */
165
257
  setUsage(usage: {
166
258
  inputTokens?: number;
167
259
  outputTokens?: number;
260
+ /** Input tokens served from a provider-managed prompt cache. */
261
+ cacheReadInputTokens?: number;
262
+ /**
263
+ * Input tokens written to a provider-managed prompt cache
264
+ * (called "cache creation" by Anthropic).
265
+ */
266
+ cacheWriteInputTokens?: number;
267
+ /**
268
+ * Output tokens spent on reasoning / extended thinking. A subset of
269
+ * `outputTokens`, never in addition to it: providers already include
270
+ * reasoning tokens in the output total, so pass both as reported and
271
+ * do no arithmetic.
272
+ */
273
+ reasoningOutputTokens?: number;
168
274
  }): this;
169
275
  /**
170
276
  * Why generation stopped (`gen_ai.response.finish_reasons`), e.g. `"stop"`,
@@ -233,6 +339,6 @@ declare function observe<F extends (...args: never[]) => unknown>(fn: F, options
233
339
  declare function withSession<T>(fn: (sessionId: string) => T): T;
234
340
  declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T): T;
235
341
 
236
- declare const VERSION = "0.3.1";
342
+ declare const VERSION = "0.5.0";
237
343
 
238
- export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, getTracer, init, observe, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession };
344
+ export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, type WorkspaceExporterFactory, getTracer, init, observe, registerWorkspace, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession, withWorkspace };
package/dist/index.d.ts CHANGED
@@ -35,12 +35,80 @@ interface RiusOptions {
35
35
 
36
36
  type HeartbeatTransport = (payload: Record<string, unknown>, timeoutMs: number) => Promise<void>;
37
37
 
38
+ /**
39
+ * Workspaces: route spans from one process to per-customer destinations.
40
+ *
41
+ * One client, one provider, one batch pipeline; the *destination* is a
42
+ * context-scoped property. `withWorkspace(alias, fn)` sets an OTel context
43
+ * key (exactly like `withSession`), `WorkspaceSpanProcessor` stamps it as a
44
+ * transient attribute at span start, and `RoutingSpanExporter` partitions
45
+ * each export batch by that attribute, strips it, and forwards every
46
+ * partition to the exporter registered for its alias. Spans started outside
47
+ * any scope go to the default destination.
48
+ *
49
+ * Because the alias rides OTel context, everything started in scope routes
50
+ * together: `observe` wrappers, generations, sessions, and spans created by
51
+ * auto-instrumentation. That is the property a second client could never
52
+ * give, which is why this SDK has no scoped-client mode at all.
53
+ *
54
+ * Two rules the design enforces or warns about:
55
+ *
56
+ * - The routing attribute never reaches the wire. The destination's API key
57
+ * is what tells the backend which workspace a span belongs to; the alias
58
+ * is process-local configuration, so the exporter strips it before
59
+ * delegating.
60
+ * - One trace, one workspace. The backend derives the workspace from the API
61
+ * key per request, so a trace split across scopes would come apart.
62
+ * Starting a span under a different alias than its parent's logs a
63
+ * warning; switch workspaces at request boundaries, not inside a trace.
64
+ */
65
+
66
+ type WorkspaceExporterFactory = (apiKey: string) => SpanExporter;
67
+ /**
68
+ * Scope every span started inside `fn` to one workspace destination.
69
+ *
70
+ * `alias` names a workspace registered via `init({ workspaces })` or
71
+ * `registerWorkspace()`; the scope's spans are exported with that
72
+ * workspace's API key. Scopes nest and follow async continuations the way
73
+ * all OTel context does, but a trace must stay inside one workspace: enter
74
+ * the scope at a request boundary, before the root span starts.
75
+ *
76
+ * ```typescript
77
+ * await withWorkspace("acme", async () => {
78
+ * await handle(request) // every span of the request lands in acme's workspace
79
+ * })
80
+ * ```
81
+ */
82
+ declare function withWorkspace<T>(alias: string, fn: (alias: string) => T): T;
83
+ /**
84
+ * Add (or rotate the key of) a workspace destination on the global client.
85
+ *
86
+ * Requires `init({ workspaces })` to have opted into routing (an empty
87
+ * object opts in with no static routes). Spans started inside
88
+ * `withWorkspace(alias, ...)` are then exported with `apiKey`.
89
+ */
90
+ declare function registerWorkspace(alias: string, apiKey: string): void;
91
+
38
92
  /** Options accepted by {@link init}, extending the shared configuration. */
39
93
  interface InitOptions extends RiusOptions {
40
94
  /** Inject an exporter instead of OTLP. The test seam; prefer this to mocking. */
41
95
  spanExporter?: SpanExporter;
42
96
  /** Override the heartbeat HTTP transport. The test seam; prefer this to mocking fetch. */
43
97
  heartbeatTransport?: HeartbeatTransport;
98
+ /**
99
+ * Enable multi-workspace routing: a map of alias to API key. Spans started
100
+ * inside `withWorkspace(alias, fn)` are exported with that workspace's
101
+ * key; spans outside any scope use the default `apiKey`. Pass `{}` to opt
102
+ * in with no static routes and register destinations later via
103
+ * `registerWorkspace()`. One trace must stay inside one workspace.
104
+ */
105
+ workspaces?: Record<string, string>;
106
+ /**
107
+ * Override how per-workspace exporters are built from an API key. The
108
+ * test seam, like `spanExporter`; defaults to the standard OTLP exporter
109
+ * against the configured endpoint.
110
+ */
111
+ workspaceExporterFactory?: WorkspaceExporterFactory;
44
112
  }
45
113
  /**
46
114
  * Handle over a configured tracer pipeline, returned by {@link init}.
@@ -155,16 +223,54 @@ interface GenerationOptions {
155
223
  * so use the provider's own parameter names.
156
224
  */
157
225
  modelParameters?: Record<string, unknown>;
226
+ /**
227
+ * Requested reasoning/thinking effort level
228
+ * (`gen_ai.request.reasoning.level`), e.g. OpenAI's `reasoning.effort`
229
+ * values. Provider-defined string, recorded verbatim. A first-class option
230
+ * because the `modelParameters` pass-through would spell the key
231
+ * `gen_ai.request.reasoning_level`, which is not the convention's name.
232
+ */
233
+ reasoningLevel?: string;
158
234
  }
159
235
  /** An LLM call. Content uses gen_ai message keys, never input.value. */
160
236
  declare class Generation extends Observation {
237
+ private readonly provider?;
161
238
  private firstTokenRecorded;
239
+ /**
240
+ * The provider passed at creation; drives the Anthropic input-token summing
241
+ * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
242
+ */
243
+ constructor(span: ConstructorParameters<typeof Observation>[0], provider?: string | undefined);
162
244
  setInput(value: unknown): this;
163
245
  setOutput(value: unknown): this;
164
246
  setModel(model: string): this;
247
+ /**
248
+ * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is;
249
+ * never pre-add anything. Per the GenAI conventions, `inputTokens` is the
250
+ * total including cached tokens (the cache counts are subsets of it).
251
+ * Anthropic's API reports `input_tokens` excluding the cache counts, and
252
+ * the conventions require the instrumentation to do the summing, so when
253
+ * the generation's provider is `"anthropic"` the emitted total is
254
+ * `inputTokens` plus both cache counts. Every other provider is recorded
255
+ * verbatim.
256
+ */
165
257
  setUsage(usage: {
166
258
  inputTokens?: number;
167
259
  outputTokens?: number;
260
+ /** Input tokens served from a provider-managed prompt cache. */
261
+ cacheReadInputTokens?: number;
262
+ /**
263
+ * Input tokens written to a provider-managed prompt cache
264
+ * (called "cache creation" by Anthropic).
265
+ */
266
+ cacheWriteInputTokens?: number;
267
+ /**
268
+ * Output tokens spent on reasoning / extended thinking. A subset of
269
+ * `outputTokens`, never in addition to it: providers already include
270
+ * reasoning tokens in the output total, so pass both as reported and
271
+ * do no arithmetic.
272
+ */
273
+ reasoningOutputTokens?: number;
168
274
  }): this;
169
275
  /**
170
276
  * Why generation stopped (`gen_ai.response.finish_reasons`), e.g. `"stop"`,
@@ -233,6 +339,6 @@ declare function observe<F extends (...args: never[]) => unknown>(fn: F, options
233
339
  declare function withSession<T>(fn: (sessionId: string) => T): T;
234
340
  declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T): T;
235
341
 
236
- declare const VERSION = "0.3.1";
342
+ declare const VERSION = "0.5.0";
237
343
 
238
- export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, getTracer, init, observe, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession };
344
+ export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, type WorkspaceExporterFactory, getTracer, init, observe, registerWorkspace, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession, withWorkspace };
package/dist/index.js CHANGED
@@ -343,7 +343,7 @@ function kindAttributes(kind) {
343
343
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
344
344
  return attributes;
345
345
  }
346
- var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, 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_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;
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_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_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;
347
347
  var init_semconv = __esm({
348
348
  "src/semconv.ts"() {
349
349
  "use strict";
@@ -354,12 +354,17 @@ var init_semconv = __esm({
354
354
  INPUT_VALUE = "input.value";
355
355
  OUTPUT_VALUE = "output.value";
356
356
  SESSION_ID = "session.id";
357
+ WORKSPACE_ROUTE = "rius.workspace";
357
358
  GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
358
359
  GEN_AI_PROVIDER_NAME = "gen_ai.provider.name";
359
360
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
361
+ GEN_AI_REQUEST_REASONING_LEVEL = "gen_ai.request.reasoning.level";
360
362
  GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
361
363
  GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
362
364
  GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
365
+ GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens";
366
+ GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS = "gen_ai.usage.cache_write.input_tokens";
367
+ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS = "gen_ai.usage.reasoning.output_tokens";
363
368
  GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages";
364
369
  GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages";
365
370
  GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons";
@@ -387,6 +392,12 @@ var init_semconv = __esm({
387
392
  OUTPUT_VALUE,
388
393
  GEN_AI_INPUT_MESSAGES,
389
394
  GEN_AI_OUTPUT_MESSAGES,
395
+ // Sensitive per the GenAI conventions (semconv-genai#431): tool definitions
396
+ // routinely embed proprietary prompt engineering, and sometimes credentials
397
+ // or internal URLs in parameter defaults. gen_ai.tool.name stays: it is
398
+ // identity, not content.
399
+ "gen_ai.tool.description",
400
+ "gen_ai.tool.definitions",
390
401
  "gen_ai.prompt",
391
402
  "gen_ai.completion",
392
403
  "llm.input_messages",
@@ -422,7 +433,10 @@ var init_semconv = __esm({
422
433
  GEN_AI_TOOL_NAME,
423
434
  // Identity, not content: a pending span must be groupable into its
424
435
  // session while still running, that is the live view's whole point.
425
- SESSION_ID
436
+ SESSION_ID,
437
+ // Routing, not content: a crashed run's snapshot must land in the same
438
+ // workspace its final span would have. Stripped at export either way.
439
+ WORKSPACE_ROUTE
426
440
  ]);
427
441
  PENDING_IDENTITY_PREFIXES = [GEN_AI_REQUEST_PREFIX];
428
442
  }
@@ -997,9 +1011,131 @@ var init_session = __esm({
997
1011
  }
998
1012
  });
999
1013
 
1014
+ // src/workspace.ts
1015
+ import { context as apiContext2, createContextKey as createContextKey2, trace } from "@opentelemetry/api";
1016
+ function withWorkspace(alias, fn) {
1017
+ if (!alias) throw new Error("workspace alias must be a non-empty string");
1018
+ return apiContext2.with(apiContext2.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1019
+ }
1020
+ function setGlobalRouting(routing) {
1021
+ globalRouting = routing;
1022
+ }
1023
+ function registerWorkspace(alias, apiKey) {
1024
+ if (globalRouting === void 0) {
1025
+ throw new Error(
1026
+ "[rius] workspace routing is not enabled: pass workspaces (an empty object is fine) to init() to opt in before registering destinations"
1027
+ );
1028
+ }
1029
+ globalRouting.register(alias, apiKey);
1030
+ }
1031
+ var WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1032
+ var init_workspace = __esm({
1033
+ "src/workspace.ts"() {
1034
+ "use strict";
1035
+ init_esm_shims();
1036
+ init_esm();
1037
+ init_semconv();
1038
+ WORKSPACE_KEY = createContextKey2("rius-workspace-alias");
1039
+ WorkspaceSpanProcessor = class {
1040
+ onStart(span, parentContext) {
1041
+ const alias = parentContext.getValue(WORKSPACE_KEY);
1042
+ if (typeof alias !== "string") return;
1043
+ const parent = trace.getSpan(parentContext);
1044
+ const parentAlias = parent?.attributes?.[WORKSPACE_ROUTE];
1045
+ if (parentAlias !== void 0 && parentAlias !== alias) {
1046
+ console.warn(
1047
+ `[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.`
1048
+ );
1049
+ }
1050
+ span.setAttribute(WORKSPACE_ROUTE, alias);
1051
+ }
1052
+ onEnd(_span) {
1053
+ }
1054
+ async forceFlush() {
1055
+ }
1056
+ async shutdown() {
1057
+ }
1058
+ };
1059
+ RoutingSpanExporter = class {
1060
+ constructor(defaultExporter, factory, routes = {}) {
1061
+ this.defaultExporter = defaultExporter;
1062
+ this.factory = factory;
1063
+ this.routes = new Map(Object.entries(routes));
1064
+ }
1065
+ defaultExporter;
1066
+ factory;
1067
+ routes;
1068
+ exporters = /* @__PURE__ */ new Map();
1069
+ warnedAliases = /* @__PURE__ */ new Set();
1070
+ /** Add or replace a route. Replacing supports key rotation. */
1071
+ register(alias, apiKey) {
1072
+ if (!alias || !apiKey) {
1073
+ throw new Error("workspace alias and apiKey must be non-empty strings");
1074
+ }
1075
+ this.routes.set(alias, apiKey);
1076
+ this.warnedAliases.delete(alias);
1077
+ }
1078
+ export(spans, resultCallback) {
1079
+ const groups = /* @__PURE__ */ new Map();
1080
+ for (const span of spans) {
1081
+ const attributes = span.attributes;
1082
+ const raw = attributes[WORKSPACE_ROUTE];
1083
+ let destination = this.defaultExporter;
1084
+ if (raw !== void 0) {
1085
+ delete attributes[WORKSPACE_ROUTE];
1086
+ destination = this.resolve(String(raw));
1087
+ }
1088
+ const group = groups.get(destination);
1089
+ if (group === void 0) groups.set(destination, [span]);
1090
+ else group.push(span);
1091
+ }
1092
+ let pending = groups.size;
1093
+ if (pending === 0) {
1094
+ resultCallback({ code: ExportResultCode.SUCCESS });
1095
+ return;
1096
+ }
1097
+ let failed;
1098
+ for (const [exporter, group] of groups) {
1099
+ exporter.export(group, (result) => {
1100
+ if (result.code !== ExportResultCode.SUCCESS) failed = result;
1101
+ pending -= 1;
1102
+ if (pending === 0) resultCallback(failed ?? { code: ExportResultCode.SUCCESS });
1103
+ });
1104
+ }
1105
+ }
1106
+ resolve(alias) {
1107
+ const apiKey = this.routes.get(alias);
1108
+ if (apiKey === void 0) {
1109
+ if (!this.warnedAliases.has(alias)) {
1110
+ this.warnedAliases.add(alias);
1111
+ console.warn(
1112
+ `[rius] no workspace registered for alias "${alias}"; its spans go to the default destination. Register it with registerWorkspace("${alias}", apiKey) or in init({ workspaces }).`
1113
+ );
1114
+ }
1115
+ return this.defaultExporter;
1116
+ }
1117
+ let exporter = this.exporters.get(apiKey);
1118
+ if (exporter === void 0) {
1119
+ exporter = this.factory(apiKey);
1120
+ this.exporters.set(apiKey, exporter);
1121
+ }
1122
+ return exporter;
1123
+ }
1124
+ async forceFlush() {
1125
+ await this.defaultExporter.forceFlush?.();
1126
+ await Promise.all([...this.exporters.values()].map((e) => e.forceFlush?.()));
1127
+ }
1128
+ async shutdown() {
1129
+ await this.defaultExporter.shutdown();
1130
+ await Promise.all([...this.exporters.values()].map((e) => e.shutdown()));
1131
+ }
1132
+ };
1133
+ }
1134
+ });
1135
+
1000
1136
  // src/client.ts
1001
1137
  import { randomUUID as randomUUID2 } from "crypto";
1002
- import { trace } from "@opentelemetry/api";
1138
+ import { trace as trace2 } from "@opentelemetry/api";
1003
1139
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
1004
1140
  import { resourceFromAttributes } from "@opentelemetry/resources";
1005
1141
  import {
@@ -1020,11 +1156,20 @@ function init(options = {}) {
1020
1156
  const processors = new DelegatingSpanProcessor();
1021
1157
  const authHeaders = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {};
1022
1158
  let health;
1159
+ let routing;
1023
1160
  if (!config.disabled) {
1024
- const base = options.spanExporter ?? new OTLPTraceExporter({
1161
+ let base = options.spanExporter ?? new OTLPTraceExporter({
1025
1162
  url: `${config.endpoint}/v1/traces`,
1026
1163
  headers: config.apiKey ? authHeaders : void 0
1027
1164
  });
1165
+ if (options.workspaces !== void 0) {
1166
+ const factory = options.workspaceExporterFactory ?? ((apiKey) => new OTLPTraceExporter({
1167
+ url: `${config.endpoint}/v1/traces`,
1168
+ headers: { Authorization: `Bearer ${apiKey}` }
1169
+ }));
1170
+ routing = new RoutingSpanExporter(base, factory, options.workspaces);
1171
+ base = routing;
1172
+ }
1028
1173
  health = new ExportOutcomeExporter(base);
1029
1174
  const exporter = !config.captureContent || config.mask !== void 0 ? new MaskingSpanExporter(health, {
1030
1175
  captureContent: config.captureContent,
@@ -1032,6 +1177,9 @@ function init(options = {}) {
1032
1177
  }) : health;
1033
1178
  const batch = new BatchSpanProcessor(exporter);
1034
1179
  processors.add(new SessionSpanProcessor(config.sessionId));
1180
+ if (routing !== void 0) {
1181
+ processors.add(new WorkspaceSpanProcessor());
1182
+ }
1035
1183
  if (config.partialSpans) {
1036
1184
  processors.add(new PendingSpanProcessor(batch, { delayMs: config.partialSpansDelayMs }));
1037
1185
  }
@@ -1074,10 +1222,11 @@ function init(options = {}) {
1074
1222
  heartbeat = { sender, beforeExitHandler };
1075
1223
  }
1076
1224
  globalClient = createClient({ provider, processors, health, ready, heartbeat });
1225
+ setGlobalRouting(routing);
1077
1226
  return globalClient;
1078
1227
  }
1079
1228
  function getTracer() {
1080
- return trace.getTracer(TRACER_NAME);
1229
+ return trace2.getTracer(TRACER_NAME);
1081
1230
  }
1082
1231
  var sinks, heartbeats, createClient, RiusClient, globalClient;
1083
1232
  var init_client = __esm({
@@ -1093,6 +1242,7 @@ var init_client = __esm({
1093
1242
  init_pending();
1094
1243
  init_semconv();
1095
1244
  init_session();
1245
+ init_workspace();
1096
1246
  sinks = /* @__PURE__ */ new WeakMap();
1097
1247
  heartbeats = /* @__PURE__ */ new WeakMap();
1098
1248
  RiusClient = class _RiusClient {
@@ -1136,7 +1286,8 @@ var init_client = __esm({
1136
1286
  } finally {
1137
1287
  if (globalClient === this) {
1138
1288
  globalClient = void 0;
1139
- trace.disable();
1289
+ setGlobalRouting(void 0);
1290
+ trace2.disable();
1140
1291
  }
1141
1292
  }
1142
1293
  }
@@ -1155,6 +1306,15 @@ init_semconv();
1155
1306
  init_serde();
1156
1307
  init_spans();
1157
1308
  var Generation = class extends Observation {
1309
+ /**
1310
+ * The provider passed at creation; drives the Anthropic input-token summing
1311
+ * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
1312
+ */
1313
+ constructor(span, provider) {
1314
+ super(span);
1315
+ this.provider = provider;
1316
+ }
1317
+ provider;
1158
1318
  firstTokenRecorded = false;
1159
1319
  setInput(value) {
1160
1320
  this.span.setAttribute(GEN_AI_INPUT_MESSAGES, toAttributeValue(value));
@@ -1168,13 +1328,34 @@ var Generation = class extends Observation {
1168
1328
  this.span.setAttribute(GEN_AI_RESPONSE_MODEL, model);
1169
1329
  return this;
1170
1330
  }
1331
+ /**
1332
+ * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is;
1333
+ * never pre-add anything. Per the GenAI conventions, `inputTokens` is the
1334
+ * total including cached tokens (the cache counts are subsets of it).
1335
+ * Anthropic's API reports `input_tokens` excluding the cache counts, and
1336
+ * the conventions require the instrumentation to do the summing, so when
1337
+ * the generation's provider is `"anthropic"` the emitted total is
1338
+ * `inputTokens` plus both cache counts. Every other provider is recorded
1339
+ * verbatim.
1340
+ */
1171
1341
  setUsage(usage) {
1172
1342
  if (usage.inputTokens !== void 0) {
1173
- this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, usage.inputTokens);
1343
+ const sums = this.provider?.toLowerCase() === "anthropic";
1344
+ const total = sums ? usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) : usage.inputTokens;
1345
+ this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, total);
1174
1346
  }
1175
1347
  if (usage.outputTokens !== void 0) {
1176
1348
  this.span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage.outputTokens);
1177
1349
  }
1350
+ if (usage.cacheReadInputTokens !== void 0) {
1351
+ this.span.setAttribute(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, usage.cacheReadInputTokens);
1352
+ }
1353
+ if (usage.cacheWriteInputTokens !== void 0) {
1354
+ this.span.setAttribute(GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, usage.cacheWriteInputTokens);
1355
+ }
1356
+ if (usage.reasoningOutputTokens !== void 0) {
1357
+ this.span.setAttribute(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.reasoningOutputTokens);
1358
+ }
1178
1359
  return this;
1179
1360
  }
1180
1361
  /**
@@ -1214,17 +1395,20 @@ function configure2(generation, options) {
1214
1395
  for (const [key, value] of Object.entries(options.modelParameters ?? {})) {
1215
1396
  generation.setAttribute(`${GEN_AI_REQUEST_PREFIX}${key}`, value);
1216
1397
  }
1398
+ if (options.reasoningLevel !== void 0) {
1399
+ generation.setAttribute(GEN_AI_REQUEST_REASONING_LEVEL, options.reasoningLevel);
1400
+ }
1217
1401
  if (options.input !== void 0) generation.setInput(options.input);
1218
1402
  return generation;
1219
1403
  }
1220
1404
  function startGeneration(name, options = {}) {
1221
1405
  const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1222
- return configure2(new Generation(span), options);
1406
+ return configure2(new Generation(span, options.provider), options);
1223
1407
  }
1224
1408
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1225
1409
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1226
1410
  return getTracer().startActiveSpan(name, { attributes: attributesFor(options) }, async (span) => {
1227
- const generation = configure2(new Generation(span), options);
1411
+ const generation = configure2(new Generation(span, options.provider), options);
1228
1412
  try {
1229
1413
  return await fn(generation);
1230
1414
  } catch (error) {
@@ -1260,7 +1444,8 @@ function observe(fn, options = {}) {
1260
1444
  init_semconv();
1261
1445
  init_session();
1262
1446
  init_spans();
1263
- var VERSION = "0.3.1";
1447
+ init_workspace();
1448
+ var VERSION = "0.5.0";
1264
1449
  export {
1265
1450
  Generation,
1266
1451
  Observation,
@@ -1270,9 +1455,11 @@ export {
1270
1455
  getTracer,
1271
1456
  init,
1272
1457
  observe,
1458
+ registerWorkspace,
1273
1459
  startAsCurrentGeneration,
1274
1460
  startAsCurrentSpan,
1275
1461
  startGeneration,
1276
1462
  startSpan,
1277
- withSession
1463
+ withSession,
1464
+ withWorkspace
1278
1465
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassflow-ai/rius",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "OpenTelemetry-native tracing for AI agents and LLM applications",
5
5
  "keywords": [
6
6
  "opentelemetry",