@telemetry-dev/anthropic 0.1.0 → 0.1.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 (3) hide show
  1. package/dist/index.mjs +59 -52
  2. package/package.json +4 -4
  3. package/src/index.ts +174 -214
package/dist/index.mjs CHANGED
@@ -6,22 +6,27 @@ const WRAPPED = Symbol("telemetry.dev.anthropic.wrapped");
6
6
  const WRAPPED_ORIGINAL = Symbol("telemetry.dev.anthropic.original");
7
7
  const wrappedClients = /* @__PURE__ */ new WeakSet();
8
8
  function asRecord(value) {
9
- return value !== null && typeof value === "object" ? value : void 0;
9
+ if (value === null || value === void 0 || value instanceof Function) return void 0;
10
+ return Object(value) === value ? value : void 0;
10
11
  }
11
12
  function readString(value) {
12
- return typeof value === "string" ? value : void 0;
13
+ return String(value) === value ? value : void 0;
13
14
  }
14
15
  function readNumber(value) {
15
- return typeof value === "number" ? value : void 0;
16
+ return Number(value) === value ? value : void 0;
16
17
  }
17
18
  function compactUsage(usage) {
18
19
  if (!usage) return void 0;
19
20
  return Object.values(usage).some((value) => value !== void 0) ? usage : void 0;
20
21
  }
21
22
  function stopSequences(value) {
22
- if (typeof value === "string") return [value];
23
+ const single = readString(value);
24
+ if (single !== void 0) return [single];
23
25
  if (!Array.isArray(value)) return void 0;
24
- const strings = value.filter((item) => typeof item === "string");
26
+ const strings = value.flatMap((item) => {
27
+ const string = readString(item);
28
+ return string === void 0 ? [] : [string];
29
+ });
25
30
  return strings.length > 0 ? strings : void 0;
26
31
  }
27
32
  function messagesRequest(body) {
@@ -47,14 +52,14 @@ function messagesRequest(body) {
47
52
  };
48
53
  }
49
54
  function messagesUsage(usage) {
50
- const u = asRecord(usage);
51
- const outputDetails = asRecord(u?.output_tokens_details);
55
+ const record = asRecord(usage);
56
+ const details = asRecord(record?.output_tokens_details);
52
57
  return compactUsage({
53
- inputTokens: readNumber(u?.input_tokens),
54
- outputTokens: readNumber(u?.output_tokens),
55
- cacheReadInputTokens: readNumber(u?.cache_read_input_tokens),
56
- cacheCreationInputTokens: readNumber(u?.cache_creation_input_tokens),
57
- reasoningOutputTokens: readNumber(outputDetails?.thinking_tokens)
58
+ inputTokens: readNumber(record?.input_tokens),
59
+ outputTokens: readNumber(record?.output_tokens),
60
+ cacheReadInputTokens: readNumber(record?.cache_read_input_tokens),
61
+ cacheCreationInputTokens: readNumber(record?.cache_creation_input_tokens),
62
+ reasoningOutputTokens: readNumber(details?.thinking_tokens)
58
63
  });
59
64
  }
60
65
  function mergeUsage(current, incoming) {
@@ -68,23 +73,23 @@ function mergeUsage(current, incoming) {
68
73
  });
69
74
  }
