@struct-ai/sdk 0.3.0 → 0.4.2

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.
Files changed (58) hide show
  1. package/README.md +101 -16
  2. package/dist/commonjs/context.d.ts +45 -0
  3. package/dist/commonjs/context.js +78 -1
  4. package/dist/commonjs/core.js +184 -29
  5. package/dist/commonjs/events.d.ts +17 -6
  6. package/dist/commonjs/events.js +82 -59
  7. package/dist/commonjs/genai-content.d.ts +52 -0
  8. package/dist/commonjs/genai-content.js +143 -0
  9. package/dist/commonjs/instrument.d.ts +47 -0
  10. package/dist/commonjs/instrument.js +158 -0
  11. package/dist/commonjs/integrations/anthropic-content.js +18 -6
  12. package/dist/commonjs/integrations/anthropic.d.ts +8 -1
  13. package/dist/commonjs/integrations/anthropic.js +515 -104
  14. package/dist/commonjs/integrations/index.js +8 -0
  15. package/dist/commonjs/integrations/langchain-callback.d.ts +182 -27
  16. package/dist/commonjs/integrations/langchain-callback.js +754 -87
  17. package/dist/commonjs/integrations/langchain-content.js +1 -1
  18. package/dist/commonjs/integrations/langchain.d.ts +3 -0
  19. package/dist/commonjs/integrations/langchain.js +353 -7
  20. package/dist/commonjs/integrations/openai-content.d.ts +34 -0
  21. package/dist/commonjs/integrations/openai-content.js +375 -0
  22. package/dist/commonjs/integrations/openai.d.ts +39 -0
  23. package/dist/commonjs/integrations/openai.js +305 -0
  24. package/dist/commonjs/semconv.d.ts +12 -0
  25. package/dist/commonjs/semconv.js +13 -1
  26. package/dist/commonjs/truncation.d.ts +29 -0
  27. package/dist/commonjs/truncation.js +184 -10
  28. package/dist/commonjs/version.d.ts +2 -0
  29. package/dist/commonjs/version.js +6 -0
  30. package/dist/esm/context.d.ts +45 -0
  31. package/dist/esm/context.js +74 -1
  32. package/dist/esm/core.js +185 -30
  33. package/dist/esm/events.d.ts +17 -6
  34. package/dist/esm/events.js +82 -61
  35. package/dist/esm/genai-content.d.ts +52 -0
  36. package/dist/esm/genai-content.js +137 -0
  37. package/dist/esm/instrument.d.ts +47 -0
  38. package/dist/esm/instrument.js +155 -0
  39. package/dist/esm/integrations/anthropic-content.js +19 -7
  40. package/dist/esm/integrations/anthropic.d.ts +8 -1
  41. package/dist/esm/integrations/anthropic.js +514 -107
  42. package/dist/esm/integrations/index.js +8 -0
  43. package/dist/esm/integrations/langchain-callback.d.ts +182 -27
  44. package/dist/esm/integrations/langchain-callback.js +756 -89
  45. package/dist/esm/integrations/langchain-content.js +1 -1
  46. package/dist/esm/integrations/langchain.d.ts +3 -0
  47. package/dist/esm/integrations/langchain.js +352 -7
  48. package/dist/esm/integrations/openai-content.d.ts +34 -0
  49. package/dist/esm/integrations/openai-content.js +360 -0
  50. package/dist/esm/integrations/openai.d.ts +39 -0
  51. package/dist/esm/integrations/openai.js +296 -0
  52. package/dist/esm/semconv.d.ts +12 -0
  53. package/dist/esm/semconv.js +12 -0
  54. package/dist/esm/truncation.d.ts +29 -0
  55. package/dist/esm/truncation.js +182 -10
  56. package/dist/esm/version.d.ts +2 -0
  57. package/dist/esm/version.js +3 -0
  58. package/package.json +11 -3
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._setChatRequestAttrsForTest = exports._detectProviderForTest = void 0;
3
4
  exports.patch = patch;
4
5
  exports.unpatch = unpatch;
6
+ exports.wrapCreate = wrapCreate;
7
+ exports.wrapStream = wrapStream;
5
8
  exports._wrapCreateForTest = _wrapCreateForTest;
6
9
  exports._wrapStreamForTest = _wrapStreamForTest;
7
10
  exports._setActivePatchCtxForTest = _setActivePatchCtxForTest;
@@ -14,9 +17,11 @@ function childParentContext() {
14
17
  : api_1.context.active();
15
18
  }
16
19
  const core_js_1 = require("../core.js");
20
+ const genai_content_js_1 = require("../genai-content.js");
17
21
  const events_js_1 = require("../events.js");
22
+ const genai_content_js_2 = require("../genai-content.js");
23
+ const instrument_js_1 = require("../instrument.js");
18
24
  const semconv_js_1 = require("../semconv.js");
19
- const truncation_js_1 = require("../truncation.js");
20
25
  const anthropic_content_js_1 = require("./anthropic-content.js");
21
26
  const STRUCT_WRAPPED = Symbol.for("struct.wrapped");
22
27
  const STRUCT_ORIGINAL = Symbol.for("struct.original");
