@telemetry-dev/bedrock 0.1.9 → 0.1.12

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.
@@ -10,7 +10,9 @@ import {
10
10
  omitUndefined,
11
11
  parseJson,
12
12
  PROVIDER,
13
+ STREAM_CAPTURE_MAX_ITEMS,
13
14
  stringValue,
15
+ StreamRetention,
14
16
  type JsonRecord,
15
17
  type JsonValue,
16
18
  type StreamState,
@@ -164,12 +166,7 @@ function nativeResponseFields(
164
166
  {
165
167
  output: body,
166
168
  finishReason: stringValue(body.stop_reason),
167
- usage: omitUndefined({
168
- inputTokens: numberValue(usage.input_tokens),
169
- outputTokens: numberValue(usage.output_tokens),
170
- cacheReadInputTokens: numberValue(usage.cache_read_input_tokens),
171
- cacheCreationInputTokens: numberValue(usage.cache_creation_input_tokens),
172
- }),
169
+ ...claudeUsageFields(usage),
173
170
  },
174
171
  metadata,
175
172
  );
@@ -215,6 +212,39 @@ function nativeResponseFields(
215
212
  return withHeaderUsage({ output: body }, metadata);
216
213
  }
217
214
 
215
+ const CLAUDE_USAGE_KEYS = {
216
+ inputTokens: "input_tokens",
217
+ outputTokens: "output_tokens",
218
+ cacheReadInputTokens: "cache_read_input_tokens",
219
+ cacheCreationInputTokens: "cache_creation_input_tokens",
220
+ } as const;
221
+
222
+ function claudeUsageFields(record: JsonRecord): SpanFields {
223
+ const usage: NonNullable<SpanFields["usage"]> = {};
224
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
225
+ const value = numberValue(record[source]);
226
+ if (value !== undefined) usage[key as keyof typeof CLAUDE_USAGE_KEYS] = value;
227
+ }
228
+ const iterations = record.iterations;
229
+ if (!Array.isArray(iterations)) return { usage };
230
+
231
+ // Executor iterations are already in top-level usage; only compaction is additive.
232
+ const first = Math.max(0, iterations.length - STREAM_CAPTURE_MAX_ITEMS);
233
+ for (let index = first; index < iterations.length; index += 1) {
234
+ const entry = iterations[index];
235
+ if (!isRecord(entry) || entry.type !== "compaction") continue;
236
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
237
+ const value = numberValue(entry[source]);
238
+ const target = key as keyof typeof CLAUDE_USAGE_KEYS;
239
+ if (value !== undefined) usage[target] = (usage[target] ?? 0) + value;
240
+ }
241
+ }
242
+ return {
243
+ usage,
244
+ ...(first > 0 ? { attributes: { "telemetry.dev.capture.truncated": true } } : {}),
245
+ };
246
+ }
247
+
218
248
  function withHeaderUsage<T>(fields: SpanFields, metadata: T): SpanFields {
219
249
  if (fields.usage && Object.keys(fields.usage).length > 0) return fields;
220
250
 
@@ -251,9 +281,13 @@ function headerNumber<T>(value: T): number | undefined {
251
281
  }
252
282
 
253
283
  export class InvokeModelStreamState implements StreamState {
284
+ readonly retention = new StreamRetention();
254
285
  private readonly chunks: unknown[] = [];
255
286
  private text = "";
256
287
  private usage: SpanFields["usage"];
288
+ private finalUsage: NonNullable<SpanFields["usage"]> = {};
289
+ private startUsage: NonNullable<SpanFields["usage"]> = {};
290
+ private compaction: SpanFields = {};
257
291
  private finishReason: string | undefined;
258
292
  private error: Error | undefined;
259
293
  private errorFields: SpanFields | undefined;
@@ -277,8 +311,10 @@ export class InvokeModelStreamState implements StreamState {
277
311
  return (typeof raw === "string" || raw instanceof Uint8Array) && raw.length > 0;
278
312
  }
279
313
 
280
- this.chunks.push(parsed);
281
- this.text += textFromProviderChunk(parsed);
314
+ if (this.retention.accept(parsed)) {
315
+ this.chunks.push(parsed);
316
+ this.text += textFromProviderChunk(parsed);
317
+ }
282
318
 
283
319
  if (isRecord(parsed)) {
284
320
  const delta = isRecord(parsed.delta) ? parsed.delta : undefined;
@@ -286,6 +322,28 @@ export class InvokeModelStreamState implements StreamState {
286
322
  const messageUsage = isRecord(message?.usage) ? message?.usage : undefined;
287
323
  const usage = isRecord(parsed.usage) ? parsed.usage : undefined;
288
324
 
325
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
326
+ const value = numberValue(usage?.[source]);
327
+ if (value !== undefined) this.finalUsage[key as keyof typeof CLAUDE_USAGE_KEYS] = value;
328
+ }
329
+ // message_start counts exclude compaction like message_delta counts; metrics may not.
330
+ this.startUsage = omitUndefined({
331
+ ...this.startUsage,
332
+ inputTokens: numberValue(messageUsage?.input_tokens) ?? this.startUsage.inputTokens,
333
+ outputTokens: numberValue(messageUsage?.output_tokens) ?? this.startUsage.outputTokens,
334
+ cacheReadInputTokens:
335
+ numberValue(messageUsage?.cache_read_input_tokens) ??
336
+ this.startUsage.cacheReadInputTokens,
337
+ cacheCreationInputTokens:
338
+ numberValue(messageUsage?.cache_creation_input_tokens) ??
339
+ this.startUsage.cacheCreationInputTokens,
340
+ });
341
+ // An interrupted stream keeps message_start's iterations until a cumulative delta replaces them.
342
+ const iterations = Array.isArray(usage?.iterations)
343
+ ? usage.iterations
344
+ : messageUsage?.iterations;
345
+ if (Array.isArray(iterations)) this.compaction = claudeUsageFields({ iterations });
346
+
289
347
  const metrics = isRecord(parsed["amazon-bedrock-invocationMetrics"])
290
348
  ? parsed["amazon-bedrock-invocationMetrics"]
291
349
  : undefined;
@@ -310,7 +368,7 @@ export class InvokeModelStreamState implements StreamState {
310
368
  this.usage = omitUndefined({
311
369
  ...this.usage,
312
370
  inputTokens:
313
- numberValue(messageUsage?.input_tokens) ??
371
+ numberValue(usage?.input_tokens) ??
314
372
  numberValue(metrics?.inputTokenCount) ??
315
373
  numberValue(parsed.inputTextTokenCount) ??
316
374
  this.usage?.inputTokens,
@@ -320,10 +378,9 @@ export class InvokeModelStreamState implements StreamState {
320
378
  numberValue(parsed.totalOutputTextTokenCount) ??
321
379
  this.usage?.outputTokens,
322
380
  cacheReadInputTokens:
323
- numberValue(messageUsage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
381
+ numberValue(usage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
324
382
  cacheCreationInputTokens:
325
- numberValue(messageUsage?.cache_creation_input_tokens) ??
326
- this.usage?.cacheCreationInputTokens,
383
+ numberValue(usage?.cache_creation_input_tokens) ?? this.usage?.cacheCreationInputTokens,
327
384
  });
328
385
  }
329
386
 
@@ -331,6 +388,18 @@ export class InvokeModelStreamState implements StreamState {
331
388
  }
332
389
 
333
390
  finish(): SpanFields {
391
+ const usage = { ...this.startUsage, ...this.usage, ...this.finalUsage };
392
+ for (const key of Object.keys(CLAUDE_USAGE_KEYS) as (keyof typeof CLAUDE_USAGE_KEYS)[]) {
393
+ const value = this.compaction.usage?.[key];
394
+ const baseKey = key === "outputTokens" ? "outputTokens" : "inputTokens";
395
+ // Invocation metrics have no documented compaction exclusion; never add to that base.
396
+ if (
397
+ value !== undefined &&
398
+ (this.finalUsage[baseKey] !== undefined || this.usage?.[baseKey] === undefined)
399
+ ) {
400
+ usage[key] = (usage[key] ?? 0) + value;
401
+ }
402
+ }
334
403
  return mergeFields(
335
404
  omitUndefined({
336
405
  output: this.text
@@ -343,7 +412,8 @@ export class InvokeModelStreamState implements StreamState {
343
412
  ]
344
413
  : this.chunks,
345
414
  finishReason: this.finishReason,
346
- usage: this.usage,
415
+ usage,
416
+ attributes: this.compaction.attributes,
347
417
  error: this.error,
348
418
  }),
349
419
  this.errorFields,