70
75
  function messagesResponse(response) {
71
- const r = asRecord(response) ?? {};
72
- const role = readString(r.role) ?? "assistant";
76
+ const record = asRecord(response) ?? {};
77
+ const role = readString(record.role) ?? "assistant";
73
78
  return {
74
- responseModel: readString(r.model),
75
- responseId: readString(r.id),
76
- finishReason: readString(r.stop_reason),
77
- output: r.content !== void 0 ? [{
79
+ responseModel: readString(record.model),
80
+ responseId: readString(record.id),
81
+ finishReason: readString(record.stop_reason),
82
+ output: record.content !== void 0 ? [{
78
83
  role,
79
- content: r.content
84
+ content: record.content
80
85
  }] : void 0,
81
- usage: messagesUsage(r.usage)
86
+ usage: messagesUsage(record.usage)
82
87
  };
83
88
  }
84
89
  function constructorName(value) {
85
- if (value === null || typeof value !== "object" && typeof value !== "function") return;
86
- const ctor = value.constructor;
87
- return typeof ctor === "function" ? ctor.name : void 0;
90
+ if (value === null || value === void 0) return void 0;
91
+ const ctor = Object(value).constructor;
92
+ return ctor instanceof Function ? ctor.name : void 0;
88
93
  }
89
94
  function providerForClient(client) {
90
95
  switch (constructorName(client)) {
@@ -99,7 +104,7 @@ function providerForResource(resource) {
99
104
  return providerForClient(asRecord(resource)?._client);
100
105
  }
101
106
  function isWrapped(fn) {
102
- if (typeof fn !== "function") return false;
107
+ if (!(fn instanceof Function)) return false;
103
108
  return fn[WRAPPED] === true;
104
109
  }
105
110
  function markWrapped(fn, original) {
@@ -116,11 +121,10 @@ function endOnce(span) {
116
121
  };
117
122
  }
118
123
  function mapRawResponse(response) {
124
+ const fields = { attributes: { "http.response.status_code": response.status } };
119
125
  const requestId = response.headers.get("request-id");
120
- return {
121
- ...requestId ? { responseId: requestId } : {},
122
- attributes: { "http.response.status_code": response.status }
123
- };
126
+ if (requestId) fields.responseId = requestId;
127
+ return fields;
124
128
  }
125
129
  function finalizeParsedValue(value, ctx) {
126
130
  if (ctx.streaming) {
@@ -139,14 +143,14 @@ function makeTracedPromise(inner, ctx) {
139
143
  const originalAsResponse = source.asResponse?.bind(source);
140
144
  const originalThenUnwrap = source._thenUnwrap?.bind(source);
141
145
  const handleError = (error) => {
142
- ctx.end({ error });
146
+ ctx.end({ error: error instanceof Error ? error : new Error(String(error)) });
143
147
  throw error;
144
148
  };
145
149
  const onParsed = (value) => finalizeParsedValue(value, ctx);
146
150
  const parsed = () => originalThen(onParsed, handleError);
147
- const traced = constructorName(source) === "APIPromise" ? source : new Promise((resolve, reject) => {
148
- parsed().then(resolve, reject);
149
- });
151
+ let traced;
152
+ if (constructorName(source) === "APIPromise") traced = source;
153
+ else traced = new Promise((resolve, reject) => parsed().then(resolve, reject));
150
154
  if (traced === source) {
151
155
  traced.then = ((onfulfilled, onrejected) => parsed().then(onfulfilled, onrejected));
152
156
  traced.catch = ((onrejected) => parsed().catch(onrejected));
@@ -169,7 +173,9 @@ function makeTracedPromise(inner, ctx) {
169
173
  };
170
174
  traced._thenUnwrap = (transform) => {
171
175
  if (!originalThenUnwrap) return rejectMissing("_thenUnwrap");
172
- return makeTracedPromise(originalThenUnwrap(transform), ctx);
176
+ return makeTracedPromise(originalThenUnwrap((data, ...args) => {
177
+ return transform(data, ...args);
178
+ }), ctx);
173
179
  };
174
180
  return traced;
175
181
  }
@@ -301,28 +307,30 @@ function recordContentBlockDelta(event, state) {
301
307
  if (deltaType === "signature_delta" && delta.signature !== void 0) data.signature = delta.signature;
302
308
  }
303
309
  function recordStreamEvent(event, state) {
304
- const e = asRecord(event) ?? {};
305
- const type = readString(e.type);
310
+ const record = asRecord(event) ?? {};
311
+ const type = readString(record.type);
306
312
  const fields = {};
307
313
  if (type === "message_start") {
308
- const message = asRecord(e.message) ?? {};
314
+ const message = asRecord(record.message) ?? {};
309
315
  fields.responseId = readString(message.id);
310
316
  fields.responseModel = readString(message.model);
311
317
  state.usage = mergeUsage(state.usage, messagesUsage(message.usage));
312
318
  }
313
- if (type === "content_block_start") recordContentBlockStart(e, state);
314
- if (type === "content_block_delta") recordContentBlockDelta(e, state);
319
+ if (type === "content_block_start") recordContentBlockStart(record, state);
320
+ if (type === "content_block_delta") recordContentBlockDelta(record, state);
315
321
  if (type === "message_delta") {
316
- state.finishReason = readString((asRecord(e.delta) ?? {}).stop_reason) ?? state.finishReason;
317
- state.usage = mergeUsage(state.usage, messagesUsage(e.usage));
322
+ state.finishReason = readString((asRecord(record.delta) ?? {}).stop_reason) ?? state.finishReason;
323
+ state.usage = mergeUsage(state.usage, messagesUsage(record.usage));
318
324
  }
319
325
  return fields;
320
326
  }
321
327
  function isStreamLike(value) {
322
- if (value === null || typeof value !== "object") return false;
323
- const stream = value;
324
- const signal = asRecord(stream.controller)?.signal;
325
- return typeof stream[Symbol.asyncIterator] === "function" && signal !== void 0 && typeof asRecord(signal)?.addEventListener === "function";
328
+ const stream = asRecord(value);
329
+ if (!stream) return false;
330
+ const controller = asRecord(stream.controller);
331
+ const signal = asRecord(controller?.signal);
332
+ if (!controller || !(controller.abort instanceof Function) || !signal || typeof signal.aborted !== "boolean" || !(signal.addEventListener instanceof Function)) return false;
333
+ return stream[Symbol.asyncIterator] instanceof Function;
326
334
  }
327
335
  function createObservedMessagesStream(source, span, startedAt, end) {
328
336
  const state = { blocks: /* @__PURE__ */ new Map() };
@@ -332,7 +340,7 @@ function createObservedMessagesStream(source, span, startedAt, end) {
332
340
  if (consumed) return;
333
341
  end({
334
342
  ...streamPartialFields(state),
335
- error: signal.reason ?? /* @__PURE__ */ new Error("Request aborted")
343
+ error: signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Request aborted")
336
344
  });
337
345
  };
338
346
  if (signal.aborted) endAborted();
@@ -350,10 +358,9 @@ function createObservedMessagesStream(source, span, startedAt, end) {
350
358
  terminalError = error;
351
359
  throw error;
352
360
  } finally {
353
- end({
354
- ...streamPartialFields(state),
355
- ...terminalError !== void 0 ? { error: terminalError } : {}
356
- });
361
+ const fields = streamPartialFields(state);
362
+ if (terminalError !== void 0) fields.error = terminalError instanceof Error ? terminalError : new Error(String(terminalError));
363
+ end(fields);
357
364
  }
358
365
  }
359
366
  return new Stream(() => iterator(), source.controller);
@@ -383,7 +390,7 @@ function wrapCreate(original, mapRequest, mapResponse, provider) {
383
390
  mapResponse
384
391
  });
385
392
  } catch (error) {
386
- end({ error });
393
+ end({ error: error instanceof Error ? error : new Error(String(error)) });
387
394
  throw error;
388
395
  }
389
396
  };
@@ -393,13 +400,13 @@ function patchInstanceMethod(target, key, request, response, provider) {
393
400
  const current = target[key];
394
401
  if (isWrapped(current) && Object.hasOwn(target, key)) return;
395
402
  const original = isWrapped(current) ? current[WRAPPED_ORIGINAL] : current;
396
- if (typeof original !== "function") return;
403
+ if (!(original instanceof Function)) return;
397
404
  target[key] = wrapCreate(original.bind(target), request, response, () => provider);
398
405
  }
399
406
  function patchPrototype(klass, key, request, response) {
400
407
  const prototype = klass.prototype;
401
408
  const original = prototype[key];
402
- if (typeof original !== "function" || isWrapped(original)) return () => {};
409
+ if (!(original instanceof Function) || isWrapped(original)) return () => {};
403
410
  prototype[key] = wrapCreate(original, request, response, providerForResource);
404
411
  return () => {
405
412
  prototype[key] = original;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/anthropic",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Anthropic SDK instrumentation for telemetry.dev: wraps the Messages API and emits OpenTelemetry GenAI spans.",
5
5
  "keywords": [
6
6
  "anthropic",
@@ -40,15 +40,15 @@
40
40
  "@opentelemetry/sdk-logs": "^0.218.0",
41
41
  "@opentelemetry/sdk-metrics": "^2.7.1",
42
42
  "@opentelemetry/sdk-trace-base": "^2.7.1",
43
+ "@telemetry-dev/sdk": "0.1.2",
43
44
  "@types/node": "^24",
44
45
  "typescript": "^5",
45
46
  "vite-plus": "0.1.20",
46
- "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20",
47
- "@telemetry-dev/sdk": "0.1.0"
47
+ "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@anthropic-ai/sdk": ">=0.110.0 <1",
51
- "@telemetry-dev/sdk": "^0.1.0"
51
+ "@telemetry-dev/sdk": "^0.1.2"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "pnpm exec vp pack",
package/src/index.ts CHANGED
@@ -13,64 +13,70 @@ const WRAPPED = Symbol("telemetry.dev.anthropic.wrapped");
13
13
  const WRAPPED_ORIGINAL = Symbol("telemetry.dev.anthropic.original");
14
14
  const wrappedClients = new WeakSet<object>();
15
15
 
16
- type UnknownRecord = Record<string, unknown>;
17
- type WrappedFunction = ((...args: unknown[]) => unknown) & {
16
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
17
+ interface OpaqueValue {}
18
+ type Value = string | number | boolean | null | undefined | OpaqueValue;
19
+ interface ValueRecord {
20
+ [key: string]: JsonValue | undefined;
21
+ [key: symbol]: JsonValue | undefined;
22
+ }
23
+ interface CallableRecord {
24
+ [key: string]: JsonValue | AnyFunction | undefined;
25
+ }
26
+ type AnyFunction = (...args: never[]) => OpaqueValue;
27
+ type WrappedFunction = AnyFunction & {
18
28
  [WRAPPED]?: true;
19
- [WRAPPED_ORIGINAL]?: (...args: never[]) => unknown;
29
+ [WRAPPED_ORIGINAL]?: AnyFunction;
20
30
  };
21
- type ProviderResolver = (resource: unknown) => string;
31
+ type ProviderResolver = (resource: Value) => string;
22
32
 
23
33
  interface AnthropicClient {
24
34
  messages: object;
25
35
  }
26
-
27
36
  interface RequestMapping {
28
37
  name: string;
29
38
  fields: StartSpanOptions;
30
39
  }
31
-
32
40
  interface WrappedResponse<T> {
33
41
  data: T;
34
42
  response: Response;
35
43
  request_id: string | null;
36
44
  }
37
-
38
45
  interface ToolBlockState {
39
- data: UnknownRecord;
46
+ data: ValueRecord;
40
47
  inputJson: string;
41
48
  }
42
-
43
49
  interface StreamState {
44
- blocks: Map<number, UnknownRecord | ToolBlockState>;
50
+ blocks: Map<number, ValueRecord | ToolBlockState>;
45
51
  usage?: SpanFields["usage"];
46
52
  finishReason?: string;
47
53
  }
48
54
 
49
- function asRecord(value: unknown): UnknownRecord | undefined {
50
- return value !== null && typeof value === "object" ? (value as UnknownRecord) : undefined;
55
+ function asRecord<T>(value: T): (T & ValueRecord) | undefined {
56
+ if (value === null || value === undefined || value instanceof Function) return undefined;
57
+ return Object(value) === value ? (value as T & ValueRecord) : undefined;
51
58
  }
52
-
53
- function readString(value: unknown): string | undefined {
54
- return typeof value === "string" ? value : undefined;
59
+ function readString<T>(value: T): string | undefined {
60
+ return String(value) === value ? (value as string) : undefined;
55
61
  }
56
-
57
- function readNumber(value: unknown): number | undefined {
58
- return typeof value === "number" ? value : undefined;
62
+ function readNumber<T>(value: T): number | undefined {
63
+ return Number(value) === value ? (value as number) : undefined;
59
64
  }
60
-
61
65
  function compactUsage(usage: SpanFields["usage"]): SpanFields["usage"] {
62
66
  if (!usage) return undefined;
63
67
  return Object.values(usage).some((value) => value !== undefined) ? usage : undefined;
64
68
  }
65
-
66
- function stopSequences(value: unknown): string[] | undefined {
67
- if (typeof value === "string") return [value];
69
+ function stopSequences<T>(value: T): string[] | undefined {
70
+ const single = readString(value);
71
+ if (single !== undefined) return [single];
68
72
  if (!Array.isArray(value)) return undefined;
69
- const strings = value.filter((item): item is string => typeof item === "string");
73
+ const strings = value.flatMap((item) => {
74
+ const string = readString(item);
75
+ return string === undefined ? [] : [string];
76
+ });
70
77
  return strings.length > 0 ? strings : undefined;
71
78
  }
72
-
73
- function messagesRequest(body: UnknownRecord): RequestMapping {
79
+ function messagesRequest(body: ValueRecord): RequestMapping {
74
80
  const model = readString(body.model);
75
81
  const input =
76
82
  body.tools !== undefined || body.tool_choice !== undefined
@@ -91,19 +97,17 @@ function messagesRequest(body: UnknownRecord): RequestMapping {
91
97
  },
92
98
  };
93
99
  }
94
-
95
- function messagesUsage(usage: unknown): SpanFields["usage"] {
96
- const u = asRecord(usage);
97
- const outputDetails = asRecord(u?.output_tokens_details);
100
+ function messagesUsage<T>(usage: T): SpanFields["usage"] {
101
+ const record = asRecord(usage);
102
+ const details = asRecord(record?.output_tokens_details);
98
103
  return compactUsage({
99
- inputTokens: readNumber(u?.input_tokens),
100
- outputTokens: readNumber(u?.output_tokens),
101
- cacheReadInputTokens: readNumber(u?.cache_read_input_tokens),
102
- cacheCreationInputTokens: readNumber(u?.cache_creation_input_tokens),
103
- reasoningOutputTokens: readNumber(outputDetails?.thinking_tokens),
104
+ inputTokens: readNumber(record?.input_tokens),
105
+ outputTokens: readNumber(record?.output_tokens),
106
+ cacheReadInputTokens: readNumber(record?.cache_read_input_tokens),
107
+ cacheCreationInputTokens: readNumber(record?.cache_creation_input_tokens),
108
+ reasoningOutputTokens: readNumber(details?.thinking_tokens),
104
109
  });
105
110
  }
106
-
107
111
  function mergeUsage(
108
112
  current: SpanFields["usage"],
109
113
  incoming: SpanFields["usage"],
@@ -118,28 +122,23 @@ function mergeUsage(
118
122
  reasoningOutputTokens: incoming.reasoningOutputTokens ?? current?.reasoningOutputTokens,
119
123
  });
120
124
  }
121
-
122
- function messagesResponse(response: unknown): SpanFields {
123
- const r = asRecord(response) ?? {};
124
- const role = readString(r.role) ?? "assistant";
125
+ function messagesResponse<T>(response: T): SpanFields {
126
+ const record: ValueRecord = asRecord(response) ?? {};
127
+ const role = readString(record.role) ?? "assistant";
125
128
  return {
126
- responseModel: readString(r.model),
127
- responseId: readString(r.id),
128
- finishReason: readString(r.stop_reason),
129
- output: r.content !== undefined ? [{ role, content: r.content }] : undefined,
130
- usage: messagesUsage(r.usage),
129
+ responseModel: readString(record.model),
130
+ responseId: readString(record.id),
131
+ finishReason: readString(record.stop_reason),
132
+ output: record.content !== undefined ? [{ role, content: record.content }] : undefined,
133
+ usage: messagesUsage(record.usage),
131
134
  };
132
135
  }
133
-
134
- function constructorName(value: unknown): string | undefined {
135
- if (value === null || (typeof value !== "object" && typeof value !== "function")) {
136
- return undefined;
137
- }
138
- const ctor = value.constructor;
139
- return typeof ctor === "function" ? ctor.name : undefined;
136
+ function constructorName<T>(value: T): string | undefined {
137
+ if (value === null || value === undefined) return undefined;
138
+ const ctor = Object(value).constructor;
139
+ return ctor instanceof Function ? ctor.name : undefined;
140
140
  }
141
-
142
- function providerForClient(client: unknown): string {
141
+ function providerForClient<T>(client: T): string {
143
142
  switch (constructorName(client)) {
144
143
  case "AnthropicBedrock":
145
144
  case "AsyncAnthropicBedrock":
@@ -151,23 +150,18 @@ function providerForClient(client: unknown): string {
151
150
  return "anthropic";
152
151
  }
153
152
  }
154
-
155
- function providerForResource(resource: unknown): string {
153
+ function providerForResource<T>(resource: T): string {
156
154
  return providerForClient(asRecord(resource)?._client);
157
155
  }
158
-
159
- function isWrapped(fn: unknown): fn is WrappedFunction {
160
- if (typeof fn !== "function") return false;
161
- const wrapped = fn as WrappedFunction;
162
- return wrapped[WRAPPED] === true;
156
+ function isWrapped<T>(fn: T): fn is T & WrappedFunction {
157
+ if (!(fn instanceof Function)) return false;
158
+ return (fn as WrappedFunction)[WRAPPED] === true;
163
159
  }
164
-
165
- function markWrapped<T extends WrappedFunction>(fn: T, original: (...args: never[]) => unknown): T {
160
+ function markWrapped<T extends WrappedFunction>(fn: T, original: AnyFunction): T {
166
161
  Object.defineProperty(fn, WRAPPED, { value: true });
167
162
  Object.defineProperty(fn, WRAPPED_ORIGINAL, { value: original });
168
163
  return fn;
169
164
  }
170
-
171
165
  function endOnce(span: SpanHandle): (fields?: SpanFields) => void {
172
166
  let ended = false;
173
167
  return (fields?: SpanFields) => {
@@ -180,39 +174,32 @@ function endOnce(span: SpanHandle): (fields?: SpanFields) => void {
180
174
  type TracedResult<T> = Promise<T> & {
181
175
  asResponse(): Promise<Response>;
182
176
  withResponse(): Promise<WrappedResponse<T>>;
183
- _thenUnwrap<U>(transform: (data: T, ...args: unknown[]) => U): TracedResult<U>;
177
+ _thenUnwrap<U>(transform: (data: T, ...args: Value[]) => U): TracedResult<U>;
184
178
  };
185
-
186
179
  interface TracePromiseContext {
187
180
  end: (fields?: SpanFields) => void;
188
181
  span: SpanHandle;
189
182
  startedAt: number;
190
183
  streaming: boolean;
191
- mapResponse: (response: unknown) => SpanFields;
184
+ mapResponse: (response: Value) => SpanFields;
192
185
  }
193
-
194
186
  interface StreamLike<T> extends AsyncIterable<T> {
195
187
  controller: AbortController;
196
188
  }
197
-
198
- type InnerAPIPromise = {
199
- then: Promise<unknown>["then"];
200
- catch: Promise<unknown>["catch"];
201
- finally: Promise<unknown>["finally"];
189
+ interface InnerAPIPromise {
190
+ then: Promise<Value>["then"];
191
+ catch: Promise<Value>["catch"];
192
+ finally: Promise<Value>["finally"];
202
193
  asResponse?: () => Promise<Response>;
203
- withResponse?: () => Promise<WrappedResponse<unknown>>;
204
- _thenUnwrap?: <U>(transform: (data: unknown, ...args: unknown[]) => U) => InnerAPIPromise;
205
- };
206
-
194
+ _thenUnwrap?: <U>(transform: (data: Value, ...args: Value[]) => U) => InnerAPIPromise;
195
+ }
207
196
  function mapRawResponse(response: Response): SpanFields {
197
+ const fields: SpanFields = { attributes: { "http.response.status_code": response.status } };
208
198
  const requestId = response.headers.get("request-id");
209
- return {
210
- ...(requestId ? { responseId: requestId } : {}),
211
- attributes: { "http.response.status_code": response.status },
212
- };
199
+ if (requestId) fields.responseId = requestId;
200
+ return fields;
213
201
  }
214
-
215
- function finalizeParsedValue(value: unknown, ctx: TracePromiseContext): unknown {
202
+ function finalizeParsedValue(value: Value, ctx: TracePromiseContext): Value {
216
203
  if (ctx.streaming) {
217
204
  const wrapped = wrapStream(value, ctx.span, ctx.startedAt, ctx.end);
218
205
  if (wrapped !== value) return wrapped;
@@ -220,28 +207,26 @@ function finalizeParsedValue(value: unknown, ctx: TracePromiseContext): unknown
220
207
  ctx.end(ctx.mapResponse(value));
221
208
  return value;
222
209
  }
223
-
224
210
  function rejectMissing(method: string): Promise<never> {
225
211
  return Promise.reject(new TypeError(`wrapped result has no ${method}`));
226
212
  }
227
-
228
- function makeTracedPromise<T>(inner: unknown, ctx: TracePromiseContext): TracedResult<T> {
229
- const source = inner as InnerAPIPromise;
213
+ function makeTracedPromise<T>(inner: InnerAPIPromise, ctx: TracePromiseContext): TracedResult<T> {
214
+ const source = inner;
230
215
  const originalThen = source.then.bind(source);
231
216
  const originalAsResponse = source.asResponse?.bind(source);
232
217
  const originalThenUnwrap = source._thenUnwrap?.bind(source);
233
- const handleError = (error: unknown): never => {
234
- ctx.end({ error });
218
+ const handleError = (error: Value): never => {
219
+ ctx.end({ error: error instanceof Error ? error : new Error(String(error)) });
235
220
  throw error;
236
221
  };
237
- const onParsed = (value: unknown) => finalizeParsedValue(value, ctx) as T;
222
+ const onParsed = (value: Value) => finalizeParsedValue(value, ctx) as T;
238
223
  const parsed = () => originalThen(onParsed, handleError);
239
- const traced =
240
- constructorName(source) === "APIPromise"
241
- ? (source as TracedResult<T>)
242
- : (new Promise<T>((resolve, reject) => {
243
- parsed().then(resolve, reject);
244
- }) as TracedResult<T>);
224
+ let traced: TracedResult<T>;
225
+ if (constructorName(source) === "APIPromise") {
226
+ traced = source as TracedResult<T>;
227
+ } else {
228
+ traced = new Promise<T>((resolve, reject) => parsed().then(resolve, reject)) as TracedResult<T>;
229
+ }
245
230
  if (traced === source) {
246
231
  // oxlint-disable-next-line unicorn/no-thenable -- preserve Anthropic APIPromise subclass contract.
247
232
  traced.then = ((onfulfilled, onrejected) =>
@@ -259,28 +244,27 @@ function makeTracedPromise<T>(inner: unknown, ctx: TracePromiseContext): TracedR
259
244
  traced.withResponse = () => {
260
245
  if (!originalAsResponse) return rejectMissing("withResponse");
261
246
  return Promise.all([parsed(), originalAsResponse()]).then(
262
- ([data, response]) => ({
263
- data,
264
- response,
265
- request_id: response.headers.get("request-id"),
266
- }),
247
+ ([data, response]) => ({ data, response, request_id: response.headers.get("request-id") }),
267
248
  handleError,
268
249
  );
269
250
  };
270
- traced._thenUnwrap = <U>(transform: (data: T, ...args: unknown[]) => U): TracedResult<U> => {
271
- if (!originalThenUnwrap) return rejectMissing("_thenUnwrap") as unknown as TracedResult<U>;
251
+ traced._thenUnwrap = <U>(transform: (data: T, ...args: Value[]) => U): TracedResult<U> => {
252
+ if (!originalThenUnwrap) {
253
+ return rejectMissing("_thenUnwrap") as Promise<never> & TracedResult<U>;
254
+ }
272
255
  return makeTracedPromise(
273
- originalThenUnwrap(transform as (data: unknown, ...args: unknown[]) => U),
256
+ originalThenUnwrap((data, ...args) => {
257
+ return transform(data as T, ...args);
258
+ }),
274
259
  ctx,
275
260
  );
276
261
  };
277
262
  return traced;
278
263
  }
279
264
 
280
- function isToolBlockState(value: UnknownRecord | ToolBlockState): value is ToolBlockState {
265
+ function isToolBlockState(value: ValueRecord | ToolBlockState): value is ToolBlockState {
281
266
  return "data" in value && "inputJson" in value;
282
267
  }
283
-
284
268
  function setFirstStreamUpdate(
285
269
  span: SpanHandle,
286
270
  startedAt: number,
@@ -295,23 +279,21 @@ function setFirstStreamUpdate(
295
279
  responseModel: fields.responseModel,
296
280
  });
297
281
  }
298
-
299
- function parseToolInput(inputJson: string): unknown {
282
+ function parseToolInput(inputJson: string): JsonValue {
300
283
  if (inputJson.length === 0) return {};
301
284
  try {
302
- return JSON.parse(inputJson) as unknown;
285
+ return JSON.parse(inputJson);
303
286
  } catch {
304
287
  return inputJson;
305
288
  }
306
289
  }
307
-
308
- function finalizeBlock(block: UnknownRecord | ToolBlockState): UnknownRecord {
290
+ function finalizeBlock(block: ValueRecord | ToolBlockState) {
309
291
  if (!isToolBlockState(block)) return block;
310
292
  const input = parseToolInput(block.inputJson);
311
293
  if (block.data.input === undefined) return { ...block.data, input };
312
294
  if (block.inputJson.length === 0) return block.data;
313
- const existingInput = asRecord(block.data.input);
314
- const parsedInput = asRecord(input);
295
+ const existingInput: ValueRecord | undefined = asRecord(block.data.input);
296
+ const parsedInput: ValueRecord | undefined = asRecord(input);
315
297
  return {
316
298
  ...block.data,
317
299
  input:
@@ -320,37 +302,26 @@ function finalizeBlock(block: UnknownRecord | ToolBlockState): UnknownRecord {
320
302
  : input,
321
303
  };
322
304
  }
323
-
324
- function streamOutput(state: StreamState): UnknownRecord[] | undefined {
305
+ function streamOutput(state: StreamState): ValueRecord[] | undefined {
325
306
  if (state.blocks.size === 0) return undefined;
326
- return [
327
- {
328
- role: "assistant",
329
- content: [...state.blocks.entries()]
330
- .sort(([left], [right]) => left - right)
331
- .map(([, block]) => finalizeBlock(block)),
332
- },
333
- ];
307
+ const content = [...state.blocks.entries()]
308
+ .sort(([left], [right]) => left - right)
309
+ .map(([, block]) => finalizeBlock(block));
310
+ return [{ role: "assistant", content: content as JsonValue[] }];
334
311
  }
335
-
336
312
  function streamPartialFields(state: StreamState): SpanFields {
337
- return {
338
- output: streamOutput(state),
339
- usage: state.usage,
340
- finishReason: state.finishReason,
341
- };
313
+ return { output: streamOutput(state), usage: state.usage, finishReason: state.finishReason };
342
314
  }
343
-
344
- function recordContentBlockStart(event: UnknownRecord, state: StreamState): void {
315
+ function recordContentBlockStart(event: ValueRecord, state: StreamState): void {
345
316
  const index = readNumber(event.index) ?? state.blocks.size;
346
- const contentBlock = asRecord(event.content_block) ?? {};
317
+ const contentBlock: ValueRecord = asRecord(event.content_block) ?? {};
347
318
  const type = readString(contentBlock.type);
348
319
  if (type === "text") {
349
320
  state.blocks.set(index, { type, text: readString(contentBlock.text) ?? "" });
350
321
  return;
351
322
  }
352
323
  if (type === "tool_use" || type === "server_tool_use") {
353
- const data: UnknownRecord = { ...contentBlock, type };
324
+ const data = { ...contentBlock, type };
354
325
  state.blocks.set(index, { data, inputJson: "" });
355
326
  return;
356
327
  }
@@ -360,12 +331,11 @@ function recordContentBlockStart(event: UnknownRecord, state: StreamState): void
360
331
  }
361
332
  state.blocks.set(index, { ...contentBlock });
362
333
  }
363
-
364
334
  function blockForDelta(
365
335
  index: number,
366
336
  deltaType: string | undefined,
367
337
  state: StreamState,
368
- ): UnknownRecord | ToolBlockState {
338
+ ): ValueRecord | ToolBlockState {
369
339
  const existing = state.blocks.get(index);
370
340
  if (existing) return existing;
371
341
  if (deltaType === "input_json_delta") {
@@ -380,22 +350,19 @@ function blockForDelta(
380
350
  state.blocks.set(index, block);
381
351
  return block;
382
352
  }
383
-
384
- function appendStringField(target: UnknownRecord, key: string, value: string | undefined): void {
353
+ function appendStringField(target: ValueRecord, key: string, value: string | undefined): void {
385
354
  if (value === undefined) return;
386
355
  const existing = readString(target[key]) ?? "";
387
356
  target[key] = `${existing}${value}`;
388
357
  }
389
-
390
- function appendArrayField(target: UnknownRecord, key: string, value: unknown): void {
358
+ function appendArrayField(target: ValueRecord, key: string, value: JsonValue | undefined): void {
391
359
  if (value === undefined) return;
392
- const existing = Array.isArray(target[key]) ? (target[key] as unknown[]) : [];
360
+ const existing = Array.isArray(target[key]) ? target[key] : [];
393
361
  target[key] = [...existing, value];
394
362
  }
395
-
396
- function recordContentBlockDelta(event: UnknownRecord, state: StreamState): void {
363
+ function recordContentBlockDelta(event: ValueRecord, state: StreamState): void {
397
364
  const index = readNumber(event.index) ?? 0;
398
- const delta = asRecord(event.delta) ?? {};
365
+ const delta: ValueRecord = asRecord(event.delta) ?? {};
399
366
  const deltaType = readString(delta.type);
400
367
  const block = blockForDelta(index, deltaType, state);
401
368
  if (deltaType === "input_json_delta") {
@@ -405,67 +372,67 @@ function recordContentBlockDelta(event: UnknownRecord, state: StreamState): void
405
372
  const data = isToolBlockState(block) ? block.data : block;
406
373
  if (deltaType === "text_delta") appendStringField(data, "text", readString(delta.text));
407
374
  if (deltaType === "citations_delta") appendArrayField(data, "citations", delta.citation);
408
- if (deltaType === "thinking_delta") {
375
+ if (deltaType === "thinking_delta")
409
376
  appendStringField(data, "thinking", readString(delta.thinking));
410
- }
411
377
  if (deltaType === "signature_delta" && delta.signature !== undefined)
412
378
  data.signature = delta.signature;
413
379
  }
414
-
415
- function recordStreamEvent(event: unknown, state: StreamState): SpanFields {
416
- const e = asRecord(event) ?? {};
417
- const type = readString(e.type);
380
+ function recordStreamEvent<T>(event: T, state: StreamState): SpanFields {
381
+ const record: ValueRecord = asRecord(event) ?? {};
382
+ const type = readString(record.type);
418
383
  const fields: SpanFields = {};
419
384
  if (type === "message_start") {
420
- const message = asRecord(e.message) ?? {};
385
+ const message: ValueRecord = asRecord(record.message) ?? {};
421
386
  fields.responseId = readString(message.id);
422
387
  fields.responseModel = readString(message.model);
423
388
  state.usage = mergeUsage(state.usage, messagesUsage(message.usage));
424
389
  }
425
- if (type === "content_block_start") recordContentBlockStart(e, state);
426
- if (type === "content_block_delta") recordContentBlockDelta(e, state);
390
+ if (type === "content_block_start") recordContentBlockStart(record, state);
391
+ if (type === "content_block_delta") recordContentBlockDelta(record, state);
427
392
  if (type === "message_delta") {
428
- const delta = asRecord(e.delta) ?? {};
393
+ const delta = asRecord(record.delta) ?? {};
429
394
  state.finishReason = readString(delta.stop_reason) ?? state.finishReason;
430
- state.usage = mergeUsage(state.usage, messagesUsage(e.usage));
395
+ state.usage = mergeUsage(state.usage, messagesUsage(record.usage));
431
396
  }
432
397
  return fields;
433
398
  }
434
-
435
- function isStreamLike(value: unknown): value is StreamLike<unknown> {
436
- if (value === null || typeof value !== "object") return false;
437
- const stream = value as Partial<StreamLike<unknown>>;
438
- const signal = asRecord(stream.controller)?.signal;
439
- return (
440
- typeof stream[Symbol.asyncIterator] === "function" &&
441
- signal !== undefined &&
442
- typeof asRecord(signal)?.addEventListener === "function"
443
- );
399
+ function isStreamLike<T>(value: T): value is T & StreamLike<Value> {
400
+ const stream = asRecord(value);
401
+ if (!stream) return false;
402
+ const controller = asRecord(stream.controller);
403
+ const signal = asRecord(controller?.signal);
404
+ if (
405
+ !controller ||
406
+ !(controller.abort instanceof Function) ||
407
+ !signal ||
408
+ typeof signal.aborted !== "boolean" ||
409
+ !(signal.addEventListener instanceof Function)
410
+ )
411
+ return false;
412
+ return stream[Symbol.asyncIterator] instanceof Function;
444
413
  }
445
-
446
414
  function createObservedMessagesStream(
447
- source: StreamLike<unknown>,
415
+ source: StreamLike<Value>,
448
416
  span: SpanHandle,
449
417
  startedAt: number,
450
418
  end: (fields?: SpanFields) => void,
451
- ): Stream<unknown> {
419
+ ): Stream<Value> {
452
420
  const state: StreamState = { blocks: new Map() };
453
- // The iterator's finally block never runs when iteration never starts, so an
454
- // abort on an unconsumed stream must end the span here. Once iteration begins,
455
- // the finally block owns the span end (the SDK aborts the controller as normal
456
- // cleanup when the caller stops early, which is not an error).
457
421
  let consumed = false;
458
422
  const signal = source.controller.signal;
459
423
  const endAborted = () => {
460
424
  if (consumed) return;
461
- end({ ...streamPartialFields(state), error: signal.reason ?? new Error("Request aborted") });
425
+ end({
426
+ ...streamPartialFields(state),
427
+ error: signal.reason instanceof Error ? signal.reason : new Error("Request aborted"),
428
+ });
462
429
  };
463
430
  if (signal.aborted) endAborted();
464
431
  else signal.addEventListener("abort", endAborted, { once: true });
465
432
  async function* iterator() {
466
433
  consumed = true;
467
434
  const sawFirst = { value: false };
468
- let terminalError: unknown;
435
+ let terminalError: Value;
469
436
  try {
470
437
  for await (const event of source) {
471
438
  const fields = recordStreamEvent(event, state);
@@ -473,91 +440,86 @@ function createObservedMessagesStream(
473
440
  yield event;
474
441
  }
475
442
  } catch (error) {
476
- terminalError = error;
443
+ terminalError = error as Value;
477
444
  throw error;
478
445
  } finally {
479
- end({
480
- ...streamPartialFields(state),
481
- ...(terminalError !== undefined ? { error: terminalError } : {}),
482
- });
446
+ const fields = streamPartialFields(state);
447
+ if (terminalError !== undefined)
448
+ fields.error =
449
+ terminalError instanceof Error ? terminalError : new Error(String(terminalError));
450
+ end(fields);
483
451
  }
484
452
  }
485
453
  return new Stream(() => iterator(), source.controller);
486
454
  }
487
-
488
- function wrapStream(
489
- value: unknown,
455
+ function wrapStream<T>(
456
+ value: T,
490
457
  span: SpanHandle,
491
458
  startedAt: number,
492
459
  end: (fields?: SpanFields) => void,
493
- ): unknown {
460
+ ): T | Stream<Value> {
494
461
  if (!isStreamLike(value)) return value;
495
462
  return createObservedMessagesStream(value, span, startedAt, end);
496
463
  }
497
-
498
- function wrapCreate<Fn extends (...args: never[]) => unknown>(
464
+ function wrapCreate<Fn extends AnyFunction>(
499
465
  original: Fn,
500
- mapRequest: (body: UnknownRecord) => RequestMapping,
501
- mapResponse: (response: unknown) => SpanFields,
466
+ mapRequest: (body: ValueRecord) => RequestMapping,
467
+ mapResponse: (response: Value) => SpanFields,
502
468
  provider: ProviderResolver,
503
469
  ): Fn {
504
470
  if (isWrapped(original)) return original;
505
- const wrapped = function (this: unknown, ...args: unknown[]): unknown {
506
- const body = asRecord(args[0]) ?? {};
471
+ const wrapped: WrappedFunction = function (this: Value, ...args: Value[]): OpaqueValue {
472
+ const body: ValueRecord = asRecord(args[0]) ?? {};
507
473
  const streaming = body.stream === true;
508
474
  const request = mapRequest(body);
509
475
  const span = startSpan(request.name, { ...request.fields, provider: provider(this) });
510
476
  const end = endOnce(span);
511
477
  const startedAt = Date.now();
512
-
513
478
  try {
514
479
  const result = original.apply(this, args as never[]);
515
- return makeTracedPromise(result, { end, span, startedAt, streaming, mapResponse });
480
+ return makeTracedPromise(result as InnerAPIPromise, {
481
+ end,
482
+ span,
483
+ startedAt,
484
+ streaming,
485
+ mapResponse,
486
+ }) as OpaqueValue;
516
487
  } catch (error) {
517
- end({ error });
488
+ end({ error: error instanceof Error ? error : new Error(String(error)) });
518
489
  throw error;
519
490
  }
520
491
  };
521
- return markWrapped(wrapped as WrappedFunction, original) as unknown as Fn;
492
+ return markWrapped(wrapped, original) as AnyFunction & Fn;
522
493
  }
523
-
524
- function patchInstanceMethod<T extends UnknownRecord>(
494
+ function patchInstanceMethod<T extends ValueRecord>(
525
495
  target: T,
526
496
  key: keyof T,
527
- request: (body: UnknownRecord) => RequestMapping,
528
- response: (value: unknown) => SpanFields,
497
+ request: (body: ValueRecord) => RequestMapping,
498
+ response: (value: Value) => SpanFields,
529
499
  provider: string,
530
500
  ): void {
531
501
  const current = target[key];
532
- // An inherited wrapper (from the global prototype patch) is not an own wrapper:
533
- // install one over the original so the client keeps emitting spans after
534
- // uninstrumentAnthropic().
535
502
  if (isWrapped(current) && Object.hasOwn(target, key)) return;
536
503
  const original = isWrapped(current) ? current[WRAPPED_ORIGINAL] : current;
537
- if (typeof original !== "function") return;
504
+ if (!(original instanceof Function)) return;
505
+ const callable = original as AnyFunction;
538
506
  target[key] = wrapCreate(
539
- original.bind(target) as (...args: never[]) => unknown,
507
+ callable.bind(target),
540
508
  request,
541
509
  response,
542
510
  () => provider,
543
- ) as T[keyof T];
511
+ ) as AnyFunction & T[keyof T];
544
512
  }
545
-
546
513
  function patchPrototype(
547
514
  klass: { prototype: object },
548
515
  key: string,
549
- request: (body: UnknownRecord) => RequestMapping,
550
- response: (value: unknown) => SpanFields,
516
+ request: (body: ValueRecord) => RequestMapping,
517
+ response: (value: Value) => SpanFields,
551
518
  ): () => void {
552
- const prototype = klass.prototype as UnknownRecord;
519
+ const prototype = klass.prototype as CallableRecord;
553
520
  const original = prototype[key];
554
- if (typeof original !== "function" || isWrapped(original)) return () => {};
555
- prototype[key] = wrapCreate(
556
- original as (...args: never[]) => unknown,
557
- request,
558
- response,
559
- providerForResource,
560
- );
521
+ if (!(original instanceof Function) || isWrapped(original)) return () => {};
522
+ prototype[key] = wrapCreate(original, request, response, providerForResource);
561
523
  return () => {
562
524
  prototype[key] = original;
563
525
  };
@@ -575,13 +537,11 @@ export function wrapAnthropic<T extends AnthropicClient>(client: T): T {
575
537
  wrappedClients.add(client);
576
538
  return client;
577
539
  }
578
-
579
540
  export function instrumentAnthropic(): void {
580
541
  if (installed) return;
581
542
  restorePatches = [patchPrototype(Messages, "create", messagesRequest, messagesResponse)];
582
543
  installed = true;
583
544
  }
584
-
585
545
  export function uninstrumentAnthropic(): void {
586
546
  if (!installed) return;
587
547
  for (const restore of restorePatches.reverse()) restore();