@telemetry-dev/bedrock 0.1.0 → 0.1.1

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/src/internal.ts CHANGED
@@ -11,97 +11,118 @@ export interface BedrockInstrumentationOptions {
11
11
  captureAgentTrace?: boolean;
12
12
  }
13
13
 
14
- export type SendFn = (...args: unknown[]) => unknown;
14
+ export type JsonValue =
15
+ | string
16
+ | number
17
+ | boolean
18
+ | null
19
+ | undefined
20
+ | JsonValue[]
21
+ | JsonRecord
22
+ | ((...args: JsonValue[]) => JsonValue);
23
+ export interface JsonRecord {
24
+ [key: string]: JsonValue;
25
+ }
26
+
27
+ export type SendFn = (...args: unknown[]) => JsonValue | object | Promise<JsonValue | object>;
15
28
 
16
29
  export interface OpHandler {
17
- spanName(input: Record<string, unknown>): string;
18
- spanType(input: Record<string, unknown>): SpanType;
19
- requestFields(input: Record<string, unknown>, options: BedrockInstrumentationOptions): SpanFields;
30
+ spanName(input: JsonRecord): string;
31
+ spanType(input: JsonRecord): SpanType;
32
+ requestFields(input: JsonRecord, options: BedrockInstrumentationOptions): SpanFields;
20
33
  onResult(
21
- result: unknown,
34
+ result: JsonValue,
22
35
  span: SpanHandle,
23
36
  t0: number,
24
- input: Record<string, unknown>,
37
+ input: JsonRecord,
25
38
  options: BedrockInstrumentationOptions,
26
- ): unknown;
39
+ ): JsonValue | object;
27
40
  }
28
41
 