@@ -104,53 +109,96 @@ function restoreMethod(proto, methodName) {
104
109
  proto[methodName] = current[STRUCT_ORIGINAL];
105
110
  }
106
111
  }
112
+ // Exact client class names per platform (prototype chain covers subclasses)
113
+ // and official endpoint host shapes only — a generic cloud host (API-gateway,
114
+ // storage, arbitrary *.amazonaws.com/*.googleapis.com proxies) is NOT
115
+ // platform routing and must stay on the fallback. Parity: python anthropic.py.
116
+ const CLASS_NAME_RULES = [
117
+ // Mantle flavor extends BaseMantleClient, NOT the plain Bedrock base, so it
118
+ // needs its own names; default endpoint bedrock-mantle.{region}.api.aws.
119
+ [
120
+ new Set([
121
+ "AnthropicBedrock",
122
+ "BaseAnthropicBedrock",
123
+ "AnthropicBedrockMantle",
124
+ "BaseMantleClient",
125
+ ]),
126
+ "aws.bedrock",
127
+ ],
128
+ [new Set(["AnthropicVertex", "BaseAnthropicVertex"]), "gcp.vertex_ai"],
129
+ ];
130
+ function isBedrockHost(host) {
131
+ if ((host.startsWith("bedrock-runtime.") ||
132
+ host.startsWith("bedrock-runtime-fips.")) &&
133
+ (host.endsWith(".amazonaws.com") || host.endsWith(".amazonaws.com.cn"))) {
134
+ return true;
135
+ }
136
+ // Bedrock Mantle default endpoint: bedrock-mantle.{region}.api.aws
137
+ return host.startsWith("bedrock-mantle.") && host.endsWith(".api.aws");
138
+ }
139
+ function isVertexHost(host) {
140
+ return (host === "aiplatform.googleapis.com" ||
141
+ host.endsWith("-aiplatform.googleapis.com"));
142
+ }
143
+ const HOST_RULES = [
144
+ [isBedrockHost, "aws.bedrock"],
145
+ [isVertexHost, "gcp.vertex_ai"],
146
+ ];
147
+ function detectProvider(resource) {
148
+ return (0, genai_content_js_1.detectProviderFromResource)(resource, CLASS_NAME_RULES, HOST_RULES, "anthropic");
149
+ }
150
+ /** @internal */
151
+ exports._detectProviderForTest = detectProvider;
152
+ /** @internal */
153
+ const _setChatRequestAttrsForTest = (span, params, sdk, logger, provider) => setChatRequestAttrs(span, params, sdk, logger, provider);
154
+ exports._setChatRequestAttrsForTest = _setChatRequestAttrsForTest;
107
155
  function wrapCreate(original) {
108
156
  return function wrappedCreate(params, opts) {
109
157
  const patchCtx = activePatchCtx.value;
110
158
  if (!patchCtx) {
111
159
  return original.call(this, params, opts);
112
160
  }
161
+ // CLASS RULE: detection => propagation, immediately and on EVERY path
162
+ // (see openai.ts). Guarded; write-once makes repeats harmless.
163
+ const provider = detectProvider(this);
164
+ (0, context_js_1.propagateProviderToParent)(provider);
165
+ if ((0, context_js_1.isGenAiSuppressed)()) {
166
+ // A framework layer owns this chat span — run the call, emit no span.
167
+ return original.call(this, params, opts);
168
+ }
169
+ // Preflight reads touch CALLER-owned `params` — a Proxy / lazy request
170
+ // object can have throwing getters; a throw here would prevent the API
171
+ // call. Degrade: exactly one plain, uninstrumented original.call.
172
+ let streaming = false;
173
+ let model = "unknown";
174
+ try {
175
+ streaming = !!(params && params.stream === true);
176
+ model = params?.model ?? "unknown";
177
+ }
178
+ catch {
179
+ return original.call(this, params, opts);
180
+ }
113
181
  // If params.stream is true, Messages.create delegates to the streaming path.
114
182
  // We still need to instrument the returned stream like we do in messages.stream.
115
- if (params && params.stream === true) {
183
+ if (streaming) {
116
184
  return wrapStream(original).call(this, params, opts);
117
185
  }
118
186
  const { tracer, sdk, logger } = patchCtx;
119
- const internalLogger = sdk.getInternalLogger();
120
- const model = params?.model ?? "unknown";
121
- // Span creation itself can fail (custom tracer, broken context). If it
122
- // does, fall through to calling `original` directly so the user's API
123
- // call always runs. Mirrors the Python `_wrap_create` fall-through.
124
- let span;
125
- let ctx;
126
- (0, core_js_1.safe)(() => {
127
- const parentCtx = childParentContext();
128
- span = tracer.startSpan(`chat ${model}`, { kind: api_1.SpanKind.CLIENT }, parentCtx);
129
- ctx = api_1.trace.setSpan(parentCtx, span);
130
- }, "anthropic.create.start_span", internalLogger);
131
- if (!span || !ctx) {
132
- return original.call(this, params, opts);
133
- }
134
- const liveSpan = span;
135
- const liveCtx = ctx;
136
- // Enter the span's context BEFORE emitting request-side log records so
137
- // `logger.emit({ context: otelContext.active() })` picks up the chat
138
- // span as the log's TraceId / SpanId. Without this the UI can't link
139
- // user/system message logs back to the chat span.
140
- return api_1.context.with(liveCtx, () => {
141
- (0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger), "anthropic.create.set_request_attrs", internalLogger);
142
- const promise = Promise.resolve(original.call(this, params, opts));
143
- return promise.then((result) => {
144
- (0, core_js_1.safe)(() => setChatResponseAttrs(liveSpan, sdk, result, logger), "anthropic.create.set_response_attrs", internalLogger);
145
- (0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.create.set_ok_status", internalLogger);
146
- (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end", internalLogger);
147
- return result;
148
- }, (err) => {
149
- (0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.create.record_error", internalLogger);
150
- (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end_on_error", internalLogger);
151
- // User's API exception always propagates unchanged.
152
- throw err;
153
- });
187
+ // All host-boundary safety lives in the shared instrumentCall harness: the
188
+ // host call runs exactly once, nothing host-controllable wraps it, and every
189
+ // telemetry side-effect degrades via safe(). Log-record→span linkage is
190
+ // carried by the explicit span passed to the emitters, so no
191
+ // `otelContext.with(...)` is needed around the call.
192
+ return (0, instrument_js_1.instrumentCall)(() => original.call(this, params, opts), {
193
+ tracer,
194
+ spanName: `chat ${model}`,
195
+ spanKind: api_1.SpanKind.CLIENT,
196
+ parentContext: childParentContext,
197
+ sitePrefix: "anthropic.create",
198
+ internalLogger: sdk.getInternalLogger(),
199
+ onStart: (span) => setChatRequestAttrs(span, params, sdk, logger, provider),
200
+ onSuccess: (span, result) => setChatResponseAttrs(span, sdk, result, logger, provider),
201
+ onError: (span, err) => recordErrorOnSpan(span, err),
154
202
  });
155
203
  };
156
204
  }
@@ -160,9 +208,26 @@ function wrapStream(original) {
160
208
  if (!patchCtx) {
161
209
  return original.call(this, params, opts);
162
210
  }
211
+ // CLASS RULE: detection => propagation, immediately and on EVERY path —
212
+ // when suppressed (langchain owns the span), the raw client here is the
213
+ // only layer that can detect the real platform for the agent span.
214
+ const streamProvider = detectProvider(this);
215
+ (0, context_js_1.propagateProviderToParent)(streamProvider);
216
+ if ((0, context_js_1.isGenAiSuppressed)()) {
217
+ // A framework layer owns this chat span — run the call, emit no span.
218
+ return original.call(this, params, opts);
219
+ }
163
220
  const { tracer, sdk, logger } = patchCtx;
164
221
  const internalLogger = sdk.getInternalLogger();
165
- const model = params?.model ?? "unknown";
222
+ // Preflight read of CALLER-owned params — a throwing `model` getter must
223
+ // degrade to one plain uninstrumented call, never block the stream.
224
+ let model = "unknown";
225
+ try {
226
+ model = params?.model ?? "unknown";
227
+ }
228
+ catch {
229
+ return original.call(this, params, opts);
230
+ }
166
231
  // Span creation itself can fail. If it does, fall through to calling
167
232
  // `original` directly so the user's stream call always runs. Mirrors
168
233
  // the Python `_wrap_stream` fall-through pattern.
@@ -179,13 +244,6 @@ function wrapStream(original) {
179
244
  const liveSpan = span;
180
245
  const liveCtx = ctx;
181
246
  const storeSnapshot = (0, context_js_1.snapshotStore)();
182
- // Enter the span's context BEFORE request-side log emission so logs
183
- // link back to the chat span via TraceId. Same rationale as wrapCreate.
184
- // @anthropic-ai/sdk's `stream()` returns a MessageStream synchronously.
185
- const stream = api_1.context.with(liveCtx, () => {
186
- (0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger), "anthropic.stream.set_request_attrs", internalLogger);
187
- return original.call(this, params, opts);
188
- });
189
247
  let finalized = false;
190
248
  const finalize = (result, err) => {
191
249
  if (finalized)
@@ -196,7 +254,7 @@ function wrapStream(original) {
196
254
  (0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.stream.record_error", internalLogger);
197
255
  }
198
256
  else if (result) {
199
- (0, core_js_1.safe)(() => api_1.context.with(liveCtx, () => setChatResponseAttrs(liveSpan, sdk, result, logger)), "anthropic.stream.set_response_attrs", internalLogger);
257
+ (0, core_js_1.safe)(() => api_1.context.with(liveCtx, () => setChatResponseAttrs(liveSpan, sdk, result, logger, streamProvider)), "anthropic.stream.set_response_attrs", internalLogger);
200
258
  (0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.stream.set_ok_status", internalLogger);
201
259
  }
202
260
  else {
@@ -205,48 +263,416 @@ function wrapStream(original) {
205
263
  (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.stream.span_end", internalLogger);
206
264
  });
207
265
  };
208
- const ee = stream;
209
- if (ee && typeof ee.on === "function") {
210
- ee.on("finalMessage", (msg) => {
211
- finalize(msg, undefined);
212
- });
213
- ee.on("error", (err) => {
214
- finalize(undefined, err);
215
- });
216
- // If the stream is created and never iterated / finalized, at least
217
- // don't hang on test exit — "end" fires after iteration completes.
218
- ee.on("end", () => {
219
- // Only fires if no finalMessage / error fired first (rare path).
266
+ // Enter the span's context BEFORE request-side log emission so logs
267
+ // link back to the chat span via TraceId. Same rationale as wrapCreate.
268
+ // @anthropic-ai/sdk's `stream()` returns a MessageStream synchronously,
269
+ // but `messages.create({stream:true})` (delegated here from wrapCreate)
270
+ // returns an APIPromise-like thenable resolving to a raw event Stream —
271
+ // both shapes are handled below.
272
+ //
273
+ // Suppress nested instrumentation: the real `Messages.prototype.stream()`
274
+ // (`original` here) internally calls `MessageStream.createMessage`, whose
275
+ // executor synchronously invokes `messages.create({ ...params, stream:
276
+ // true }, ...)` — on the SAME patched `Messages` prototype — before its
277
+ // first `await` yields control. That resolves to our `wrapCreate`
278
+ // wrapper, which would otherwise see `params.stream === true` and
279
+ // re-delegate to a brand-new `wrapStream(original)`, instrumenting the
280
+ // same underlying raw stream a second time (2 chat spans, same
281
+ // gen_ai.response.id). `runWithContext({ suppressGenAi: true }, ...)`
282
+ // scopes an ALS store patch around exactly this synchronous call (plus
283
+ // any promise chain it kicks off internally, since AsyncLocalStorage
284
+ // propagates into `.then` continuations created within the `run()`
285
+ // callback) — `wrapCreate` checks `isGenAiSuppressed()` before its
286
+ // `stream === true` branch and, when suppressed, falls through to
287
+ // calling the TRUE raw `create` directly (no span, no re-wrap). This
288
+ // must NOT leak into user code: the store patch is popped the instant
289
+ // `original.call(...)` returns (it returns the MessageStream
290
+ // synchronously), so by the time control returns to the caller of
291
+ // `.stream()`, suppression is already off again — user code that later
292
+ // iterates the stream, or that itself calls `messages.create`/`.stream`,
293
+ // runs with a clean (unsuppressed) ALS store.
294
+ let stream;
295
+ try {
296
+ (0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger, streamProvider), "anthropic.stream.set_request_attrs", internalLogger);
297
+ // `suppressGenAi` is ALS-based (`als.run`), NOT host-controllable, so it
298
+ // may wrap the original stream call. We deliberately do NOT wrap it in
299
+ // `otelContext.with(liveCtx, ...)`: a customer's global ContextManager
300
+ // could throw there and block the stream (host-fault). Log-record→span
301
+ // linkage is carried by the explicit span passed inside
302
+ // setChatRequestAttrs, not by ambient context.
303
+ stream = (0, context_js_1.runWithContext)({ suppressGenAi: true }, () => original.call(this, params, opts));
304
+ }
305
+ catch (err) {
306
+ // Sync throw from the original call would otherwise leak the span —
307
+ // no stream/promise is ever produced to drive finalize() below.
308
+ finalize(undefined, err);
309
+ throw err;
310
+ }
311
+ // The entire branch-dispatch region below — reading `ee.on`/`.then`/
312
+ // `Symbol.asyncIterator`, registering `.on(...)` listeners, invoking
313
+ // `.then(...)`, and calling `instrumentSyncIterable` — runs SYNCHRONOUSLY
314
+ // on the success path of the customer's `.stream()`/`.create()` call, and
315
+ // was previously unguarded (round-4 only hardened the ASYNC observer
316
+ // callbacks registered inside it, e.g. the onFulfilled/onRejected
317
+ // handlers below). A hostile/broken stream-like object — a throwing
318
+ // `.on`/`.then`/`Symbol.asyncIterator` getter, or a `.on()` registration
319
+ // call that itself throws — would throw synchronously OUT of the
320
+ // customer's call. Wrap the whole dispatch in try/catch so a successful
321
+ // request never becomes a hard failure purely because OUR observer
322
+ // wiring couldn't attach: degrade to no telemetry and hand back the
323
+ // ORIGINAL stream object unchanged.
324
+ try {
325
+ const ee = stream;
326
+ // Emitter path wins first — a real MessageStream (`.on`) is never
327
+ // thenable, but check `.on` before `.then` defensively in case some
328
+ // future stream shape exposes both.
329
+ if (ee && typeof ee.on === "function") {
330
+ ee.on("finalMessage", (msg) => {
331
+ finalize(msg, undefined);
332
+ });
333
+ ee.on("error", (err) => {
334
+ finalize(undefined, err);
335
+ });
336
+ // Abort fires before "end" on an aborted stream — without this
337
+ // listener the "end" handler below finalizes with OK status even
338
+ // though the request was aborted. `finalized` makes the subsequent
339
+ // "end" a no-op.
340
+ ee.on("abort", (err) => {
341
+ finalize(undefined, err ?? new Error("aborted"));
342
+ });
343
+ // If the stream is created and never iterated / finalized, at least
344
+ // don't hang on test exit — "end" fires after iteration completes.
345
+ ee.on("end", () => {
346
+ // Only fires if no finalMessage / error / abort fired first (rare
347
+ // path for a healthy, fully-drained stream).
348
+ finalize(undefined, undefined);
349
+ });
350
+ }
351
+ else if (ee && typeof ee.then === "function") {
352
+ // `create({stream:true})` shape: an APIPromise-like thenable that
353
+ // resolves to a raw event Stream (no `.on`, no synchronous
354
+ // asyncIterator until resolved). Observe the resolution to instrument
355
+ // the resolved Stream BY MUTATION, but return the ORIGINAL thenable —
356
+ // Anthropic's APIPromise exposes methods like `.withResponse()` /
357
+ // `.asResponse()` that a native `Promise.resolve(...).then(...)` would
358
+ // strip, breaking customer code only when instrumentation is enabled.
359
+ // Our observer registers synchronously here, before the caller can
360
+ // await, so instrumentRawStream mutates the same resolved object the
361
+ // caller receives, before they iterate it.
362
+ const thenable = stream;
363
+ const observerChain = thenable.then((raw) => {
364
+ // instrumentRawStream can itself throw at points outside its OWN
365
+ // internal try/catch (e.g. a hostile `Symbol.asyncIterator`
366
+ // getter, read before that internal guard) — this must not
367
+ // escape THIS onFulfilled callback: an uncaught throw here
368
+ // becomes an unhandled rejection on the promise `.then()`
369
+ // returns, even though the customer's original APIPromise
370
+ // resolved successfully — a successful request must never crash
371
+ // the host. Degrade: end the span with no telemetry and swallow.
372
+ try {
373
+ instrumentRawStream(raw);
374
+ }
375
+ catch {
376
+ finalize(undefined, undefined);
377
+ }
378
+ }, (err) => {
379
+ // Observe the rejection to finalize the span, but do NOT rethrow:
380
+ // this observer branch must never surface as an unhandled
381
+ // rejection. The original thenable still rejects to the caller
382
+ // independently.
383
+ finalize(undefined, err);
384
+ });
385
+ // Belt-and-suspenders: both branches above are already guarded to
386
+ // never throw/reject, so this derived promise should never reject —
387
+ // but chain a no-op `.catch` anyway so this internal observer chain
388
+ // can NEVER become an unhandled-rejection source, even under future
389
+ // edits to the branches above.
390
+ observerChain.catch?.(() => { });
391
+ return stream;
392
+ }
393
+ else if (ee && typeof ee[Symbol.asyncIterator] === "function") {
394
+ // Wrap the async iterator so each .next() runs inside the captured
395
+ // ALS store.
396
+ instrumentSyncIterable(ee);
397
+ }
398
+ return stream;
399
+ }
400
+ catch {
401
+ finalize(undefined, undefined);
402
+ return stream;
403
+ }
404
+ function instrumentRawStream(raw) {
405
+ const it = raw;
406
+ if (!it || typeof it[Symbol.asyncIterator] !== "function") {
407
+ finalize(undefined, undefined); // not iterable — end the span rather than leak it
408
+ return raw;
409
+ }
410
+ // A raw stream that is awaited but never iterated would otherwise leak
411
+ // its span — finalize() only fires through the iterator hooks below.
412
+ // Anthropic's Stream carries an AbortController; close the span if the
413
+ // request is aborted before/without iteration. Residual (documented,
414
+ // accepted): a stream awaited, never iterated, AND never aborted stays
415
+ // open until process exit — no lifecycle signal exists to close it.
416
+ //
417
+ // This whole section reads a `controller` getter and calls
418
+ // `addEventListener` on whatever object the SDK (or a hostile/broken
419
+ // caller-supplied stand-in) hands us — both are fallible and, unlike
420
+ // the `Symbol.asyncIterator` assignment below, were previously
421
+ // unguarded. Wrap it so a throwing getter degrades to "no abort-driven
422
+ // finalize wiring" rather than aborting instrumentRawStream entirely
423
+ // (which would also skip the Symbol.asyncIterator instrumentation
424
+ // below and lose the whole stream's telemetry, not just the abort
425
+ // hook). The caller (the observer's onFulfilled, or the synchronous
426
+ // `.on()`/asyncIterator paths in wrapStream) also guards its own call
427
+ // to instrumentRawStream, so this is defense-in-depth, not the only
428
+ // guard.
429
+ try {
430
+ const ctrl = raw.controller;
431
+ const sig = ctrl?.signal;
432
+ if (sig) {
433
+ if (sig.aborted) {
434
+ finalize(undefined, sig.reason ?? new Error("aborted"));
435
+ }
436
+ else {
437
+ sig.addEventListener("abort", () => finalize(undefined, sig.reason ?? new Error("aborted")), { once: true });
438
+ }
439
+ }
440
+ }
441
+ catch {
442
+ // Swallow — no abort-driven finalize wiring for this stream, but
443
+ // fall through to installing the Symbol.asyncIterator override
444
+ // below so normal iteration still drives accumulate()/finalize().
445
+ }
446
+ const acc = {};
447
+ const originalIter = it[Symbol.asyncIterator].bind(it);
448
+ try {
449
+ it[Symbol.asyncIterator] = () => {
450
+ const inner = originalIter();
451
+ return {
452
+ next: () => (0, context_js_1.runWithStore)(storeSnapshot, () => inner.next().then((res) => {
453
+ // accumulate()/accToResponseMessage() inspect provider-owned
454
+ // values (a proxy getter can throw; accToResponseMessage
455
+ // JSON.parses input_json_delta, which may be malformed). A
456
+ // successfully yielded item must NEVER become a rejected
457
+ // next(): on any instrumentation failure, degrade telemetry
458
+ // (finalize with no result) and return the original res.
459
+ try {
460
+ if (!res.done)
461
+ accumulate(acc, res.value);
462
+ else
463
+ finalize(accToResponseMessage(acc), undefined);
464
+ }
465
+ catch {
466
+ finalize(undefined, undefined);
467
+ }
468
+ return res;
469
+ }, (err) => {
470
+ finalize(undefined, err);
471
+ throw err;
472
+ })),
473
+ return: (v) => (0, context_js_1.runWithStore)(storeSnapshot, () => {
474
+ // Settle the inner iterator's cleanup BEFORE finalizing, so a
475
+ // cleanup rejection is recorded as ERROR rather than the span
476
+ // being closed OK while the caller receives a rejection.
477
+ const ret = inner.return
478
+ ? inner.return(v)
479
+ : Promise.resolve({ value: v, done: true });
480
+ return Promise.resolve(ret).then((res) => {
481
+ finalize(accToResponseMessage(acc), undefined); // early break — close OK
482
+ return res;
483
+ }, (err) => {
484
+ finalize(undefined, err); // cleanup rejected — record ERROR
485
+ throw err; // propagate the original rejection unchanged
486
+ });
487
+ }),
488
+ throw: (e) => (0, context_js_1.runWithStore)(storeSnapshot, () => {
489
+ // The thrown value IS the error condition, so finalize ERROR
490
+ // with it regardless of the inner iterator's cleanup outcome.
491
+ finalize(undefined, e);
492
+ return inner.throw ? inner.throw(e) : Promise.reject(e);
493
+ }),
494
+ };
495
+ };
496
+ }
497
+ catch {
498
+ // The resolved stream object is frozen/sealed or the property is
499
+ // non-writable — assigning Symbol.asyncIterator threw. A successful
500
+ // request must never become a rejected promise because OUR
501
+ // instrumentation couldn't attach; degrade gracefully (end the span
502
+ // with no telemetry) and hand back the untouched object. No Proxy
503
+ // fallback — that would change the stream's identity, which is its
504
+ // own hazard.
220
505
  finalize(undefined, undefined);
221
- });
506
+ }
507
+ return raw;
222
508
  }
223
- // Wrap the async iterator so each .next() runs inside the captured ALS store.
224
- if (ee && typeof ee[Symbol.asyncIterator] === "function") {
225
- const originalIter = ee[Symbol.asyncIterator].bind(ee);
226
- ee[Symbol.asyncIterator] = () => {
227
- const inner = originalIter();
228
- const wrapped = {
229
- next() {
230
- return (0, context_js_1.runWithStore)(storeSnapshot, () => inner.next());
231
- },
232
- return(value) {
233
- return (0, context_js_1.runWithStore)(storeSnapshot, () => inner.return ? inner.return(value) : Promise.resolve({ value, done: true }));
234
- },
235
- throw(err) {
236
- return (0, context_js_1.runWithStore)(storeSnapshot, () => inner.throw
237
- ? inner.throw(err)
238
- : Promise.reject(err));
239
- },
509
+ function instrumentSyncIterable(target) {
510
+ const originalIter = target[Symbol.asyncIterator].bind(target);
511
+ try {
512
+ target[Symbol.asyncIterator] = () => {
513
+ const inner = originalIter();
514
+ const wrapped = {
515
+ next() {
516
+ return (0, context_js_1.runWithStore)(storeSnapshot, () => inner.next());
517
+ },
518
+ return(value) {
519
+ return (0, context_js_1.runWithStore)(storeSnapshot, () => inner.return
520
+ ? inner.return(value)
521
+ : Promise.resolve({ value, done: true }));
522
+ },
523
+ throw(err) {
524
+ return (0, context_js_1.runWithStore)(storeSnapshot, () => inner.throw ? inner.throw(err) : Promise.reject(err));
525
+ },
526
+ };
527
+ return wrapped;
240
528
  };
241
- return wrapped;
529
+ }
530
+ catch {
531
+ // Same hazard class as instrumentRawStream above: the target is
532
+ // frozen/sealed or the property is non-writable. This call happens
533
+ // synchronously inside wrapStream itself (not inside a promise
534
+ // callback), so an uncaught throw here would surface as a
535
+ // SYNCHRONOUS exception out of the user's `.stream()`/`.create()`
536
+ // call — turning a successful request into a hard failure. Degrade
537
+ // gracefully instead: end the span with no telemetry and leave the
538
+ // iterable completely untouched (the failed assignment never took
539
+ // effect).
540
+ finalize(undefined, undefined);
541
+ }
542
+ }
543
+ function accumulate(acc, ev) {
544
+ const e = ev;
545
+ if (e?.type === "message_start" && e.message) {
546
+ acc.id = e.message.id;
547
+ acc.model = e.message.model;
548
+ acc.inputTokens = e.message.usage?.input_tokens;
549
+ // Cached `create({stream:true})` requests carry cache token counts
550
+ // on message_start's usage (never on message_delta's) — without
551
+ // these, the synthesized ResponseMessage under-reports input usage
552
+ // (setChatResponseAttrs adds them into the total) and omits both
553
+ // cache attributes entirely, unlike the non-streaming and
554
+ // MessageStream (`.on("finalMessage")`) paths, which pass the SDK's
555
+ // own parsed usage straight through.
556
+ acc.cacheReadTokens = e.message.usage?.cache_read_input_tokens;
557
+ acc.cacheCreationTokens = e.message.usage?.cache_creation_input_tokens;
558
+ }
559
+ else if (e?.type === "content_block_start" && typeof e.index === "number") {
560
+ // Assemble content blocks so the synthesized response carries
561
+ // `content` — without it, setChatResponseAttrs's `if (response.content)`
562
+ // block never runs on this raw-stream path, dropping
563
+ // gen_ai.output.messages, the gen_ai.choice event, and (critically)
564
+ // recordPendingToolCalls, so a streamed tool_use never links to its
565
+ // execute_tool span. Mirrors MessageStream's finalMessage assembly,
566
+ // minimally: text blocks and tool_use blocks only.
567
+ const blocks = (acc.blocks ??= {});
568
+ const cb = e.content_block;
569
+ if (cb?.type === "tool_use") {
570
+ blocks[e.index] = {
571
+ type: "tool_use",
572
+ id: cb.id,
573
+ name: cb.name,
574
+ jsonBuf: "",
575
+ };
576
+ }
577
+ else if (cb?.type === "text") {
578
+ blocks[e.index] = { type: "text", text: cb.text ?? "" };
579
+ }
580
+ else if (cb?.type) {
581
+ blocks[e.index] = { type: cb.type };
582
+ }
583
+ }
584
+ else if (e?.type === "content_block_delta" && typeof e.index === "number") {
585
+ const blocks = (acc.blocks ??= {});
586
+ const b = blocks[e.index];
587
+ if (!b)
588
+ return;
589
+ if (e.delta?.type === "text_delta" && typeof e.delta.text === "string") {
590
+ b.text = (b.text ?? "") + e.delta.text;
591
+ }
592
+ else if (e.delta?.type === "input_json_delta" &&
593
+ typeof e.delta.partial_json === "string") {
594
+ b.jsonBuf = (b.jsonBuf ?? "") + e.delta.partial_json;
595
+ }
596
+ }
597
+ else if (e?.type === "content_block_stop" && typeof e.index === "number") {
598
+ const blocks = (acc.blocks ??= {});
599
+ const b = blocks[e.index];
600
+ if (b?.type === "tool_use") {
601
+ // Parse the accumulated partial_json into the tool_use input; fall
602
+ // back to the raw string if the model streamed malformed JSON (the
603
+ // block still links via id/name, which is what tool-call linkage
604
+ // needs).
605
+ try {
606
+ b.input = b.jsonBuf ? JSON.parse(b.jsonBuf) : {};
607
+ }
608
+ catch {
609
+ b.input = b.jsonBuf;
610
+ }
611
+ }
612
+ }
613
+ else if (e?.type === "message_delta") {
614
+ if (e.delta?.stop_reason)
615
+ acc.stopReason = e.delta.stop_reason;
616
+ if (e.usage?.output_tokens !== undefined) {
617
+ acc.outputTokens = e.usage.output_tokens;
618
+ }
619
+ }
620
+ }
621
+ // NOTE (parity, intentional divergence — self-audit round 5, item G):
622
+ // this synthesizes a FULL ResponseMessage (content, usage, stop_reason,
623
+ // id, model) from the raw SSE event stream, matching what the
624
+ // non-streaming `create()` path and `MessageStream`'s `finalMessage`
625
+ // event already carry. struct-sdk-python's `create(stream=True)` raw
626
+ // path does NOT do this reconstruction yet and emits none of these
627
+ // response-side attributes for that one call shape. This is TS being
628
+ // MORE complete than Python here, not a TS bug — do not remove it to
629
+ // "match" Python. Bringing Python's raw-stream path up to parity is a
630
+ // separate, independent follow-up.
631
+ function accToResponseMessage(acc) {
632
+ const blocks = acc.blocks;
633
+ if (acc.id === undefined && acc.inputTokens === undefined && !blocks) {
634
+ return undefined;
635
+ }
636
+ // Rebuild content in ascending block index, shaped like the SDK's own
637
+ // parsed message content so setChatResponseAttrs / iterToolUses read it
638
+ // identically to the non-streaming path.
639
+ let content;
640
+ if (blocks) {
641
+ content = Object.keys(blocks)
642
+ .map((k) => Number(k))
643
+ .sort((a, b) => a - b)
644
+ .map((i) => {
645
+ const b = blocks[i];
646
+ if (b.type === "tool_use") {
647
+ return { type: "tool_use", id: b.id, name: b.name, input: b.input ?? {} };
648
+ }
649
+ if (b.type === "text") {
650
+ return { type: "text", text: b.text ?? "" };
651
+ }
652
+ return { type: b.type };
653
+ });
654
+ }
655
+ return {
656
+ id: acc.id,
657
+ model: acc.model,
658
+ stop_reason: acc.stopReason,
659
+ content: content,
660
+ usage: {
661
+ input_tokens: acc.inputTokens,
662
+ output_tokens: acc.outputTokens,
663
+ // Raw fields only — setChatResponseAttrs remains the single place
664
+ // that adds these into the reported total, matching the
665
+ // non-streaming/MessageStream contract exactly.
666
+ cache_read_input_tokens: acc.cacheReadTokens,
667
+ cache_creation_input_tokens: acc.cacheCreationTokens,
668
+ },
242
669
  };
243
670
  }
244
- return stream;
245
671
  };
246
672
  }
247
- function setChatRequestAttrs(span, params, sdk, logger) {
673
+ function setChatRequestAttrs(span, params, sdk, logger, provider = "anthropic") {
248
674
  span.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "chat");
249
- span.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, "anthropic");
675
+ span.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, provider);
250
676
  const model = params?.model ?? "unknown";
251
677
  span.setAttribute(semconv_js_1.GEN_AI.REQUEST_MODEL, model);
252
678
  const sessionId = (0, context_js_1.getSessionId)();
@@ -270,10 +696,15 @@ function setChatRequestAttrs(span, params, sdk, logger) {
270
696
  }
271
697
  if (Array.isArray(params?.messages)) {
272
698
  span.setAttribute(semconv_js_1.STRUCT.INPUT_MESSAGE_COUNT, params.messages.length);
273
- propagateUserPromptToParent(params.messages);
699
+ // Parent-prompt preview is CONTENT — never emit it in ContentCaptureMode.None.
700
+ // Deliberately kept in EventOnly (the default): shipped behavior in both
701
+ // SDKs and what the waterfall UI reads.
702
+ if (sdk.captureContent) {
703
+ (0, genai_content_js_2.propagateUserPromptToParent)((0, anthropic_content_js_1.lastUserMessageParts)(params.messages));
704
+ }
274
705
  }
275
706
  if (sdk.emitEvents && logger && Array.isArray(params?.messages)) {
276
- (0, events_js_1.emitAnthropicMessageEvents)(logger, params.messages, params.system);
707
+ (0, events_js_1.emitAnthropicMessageEvents)(logger, params.messages, params.system, span, provider);
277
708
  }
278
709
  if (sdk.emitSpanContent) {
279
710
  if (Array.isArray(params?.messages)) {
@@ -287,7 +718,7 @@ function setChatRequestAttrs(span, params, sdk, logger) {
287
718
  }
288
719
  }
289
720
  }
290
- function setChatResponseAttrs(span, sdk, response, logger) {
721
+ function setChatResponseAttrs(span, sdk, response, logger, provider = "anthropic") {
291
722
  const usage = response.usage;
292
723
  if (usage) {
293
724
  const inputTokens = usage.input_tokens ?? 0;
@@ -317,7 +748,7 @@ function setChatResponseAttrs(span, sdk, response, logger) {
317
748
  // Always runs — populates pending queue regardless of content-capture mode.
318
749
  recordPendingToolCalls(response.content);
319
750
  if (sdk.emitEvents && logger) {
320
- (0, events_js_1.emitAnthropicChoiceEvent)(logger, response.content, response.stop_reason);
751
+ (0, events_js_1.emitAnthropicChoiceEvent)(logger, response.content, response.stop_reason, span, provider);
321
752
  }
322
753
  if (sdk.emitSpanContent) {
323
754
  span.setAttribute(semconv_js_1.GEN_AI.OUTPUT_MESSAGES, (0, anthropic_content_js_1.toOutputMessages)(response.content, response.stop_reason));
@@ -333,26 +764,6 @@ function recordPendingToolCalls(contentBlocks) {
333
764
  (0, context_js_1.ensurePendingToolCallsSlot)();
334
765
  (0, context_js_1.pushPendingToolCalls)(pairs);
335
766
  }
336
- function propagateUserPromptToParent(messages) {
337
- try {
338
- const agentSpan = (0, context_js_1.getAgentSpan)();
339
- if (!agentSpan)
340
- return;
341
- // The SDK span type doesn't expose `attributes` — we use a brand check on
342
- // the SDK's ReadableSpan-like shape.
343
- const agentAttrs = agentSpan
344
- .attributes;
345
- if (agentAttrs && agentAttrs[semconv_js_1.GEN_AI.INPUT_MESSAGES])
346
- return;
347
- const parts = (0, anthropic_content_js_1.lastUserMessageParts)(messages);
348
- if (!parts)
349
- return;
350
- agentSpan.setAttribute(semconv_js_1.GEN_AI.INPUT_MESSAGES, (0, truncation_js_1.truncateAndSerialize)([{ role: "user", parts }]));
351
- }
352
- catch {
353
- /* never fail the application for telemetry */
354
- }
355
- }
356
767
  function recordErrorOnSpan(span, err) {
357
768
  const errorType = err instanceof Error ? err.constructor.name : typeof err;
358
769
  const message = err instanceof Error ? err.message : String(err);