29
42
  export function createInstrumentedSend(
30
43
  originalSend: SendFn,
31
44
  options: BedrockInstrumentationOptions,
32
- handlers: Record<string, OpHandler>,
45
+ handlers: { [key: string]: OpHandler },
33
46
  ): SendFn {
34
- return function instrumentedSend(this: unknown, command: unknown, ...rest: unknown[]) {
35
- const name = command?.constructor?.name;
36
- const handler = typeof name === "string" ? handlers[name] : undefined;
47
+ return function instrumentedSend<TThis, TCommand, TRest extends unknown[]>(
48
+ this: TThis,
49
+ command: TCommand,
50
+ ...rest: TRest
51
+ ) {
52
+ const rawCommand = asJsonValue(command);
53
+ const rawRest = rest.map(asJsonValue);
54
+ const name = isRecord(rawCommand) ? rawCommand.constructor?.name : undefined;
55
+ const handler = String(name) === name ? handlers[name] : undefined;
37
56
  if (!handler) {
38
- return originalSend.apply(this, [command, ...rest]);
57
+ return originalSend.apply(this, [rawCommand, ...rawRest]);
39
58
  }
40
59
 
41
- const input = commandInput(command);
60
+ const input = commandInput(rawCommand);
42
61
  const t0 = performance.now();
43
62
  const spanType = safe(() => handler.spanType(input)) ?? "span";
44
63
  const spanName = safe(() => handler.spanName(input)) ?? name ?? "bedrock";
45
64
  const fields = safe(() => handler.requestFields(input, options)) ?? {};
46
65
  const span = startSpan(spanName, { type: spanType, ...fields });
47
- const callbackIndex = rest.findIndex((arg) => typeof arg === "function");
66
+ const callbackIndex = rawRest.findIndex((arg) => arg instanceof Function);
48
67
  if (callbackIndex !== -1) {
49
- const callback = rest[callbackIndex] as (error: unknown, data?: unknown) => void;
50
- const wrappedRest = [...rest];
51
- wrappedRest[callbackIndex] = (error: unknown, data?: unknown) => {
52
- if (error != null) {
53
- endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
54
- callback(error, data);
68
+ const callback = rawRest[callbackIndex] as (cause: JsonValue, data?: JsonValue) => void;
69
+ const wrappedRest = [...rawRest];
70
+ wrappedRest[callbackIndex] = <TError, TData>(error: TError, data?: TData) => {
71
+ const rawError: unknown = error;
72
+ const rawData: unknown = data;
73
+ if (rawError != null) {
74
+ endSpan(span, { ...awsMetadataFields(metadataOf(rawError)), error: asError(rawError) });
75
+ callback(asJsonValue(rawError), asJsonValue(rawData));
55
76
  return;
56
77
  }
57
- let nextData = data;
78
+ let nextData: JsonValue | object = asJsonValue(rawData);
58
79
  try {
59
- nextData = handler.onResult(data, span, t0, input, options);
80
+ nextData = handler.onResult(asJsonValue(rawData), span, t0, input, options);
60
81
  } catch {
61
- endSpan(span, awsMetadataFields(metadataOf(data)));
82
+ endSpan(span, awsMetadataFields(metadataOf(rawData)));
62
83
  }
63
- callback(error, nextData);
84
+ callback(asJsonValue(rawError), asJsonValue(nextData));
64
85
  };
65
86
  try {
66
- return originalSend.apply(this, [command, ...wrappedRest]);
87
+ return originalSend.apply(this, [rawCommand, ...wrappedRest]);
67
88
  } catch (error) {
68
- endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
89
+ endSpan(span, { ...awsMetadataFields(metadataOf(error)), error: asError(error) });
69
90
  throw error;
70
91
  }
71
92
  }
72
93
 
73
- let result: unknown;
94
+ let result: JsonValue | object | Promise<JsonValue | object>;
74
95
  try {
75
- result = originalSend.apply(this, [command, ...rest]);
96
+ result = originalSend.apply(this, [rawCommand, ...rawRest]);
76
97
  } catch (error) {
77
- endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
98
+ endSpan(span, { ...awsMetadataFields(metadataOf(error)), error: asError(error) });
78
99
  throw error;
79
100
  }
80
101
 
81
102
  return Promise.resolve(result).then(
82
103
  (resolved) => {
83
104
  try {
84
- return handler.onResult(resolved, span, t0, input, options);
105
+ return handler.onResult(asJsonValue(resolved), span, t0, input, options);
85
106
  } catch {
86
107
  endSpan(span, awsMetadataFields(metadataOf(resolved)));
87
108
  return resolved;
88
109
  }
89
110
  },
90
111
  (error) => {
91
- endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
112
+ endSpan(span, { ...awsMetadataFields(metadataOf(error)), error: asError(error) });
92
113
  throw error;
93
114
  },
94
115
  );
95
116
  };
96
117
  }
97
118
 
98
- export function commandInput(command: unknown): Record<string, unknown> {
99
- const value = (command as { input?: unknown } | null | undefined)?.input;
100
- return isRecord(value) ? value : {};
119
+ export function commandInput<T>(command: T): JsonRecord {
120
+ if (!isRecord(command)) return {};
121
+ return isRecord(command.input) ? command.input : {};
101
122
  }
102
123
 
103
- export function isRecord(value: unknown): value is Record<string, unknown> {
104
- return value !== null && typeof value === "object" && !Array.isArray(value);
124
+ export function isRecord<T>(value: T): value is T & JsonRecord {
125
+ return value !== null && Object(value) === value && !Array.isArray(value);
105
126
  }
106
127
 
107
128
  export function safe<T>(fn: () => T): T | undefined {
@@ -128,15 +149,15 @@ export function updateSpan(span: SpanHandle, fields: SpanFields): void {
128
149
  }
129
150
  }
130
151
 
131
- export function metadataOf(value: unknown): unknown {
132
- if (isRecord(value) && isRecord(value.$metadata)) return value.$metadata;
133
- return (value as { $metadata?: unknown } | null | undefined)?.$metadata;
152
+ export function metadataOf<T>(value: T): JsonValue {
153
+ if (!isRecord(value)) return undefined;
154
+ return value.$metadata;
134
155
  }
135
156
 
136
157
  export function modeledStreamError(
137
- event: Record<string, unknown>,
158
+ event: JsonRecord,
138
159
  ): { error: Error; fields: SpanFields } | undefined {
139
- const keys: Record<string, string> = {
160
+ const keys = {
140
161
  accessDeniedException: "AccessDeniedException",
141
162
  badGatewayException: "BadGatewayException",
142
163
  conflictException: "ConflictException",
@@ -150,7 +171,7 @@ export function modeledStreamError(
150
171
  serviceUnavailableException: "ServiceUnavailableException",
151
172
  throttlingException: "ThrottlingException",
152
173
  validationException: "ValidationException",
153
- };
174
+ } as const;
154
175
  for (const [key, name] of Object.entries(keys)) {
155
176
  const exception = isRecord(event[key]) ? event[key] : undefined;
156
177
  if (!exception) continue;
@@ -163,12 +184,13 @@ export function modeledStreamError(
163
184
  return undefined;
164
185
  }
165
186
 
166
- export function awsMetadataFields(metadata: unknown): SpanFields {
167
- if (!isRecord(metadata)) return {};
168
- const requestId = stringValue(metadata.requestId);
169
- const attempts = numberValue(metadata.attempts);
170
- const httpStatusCode = numberValue(metadata.httpStatusCode);
171
- const totalRetryDelay = numberValue(metadata.totalRetryDelay);
187
+ export function awsMetadataFields<T>(metadata: T): SpanFields {
188
+ const raw: unknown = metadata;
189
+ if (!isRecord(raw)) return {};
190
+ const requestId = stringValue(raw.requestId);
191
+ const attempts = numberValue(raw.attempts);
192
+ const httpStatusCode = numberValue(raw.httpStatusCode);
193
+ const totalRetryDelay = numberValue(raw.totalRetryDelay);
172
194
  const attributes: SpanFields["attributes"] = {};
173
195
  if (httpStatusCode !== undefined) attributes["aws.http.status_code"] = httpStatusCode;
174
196
  if (attempts !== undefined && attempts > 1) attributes["aws.request.attempts"] = attempts;
@@ -194,41 +216,45 @@ export function mergeFields(...fields: Array<SpanFields | undefined>): SpanField
194
216
  return merged;
195
217
  }
196
218
 
197
- export function omitUndefined<T extends Record<string, unknown>>(value: T): T {
219
+ export function omitUndefined<T extends object>(value: T): T {
198
220
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T;
199
221
  }
200
222
 
201
- export function stringValue(value: unknown): string | undefined {
202
- return typeof value === "string" && value.length > 0 ? value : undefined;
223
+ export function stringValue<T>(value: T): string | undefined {
224
+ const raw: unknown = value;
225
+ return String(raw) === raw && raw.length > 0 ? raw : undefined;
203
226
  }
204
227
 
205
- export function numberValue(value: unknown): number | undefined {
206
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
228
+ export function numberValue<T>(value: T): number | undefined {
229
+ const raw: unknown = value;
230
+ return Number(raw) === raw && Number.isFinite(raw) ? raw : undefined;
207
231
  }
208
232
 
209
- export function arrayValue<T>(value: unknown): T[] | undefined {
233
+ export function arrayValue<T, TValue = unknown>(value: TValue): T[] | undefined {
210
234
  return Array.isArray(value) ? (value as T[]) : undefined;
211
235
  }
212
236
 
213
- export function bytesToString(value: unknown): string | undefined {
214
- if (typeof value === "string") return value;
215
- if (value instanceof Uint8Array) return new TextDecoder().decode(value);
216
- if (ArrayBuffer.isView(value)) return new TextDecoder().decode(value as Uint8Array);
217
- return undefined;
237
+ export function bytesToString<T>(value: T): string | undefined {
238
+ const raw: unknown = value;
239
+ if (String(raw) === raw) return raw;
240
+ if (raw instanceof Uint8Array) return new TextDecoder().decode(raw);
241
+ if (ArrayBuffer.isView(raw)) {
242
+ return new TextDecoder().decode(new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength));
243
+ }
218
244
  }
219
245
 
220
- export function parseJson(value: unknown): unknown {
246
+ export function parseJson<T>(value: T): JsonValue {
221
247
  const text = bytesToString(value);
222
248
  if (text === undefined) return undefined;
223
249
  try {
224
- return JSON.parse(text) as unknown;
250
+ return asJsonValue(JSON.parse(text));
225
251
  } catch {
226
252
  return undefined;
227
253
  }
228
254
  }
229
255
 
230
256
  export interface StreamState {
231
- feed(event: unknown): void;
257
+ feed<T>(event: T): void;
232
258
  finish(partial: boolean): SpanFields;
233
259
  }
234
260
 
@@ -239,10 +265,7 @@ export function wrapAsyncIterable<T>(
239
265
  t0: number,
240
266
  baseFields: SpanFields,
241
267
  ): T {
242
- if (
243
- !iterable ||
244
- typeof (iterable as unknown as AsyncIterable<unknown>)[Symbol.asyncIterator] !== "function"
245
- ) {
268
+ if (!iterable || !isAsyncIterable(iterable)) {
246
269
  endSpan(span, baseFields);
247
270
  return iterable;
248
271
  }
@@ -257,11 +280,10 @@ export function wrapAsyncIterable<T>(
257
280
  endSpan(span, mergeFields(baseFields, fields));
258
281
  };
259
282
 
260
- const source = iterable as unknown as AsyncIterable<unknown>;
261
283
  const generator = async function* () {
262
284
  let completed = false;
263
285
  try {
264
- for await (const event of source) {
286
+ for await (const event of iterable) {
265
287
  if (!first) {
266
288
  first = true;
267
289
  updateSpan(span, { timeToFirstChunkMs: performance.now() - t0 });
@@ -271,17 +293,21 @@ export function wrapAsyncIterable<T>(
271
293
  }
272
294
  completed = true;
273
295
  } catch (error) {
274
- endOnce(mergeFields(state.finish(true), awsMetadataFields(metadataOf(error)), { error }));
296
+ endOnce(
297
+ mergeFields(state.finish(true), awsMetadataFields(metadataOf(error)), {
298
+ error: asError(error),
299
+ }),
300
+ );
275
301
  throw error;
276
302
  } finally {
277
303
  if (!ended) endOnce(state.finish(!completed));
278
304
  }
279
305
  };
280
306
 
281
- const wrapped: Record<PropertyKey, unknown> = {};
307
+ const wrapped: { [key: PropertyKey]: JsonValue | (() => AsyncGenerator<unknown>) } = {};
282
308
  if (isRecord(iterable)) {
283
309
  for (const key of Reflect.ownKeys(iterable)) {
284
- wrapped[key] = (iterable as Record<PropertyKey, unknown>)[key];
310
+ wrapped[key] = (iterable as { [key: PropertyKey]: JsonValue })[key];
285
311
  }
286
312
  }
287
313
  const finishAbandoned = () => endOnce(safe(() => state.finish(true)));
@@ -294,7 +320,19 @@ export function wrapAsyncIterable<T>(
294
320
  return wrapped as T;
295
321
  }
296
322
 
297
- export function basenameArn(value: unknown): string | undefined {
323
+ function isAsyncIterable<T>(value: T): value is T & AsyncIterable<unknown> {
324
+ return Object(value) === value && Symbol.asyncIterator in Object(value);
325
+ }
326
+
327
+ function asJsonValue<T>(value: T): JsonValue {
328
+ return value as JsonValue;
329
+ }
330
+
331
+ function asError<T>(value: T): Error {
332
+ return value instanceof Error ? value : new Error(String(value));
333
+ }
334
+
335
+ export function basenameArn<T>(value: T): string | undefined {
298
336
  const text = stringValue(value);
299
337
  if (!text) return undefined;
300
338
  return text.split(/[/:]/).filter(Boolean).at(-1) ?? text;
@@ -1,6 +1,7 @@
1
1
  import type { SpanFields } from "@telemetry-dev/sdk";
2
2
 
3
3
  import {
4
+ arrayValue,
4
5
  awsMetadataFields,
5
6
  isRecord,
6
7
  mergeFields,
@@ -10,22 +11,24 @@ import {
10
11
  parseJson,
11
12
  PROVIDER,
12
13
  stringValue,
14
+ type JsonRecord,
15
+ type JsonValue,
13
16
  type StreamState,
14
17
  } from "./internal.ts";
15
18
 
16
- export function isEmbeddingModel(modelId: unknown): boolean {
19
+ export function isEmbeddingModel<T>(modelId: T): boolean {
17
20
  const model = stringValue(modelId)?.toLowerCase() ?? "";
18
21
  return (
19
22
  model.includes("titan-embed") || model.includes("cohere.embed") || model.includes("-embedding")
20
23
  );
21
24
  }
22
25
 
23
- export function invokeModelSpanName(input: Record<string, unknown>): string {
26
+ export function invokeModelSpanName(input: JsonRecord): string {
24
27
  const model = stringValue(input.modelId) ?? "unknown";
25
28
  return `${isEmbeddingModel(model) ? "embeddings" : "chat"} ${model}`;
26
29
  }
27
30
 
28
- export function invokeModelRequestFields(input: Record<string, unknown>): SpanFields {
31
+ export function invokeModelRequestFields(input: JsonRecord): SpanFields {
29
32
  const model = stringValue(input.modelId);
30
33
  const body = parseBody(input.body, input.contentType);
31
34
  return omitUndefined({
@@ -37,23 +40,20 @@ export function invokeModelRequestFields(input: Record<string, unknown>): SpanFi
37
40
  });
38
41
  }
39
42
 
40
- export function invokeModelResponseFields(
41
- input: Record<string, unknown>,
42
- output: unknown,
43
- ): SpanFields {
43
+ export function invokeModelResponseFields(input: JsonRecord, output: JsonValue): SpanFields {
44
44
  const model = stringValue(input.modelId);
45
45
  const body = isRecord(output) ? parseBody(output.body, output.contentType) : undefined;
46
46
  const metadata = isRecord(output) ? output.$metadata : undefined;
47
47
  return mergeFields(awsMetadataFields(metadata), nativeResponseFields(model, body, metadata));
48
48
  }
49
49
 
50
- export function parseBody(body: unknown, contentType: unknown): unknown {
50
+ export function parseBody<TBody, TContentType>(body: TBody, contentType: TContentType): JsonValue {
51
51
  const type = stringValue(contentType) ?? "application/json";
52
52
  if (!type.includes("json")) return undefined;
53
53
  return parseJson(body);
54
54
  }
55
55
 
56
- function nativeSamplingFields(model: string | undefined, body: unknown): SpanFields {
56
+ function nativeSamplingFields<T>(model: string | undefined, body: T): SpanFields {
57
57
  if (!isRecord(body)) return {};
58
58
  const lower = model?.toLowerCase() ?? "";
59
59
  if (lower.includes("amazon.titan")) {
@@ -62,7 +62,7 @@ function nativeSamplingFields(model: string | undefined, body: unknown): SpanFie
62
62
  temperature: numberValue(cfg.temperature),
63
63
  topP: numberValue(cfg.topP),
64
64
  maxTokens: numberValue(cfg.maxTokenCount),
65
- stopSequences: Array.isArray(cfg.stopSequences) ? (cfg.stopSequences as string[]) : undefined,
65
+ stopSequences: arrayValue<string>(cfg.stopSequences),
66
66
  });
67
67
  }
68
68
  if (lower.includes("amazon.nova")) {
@@ -72,7 +72,7 @@ function nativeSamplingFields(model: string | undefined, body: unknown): SpanFie
72
72
  topP: numberValue(cfg.topP) ?? numberValue(cfg.top_p),
73
73
  topK: numberValue(cfg.topK),
74
74
  maxTokens: numberValue(cfg.maxTokens) ?? numberValue(cfg.max_new_tokens),
75
- stopSequences: Array.isArray(cfg.stopSequences) ? (cfg.stopSequences as string[]) : undefined,
75
+ stopSequences: arrayValue<string>(cfg.stopSequences),
76
76
  });
77
77
  }
78
78
  if (lower.includes("anthropic.claude")) {
@@ -81,9 +81,7 @@ function nativeSamplingFields(model: string | undefined, body: unknown): SpanFie
81
81
  topP: numberValue(body.top_p),
82
82
  topK: numberValue(body.top_k),
83
83
  maxTokens: numberValue(body.max_tokens),
84
- stopSequences: Array.isArray(body.stop_sequences)
85
- ? (body.stop_sequences as string[])
86
- : undefined,
84
+ stopSequences: arrayValue<string>(body.stop_sequences),
87
85
  });
88
86
  }
89
87
  if (lower.includes("meta.llama")) {
@@ -102,8 +100,8 @@ function nativeSamplingFields(model: string | undefined, body: unknown): SpanFie
102
100
 
103
101
  function nativeResponseFields(
104
102
  model: string | undefined,
105
- body: unknown,
106
- metadata: unknown,
103
+ body: JsonValue,
104
+ metadata: JsonValue,
107
105
  ): SpanFields {
108
106
  if (!isRecord(body)) return headerUsageFields(metadata);
109
107
  const lower = model?.toLowerCase() ?? "";
@@ -190,12 +188,12 @@ function nativeResponseFields(
190
188
  return withHeaderUsage({ output: body }, metadata);
191
189
  }
192
190
 
193
- function withHeaderUsage(fields: SpanFields, metadata: unknown): SpanFields {
191
+ function withHeaderUsage<T>(fields: SpanFields, metadata: T): SpanFields {
194
192
  if (fields.usage && Object.keys(fields.usage).length > 0) return fields;
195
193
  return mergeFields(fields, headerUsageFields(metadata));
196
194
  }
197
195
 
198
- function headerUsageFields(metadata: unknown): SpanFields {
196
+ function headerUsageFields<T>(metadata: T): SpanFields {
199
197
  const headers = isRecord(metadata)
200
198
  ? isRecord(metadata.httpHeaders)
201
199
  ? metadata.httpHeaders
@@ -210,9 +208,10 @@ function headerUsageFields(metadata: unknown): SpanFields {
210
208
  return { usage: Object.keys(usage).length > 0 ? usage : undefined };
211
209
  }
212
210
 
213
- function headerNumber(value: unknown): number | undefined {
214
- if (typeof value === "string") {
215
- const parsed = Number.parseInt(value, 10);
211
+ function headerNumber<T>(value: T): number | undefined {
212
+ const text = stringValue(value);
213
+ if (text !== undefined) {
214
+ const parsed = Number.parseInt(text, 10);
216
215
  return Number.isFinite(parsed) ? parsed : undefined;
217
216
  }
218
217
  return numberValue(value);
@@ -225,7 +224,7 @@ export class InvokeModelStreamState implements StreamState {
225
224
  private finishReason: string | undefined;
226
225
  private error: Error | undefined;
227
226
  private errorFields: SpanFields | undefined;
228
- feed(event: unknown): void {
227
+ feed<T>(event: T): void {
229
228
  if (!isRecord(event)) return;
230
229
  const streamError = modeledStreamError(event);
231
230
  if (streamError) {
@@ -303,7 +302,7 @@ export class InvokeModelStreamState implements StreamState {
303
302
  }
304
303
  }
305
304
 
306
- function textFromProviderChunk(value: unknown): string {
305
+ function textFromProviderChunk<T>(value: T): string {
307
306
  if (!isRecord(value)) return "";
308
307
  const delta = isRecord(value.delta) ? value.delta : undefined;
309
308
  const contentBlockDelta = isRecord(value.contentBlockDelta)