@telemetry-dev/bedrock 0.1.10 → 0.1.13
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/README.md +26 -4
- package/dist/agents.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{internal-handlers-PftQAPH3.mjs → internal-handlers-CLP2wXDh.mjs} +193 -33
- package/package.json +5 -5
- package/src/agent-handlers.ts +166 -31
- package/src/internal.ts +2 -2
- package/src/invoke-model.ts +77 -11
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
First-class AWS Bedrock instrumentation for the telemetry.dev TypeScript SDK.
|
|
4
4
|
|
|
5
|
+
Docs: https://telemetry.dev/docs/integrations/bedrock
|
|
6
|
+
|
|
5
7
|
This package instruments AWS SDK for JavaScript v3 clients for:
|
|
6
8
|
|
|
7
9
|
- `@aws-sdk/client-bedrock-runtime`
|
|
@@ -29,15 +31,18 @@ pnpm add @telemetry-dev/sdk @telemetry-dev/bedrock @aws-sdk/client-bedrock-runti
|
|
|
29
31
|
pnpm add @aws-sdk/client-bedrock-agent-runtime
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
Requires `@aws-sdk/client-bedrock-runtime` 3.600.0 or a later 3.x release (and the same range for the optional `@aws-sdk/client-bedrock-agent-runtime`), and `@telemetry-dev/sdk`, which needs Node.js 20.19.0 or later.
|
|
35
|
+
|
|
32
36
|
## Quickstart
|
|
33
37
|
|
|
38
|
+
`init()` reads your project API key from the `TELEMETRY_DEV_API_KEY` environment variable, so set it before you start the app.
|
|
39
|
+
|
|
34
40
|
```ts
|
|
35
41
|
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
|
|
36
42
|
import { init, shutdown } from "@telemetry-dev/sdk";
|
|
37
43
|
import { wrapBedrock } from "@telemetry-dev/bedrock";
|
|
38
44
|
|
|
39
45
|
init({
|
|
40
|
-
apiKey: process.env.TELEMETRY_DEV_API_KEY,
|
|
41
46
|
serviceName: "bedrock-app",
|
|
42
47
|
environment: "production",
|
|
43
48
|
});
|
|
@@ -98,9 +103,22 @@ instrumentBedrockAgents({ captureAgentTrace: false });
|
|
|
98
103
|
|
|
99
104
|
## Options
|
|
100
105
|
|
|
101
|
-
| Option | Default | Applies to | Notes
|
|
102
|
-
| ------------------- | ------- | --------------------- |
|
|
103
|
-
| `captureAgentTrace` | `false` | Agent Runtime streams | Aggregates trace usage and counts by default. When enabled, also attaches
|
|
106
|
+
| Option | Default | Applies to | Notes |
|
|
107
|
+
| ------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `captureAgentTrace` | `false` | Agent Runtime streams | Aggregates trace usage and counts by default. When enabled, also attaches trace events as `td.metadata.agent_trace`, filtered as described below. |
|
|
109
|
+
|
|
110
|
+
`td.metadata.agent_trace` is truncated but never passed to your `mask`, so each trace event is rebuilt from an explicit allowlist instead:
|
|
111
|
+
|
|
112
|
+
- Envelope fields: `agentId`, `agentAliasId`, `agentVersion`, `sessionId`, `collaboratorName`, `eventTime`, and the alias ARNs in `callerChain`.
|
|
113
|
+
- Content the agent sends to a model, tool, or guardrail (`modelInvocationInput`, `invocationInput`, `inputAssessments`) is kept as received when `captureInput` is on.
|
|
114
|
+
- Content that comes back (`modelInvocationOutput`, `rationale`, `observation`, `outputAssessments`, custom orchestration `event`) is kept as received when `captureOutput` is on.
|
|
115
|
+
- A failure reason (`failureTrace.failureReason`, and `td.metadata.failure_reason`) is free-form text, so it is kept only when both are on. `td.metadata.failure` always records that the invocation failed, with `td.metadata.failure_code` when AWS sends one.
|
|
116
|
+
- When a `mask` is set, all of this content is withheld.
|
|
117
|
+
- Withheld content and the trace structure around it keep only trace ids, step types, the foundation model, guardrail action, failure code, and the usage, timings, and `clientRequestId` from AWS's trace `metadata`.
|
|
118
|
+
|
|
119
|
+
Any other field, including unknown trace types and the AWS SDK's `$unknown` union member, is omitted even when both capture options are on; a content part that is kept is exported whole, including any fields inside it that are not listed here. Errors are not covered by this filtering: AWS exceptions, including errors raised inside the stream, still record the provider's error message on the span.
|
|
120
|
+
|
|
121
|
+
Output file names in `td.metadata.output_files` follow `captureOutput` the same way, and are withheld when a `mask` is set; file types are always kept.
|
|
104
122
|
|
|
105
123
|
## Signal coverage
|
|
106
124
|
|
|
@@ -159,3 +177,7 @@ The following calls pass through unless a supported operation above is used:
|
|
|
159
177
|
- `AgenticRetrieveStream`
|
|
160
178
|
|
|
161
179
|
Provider-native `InvokeModel` usage is best-effort. Anthropic Claude, Amazon Titan/Nova, Meta Llama, and Titan embeddings expose known token fields. Cohere and Mistral text models do not consistently include usage in the JSON body; spans still capture request/response bodies and finish reasons where present.
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT. See [LICENSE](https://github.com/telemetry-dev/telemetry/blob/main/packages/bedrock/LICENSE).
|
package/dist/agents.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as createInstrumentedSend, t as AGENT_HANDLERS } from "./internal-handlers-
|
|
1
|
+
import { r as createInstrumentedSend, t as AGENT_HANDLERS } from "./internal-handlers-CLP2wXDh.mjs";
|
|
2
2
|
import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
|
|
3
3
|
//#region src/agents.ts
|
|
4
4
|
const WRAPPED = /* @__PURE__ */ new WeakSet();
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as RUNTIME_HANDLERS, r as createInstrumentedSend } from "./internal-handlers-
|
|
1
|
+
import { n as RUNTIME_HANDLERS, r as createInstrumentedSend } from "./internal-handlers-CLP2wXDh.mjs";
|
|
2
2
|
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime";
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const WRAPPED = /* @__PURE__ */ new WeakSet();
|
|
@@ -413,6 +413,8 @@ var AgentStreamState = class {
|
|
|
413
413
|
textRejected = false;
|
|
414
414
|
usage = {};
|
|
415
415
|
traceEventCount = 0;
|
|
416
|
+
failed = false;
|
|
417
|
+
failureCode;
|
|
416
418
|
failureReason;
|
|
417
419
|
guardrailAction;
|
|
418
420
|
returnControl;
|
|
@@ -423,6 +425,10 @@ var AgentStreamState = class {
|
|
|
423
425
|
errorFields;
|
|
424
426
|
citationCount = 0;
|
|
425
427
|
outputFiles = [];
|
|
428
|
+
contentCapture = {
|
|
429
|
+
input: captureEnabled("input") && !captureMaskedBeforeStart(),
|
|
430
|
+
output: captureEnabled("output") && !captureMaskedBeforeStart()
|
|
431
|
+
};
|
|
426
432
|
constructor(options) {
|
|
427
433
|
this.options = options;
|
|
428
434
|
}
|
|
@@ -442,7 +448,7 @@ var AgentStreamState = class {
|
|
|
442
448
|
else this.textRejected = true;
|
|
443
449
|
const citations = arrayValue((isRecord(chunk?.attribution) ? chunk.attribution : void 0)?.citations);
|
|
444
450
|
if (citations) this.citationCount += citations.length;
|
|
445
|
-
const sanitizedFiles = (isRecord(raw.files) ? arrayValue(raw.files.files) : void 0)?.map(sanitizeOutputFile);
|
|
451
|
+
const sanitizedFiles = (isRecord(raw.files) ? arrayValue(raw.files.files) : void 0)?.map((file) => sanitizeOutputFile(file, this.contentCapture.output));
|
|
446
452
|
if (sanitizedFiles && this.retention.accept(sanitizedFiles)) this.outputFiles.push(...sanitizedFiles);
|
|
447
453
|
const returnControl = isRecord(raw.returnControl) ? raw.returnControl : void 0;
|
|
448
454
|
if (returnControl) {
|
|
@@ -457,11 +463,23 @@ var AgentStreamState = class {
|
|
|
457
463
|
const traceEvent = isRecord(raw.trace) ? raw.trace : void 0;
|
|
458
464
|
if (traceEvent) {
|
|
459
465
|
this.traceEventCount += 1;
|
|
460
|
-
if (this.options.captureAgentTrace
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
466
|
+
if (this.options.captureAgentTrace) {
|
|
467
|
+
const retained = projectTrace(traceEvent, TRACE_PART, this.contentCapture);
|
|
468
|
+
if (this.retention.accept(retained)) this.traces.push(retained);
|
|
469
|
+
}
|
|
470
|
+
const trace = isRecord(traceEvent.trace) ? traceEvent.trace : {};
|
|
471
|
+
for (const step of MODEL_INVOCATION_STEPS) {
|
|
472
|
+
const member = isRecord(trace[step]) ? trace[step] : void 0;
|
|
473
|
+
const output = isRecord(member?.modelInvocationOutput) ? member.modelInvocationOutput : {};
|
|
474
|
+
this.addUsage(isRecord(output.metadata) ? output.metadata.usage : void 0);
|
|
475
|
+
}
|
|
476
|
+
const failure = trace.failureTrace;
|
|
477
|
+
if (isRecord(failure)) {
|
|
478
|
+
this.failed = true;
|
|
479
|
+
this.failureCode = numberValue(failure.failureCode) ?? this.failureCode;
|
|
480
|
+
if (this.contentCapture.input && this.contentCapture.output) this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
|
|
481
|
+
}
|
|
482
|
+
const guardrail = trace.guardrailTrace;
|
|
465
483
|
if (isRecord(guardrail)) this.guardrailAction = stringValue(guardrail.action) ?? this.guardrailAction;
|
|
466
484
|
}
|
|
467
485
|
}
|
|
@@ -472,6 +490,8 @@ var AgentStreamState = class {
|
|
|
472
490
|
finish() {
|
|
473
491
|
const metadata = omitUndefined({
|
|
474
492
|
trace_event_count: this.traceEventCount || void 0,
|
|
493
|
+
failure: this.failed ? true : void 0,
|
|
494
|
+
failure_code: this.failureCode,
|
|
475
495
|
failure_reason: this.failureReason,
|
|
476
496
|
guardrail_action: this.guardrailAction,
|
|
477
497
|
return_control: this.sawReturnControl ? true : void 0,
|
|
@@ -504,11 +524,11 @@ var AgentStreamState = class {
|
|
|
504
524
|
else if (inputTokens !== void 0 || outputTokens !== void 0) this.usage.totalTokens = (this.usage.inputTokens ?? 0) + (this.usage.outputTokens ?? 0);
|
|
505
525
|
}
|
|
506
526
|
};
|
|
507
|
-
function sanitizeOutputFile(value) {
|
|
527
|
+
function sanitizeOutputFile(value, keepName) {
|
|
508
528
|
const raw = value;
|
|
509
529
|
if (!isRecord(raw)) return void 0;
|
|
510
530
|
return omitUndefined({
|
|
511
|
-
name: stringValue(raw.name),
|
|
531
|
+
name: keepName ? stringValue(raw.name) : void 0,
|
|
512
532
|
type: stringValue(raw.type)
|
|
513
533
|
});
|
|
514
534
|
}
|
|
@@ -700,22 +720,111 @@ function flowName(input) {
|
|
|
700
720
|
function ragName(input) {
|
|
701
721
|
return `retrieve_and_generate ${basenameArn(ragModel(input)) ?? "knowledge-base"}`;
|
|
702
722
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
for (const child of Object.values(raw)) if (isRecord(child)) collectTrace(child, onUsage);
|
|
709
|
-
else if (Array.isArray(child)) child.forEach((item) => collectTrace(item, onUsage));
|
|
710
|
-
}
|
|
711
|
-
function findKey(value, key) {
|
|
712
|
-
const raw = value;
|
|
713
|
-
if (!isRecord(raw)) return void 0;
|
|
714
|
-
if (raw[key] !== void 0) return raw[key];
|
|
715
|
-
for (const child of Object.values(raw)) {
|
|
716
|
-
const found = findKey(child, key);
|
|
717
|
-
if (found !== void 0) return found;
|
|
723
|
+
/** Content kept as received when every listed capture is on; otherwise reduced to `withheld`. */
|
|
724
|
+
var TraceContent = class {
|
|
725
|
+
constructor(needs, withheld) {
|
|
726
|
+
this.needs = needs;
|
|
727
|
+
this.withheld = withheld;
|
|
718
728
|
}
|
|
729
|
+
};
|
|
730
|
+
const SCALAR = "scalar";
|
|
731
|
+
const TRACE_METADATA = {
|
|
732
|
+
usage: {
|
|
733
|
+
inputTokens: SCALAR,
|
|
734
|
+
outputTokens: SCALAR
|
|
735
|
+
},
|
|
736
|
+
startTime: SCALAR,
|
|
737
|
+
endTime: SCALAR,
|
|
738
|
+
totalTimeMs: SCALAR,
|
|
739
|
+
operationTotalTimeMs: SCALAR,
|
|
740
|
+
clientRequestId: SCALAR
|
|
741
|
+
};
|
|
742
|
+
const MODEL_INVOCATION_INPUT = new TraceContent(["input"], {
|
|
743
|
+
traceId: SCALAR,
|
|
744
|
+
type: SCALAR,
|
|
745
|
+
foundationModel: SCALAR
|
|
746
|
+
});
|
|
747
|
+
const INVOCATION_INPUT = new TraceContent(["input"], {
|
|
748
|
+
traceId: SCALAR,
|
|
749
|
+
invocationType: SCALAR
|
|
750
|
+
});
|
|
751
|
+
const MODEL_INVOCATION_OUTPUT = new TraceContent(["output"], {
|
|
752
|
+
traceId: SCALAR,
|
|
753
|
+
metadata: TRACE_METADATA
|
|
754
|
+
});
|
|
755
|
+
const OBSERVATION = new TraceContent(["output"], {
|
|
756
|
+
traceId: SCALAR,
|
|
757
|
+
type: SCALAR
|
|
758
|
+
});
|
|
759
|
+
const MODEL_INVOCATION_STEPS = [
|
|
760
|
+
"preProcessingTrace",
|
|
761
|
+
"orchestrationTrace",
|
|
762
|
+
"postProcessingTrace",
|
|
763
|
+
"routingClassifierTrace"
|
|
764
|
+
];
|
|
765
|
+
const PROCESSING_TRACE = {
|
|
766
|
+
modelInvocationInput: MODEL_INVOCATION_INPUT,
|
|
767
|
+
modelInvocationOutput: MODEL_INVOCATION_OUTPUT
|
|
768
|
+
};
|
|
769
|
+
const TRACE_PART = {
|
|
770
|
+
agentId: SCALAR,
|
|
771
|
+
agentAliasId: SCALAR,
|
|
772
|
+
agentVersion: SCALAR,
|
|
773
|
+
sessionId: SCALAR,
|
|
774
|
+
collaboratorName: SCALAR,
|
|
775
|
+
eventTime: SCALAR,
|
|
776
|
+
callerChain: [{ agentAliasArn: SCALAR }],
|
|
777
|
+
trace: {
|
|
778
|
+
preProcessingTrace: PROCESSING_TRACE,
|
|
779
|
+
postProcessingTrace: PROCESSING_TRACE,
|
|
780
|
+
orchestrationTrace: {
|
|
781
|
+
modelInvocationInput: MODEL_INVOCATION_INPUT,
|
|
782
|
+
invocationInput: INVOCATION_INPUT,
|
|
783
|
+
modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
|
|
784
|
+
rationale: new TraceContent(["output"], { traceId: SCALAR }),
|
|
785
|
+
observation: OBSERVATION
|
|
786
|
+
},
|
|
787
|
+
routingClassifierTrace: {
|
|
788
|
+
modelInvocationInput: MODEL_INVOCATION_INPUT,
|
|
789
|
+
invocationInput: INVOCATION_INPUT,
|
|
790
|
+
modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
|
|
791
|
+
observation: OBSERVATION
|
|
792
|
+
},
|
|
793
|
+
customOrchestrationTrace: {
|
|
794
|
+
traceId: SCALAR,
|
|
795
|
+
event: new TraceContent(["output"])
|
|
796
|
+
},
|
|
797
|
+
guardrailTrace: {
|
|
798
|
+
traceId: SCALAR,
|
|
799
|
+
action: SCALAR,
|
|
800
|
+
metadata: TRACE_METADATA,
|
|
801
|
+
inputAssessments: new TraceContent(["input"]),
|
|
802
|
+
outputAssessments: new TraceContent(["output"])
|
|
803
|
+
},
|
|
804
|
+
failureTrace: {
|
|
805
|
+
traceId: SCALAR,
|
|
806
|
+
failureCode: SCALAR,
|
|
807
|
+
metadata: TRACE_METADATA,
|
|
808
|
+
failureReason: new TraceContent(["input", "output"])
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
function projectTrace(value, shape, capture) {
|
|
813
|
+
if (shape instanceof TraceContent) {
|
|
814
|
+
if (shape.needs.every((kind) => capture[kind])) return value ?? void 0;
|
|
815
|
+
return shape.withheld === void 0 ? void 0 : projectTrace(value, shape.withheld, capture);
|
|
816
|
+
}
|
|
817
|
+
if (shape === SCALAR) return [
|
|
818
|
+
"string",
|
|
819
|
+
"number",
|
|
820
|
+
"boolean"
|
|
821
|
+
].includes(typeof value) || value instanceof Date ? value : void 0;
|
|
822
|
+
if (Array.isArray(shape)) {
|
|
823
|
+
if (!Array.isArray(value)) return void 0;
|
|
824
|
+
return value.map((item) => projectTrace(item, shape[0], capture)).filter((item) => item !== void 0);
|
|
825
|
+
}
|
|
826
|
+
if (!isRecord(value)) return void 0;
|
|
827
|
+
return omitUndefined(Object.fromEntries(Object.entries(shape).map(([field, child]) => [field, projectTrace(value[field], child, capture)])));
|
|
719
828
|
}
|
|
720
829
|
//#endregion
|
|
721
830
|
//#region src/converse.ts
|
|
@@ -1147,12 +1256,7 @@ function nativeResponseFields(model, body, metadata) {
|
|
|
1147
1256
|
return withHeaderUsage({
|
|
1148
1257
|
output: body,
|
|
1149
1258
|
finishReason: stringValue(body.stop_reason),
|
|
1150
|
-
usage
|
|
1151
|
-
inputTokens: numberValue(usage.input_tokens),
|
|
1152
|
-
outputTokens: numberValue(usage.output_tokens),
|
|
1153
|
-
cacheReadInputTokens: numberValue(usage.cache_read_input_tokens),
|
|
1154
|
-
cacheCreationInputTokens: numberValue(usage.cache_creation_input_tokens)
|
|
1155
|
-
})
|
|
1259
|
+
...claudeUsageFields(usage)
|
|
1156
1260
|
}, metadata);
|
|
1157
1261
|
}
|
|
1158
1262
|
if (lower.includes("meta.llama")) return omitUndefined({
|
|
@@ -1177,6 +1281,35 @@ function nativeResponseFields(model, body, metadata) {
|
|
|
1177
1281
|
}, metadata);
|
|
1178
1282
|
return withHeaderUsage({ output: body }, metadata);
|
|
1179
1283
|
}
|
|
1284
|
+
const CLAUDE_USAGE_KEYS = {
|
|
1285
|
+
inputTokens: "input_tokens",
|
|
1286
|
+
outputTokens: "output_tokens",
|
|
1287
|
+
cacheReadInputTokens: "cache_read_input_tokens",
|
|
1288
|
+
cacheCreationInputTokens: "cache_creation_input_tokens"
|
|
1289
|
+
};
|
|
1290
|
+
function claudeUsageFields(record) {
|
|
1291
|
+
const usage = {};
|
|
1292
|
+
for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
|
|
1293
|
+
const value = numberValue(record[source]);
|
|
1294
|
+
if (value !== void 0) usage[key] = value;
|
|
1295
|
+
}
|
|
1296
|
+
const iterations = record.iterations;
|
|
1297
|
+
if (!Array.isArray(iterations)) return { usage };
|
|
1298
|
+
const first = Math.max(0, iterations.length - STREAM_CAPTURE_MAX_ITEMS);
|
|
1299
|
+
for (let index = first; index < iterations.length; index += 1) {
|
|
1300
|
+
const entry = iterations[index];
|
|
1301
|
+
if (!isRecord(entry) || entry.type !== "compaction") continue;
|
|
1302
|
+
for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
|
|
1303
|
+
const value = numberValue(entry[source]);
|
|
1304
|
+
const target = key;
|
|
1305
|
+
if (value !== void 0) usage[target] = (usage[target] ?? 0) + value;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return {
|
|
1309
|
+
usage,
|
|
1310
|
+
...first > 0 ? { attributes: { "telemetry.dev.capture.truncated": true } } : {}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1180
1313
|
function withHeaderUsage(fields, metadata) {
|
|
1181
1314
|
if (fields.usage && Object.keys(fields.usage).length > 0) return fields;
|
|
1182
1315
|
return mergeFields(fields, headerUsageFields(metadata));
|
|
@@ -1202,6 +1335,9 @@ var InvokeModelStreamState = class {
|
|
|
1202
1335
|
chunks = [];
|
|
1203
1336
|
text = "";
|
|
1204
1337
|
usage;
|
|
1338
|
+
finalUsage = {};
|
|
1339
|
+
startUsage = {};
|
|
1340
|
+
compaction = {};
|
|
1205
1341
|
finishReason;
|
|
1206
1342
|
error;
|
|
1207
1343
|
errorFields;
|
|
@@ -1228,21 +1364,44 @@ var InvokeModelStreamState = class {
|
|
|
1228
1364
|
const message = isRecord(parsed.message) ? parsed.message : void 0;
|
|
1229
1365
|
const messageUsage = isRecord(message?.usage) ? message?.usage : void 0;
|
|
1230
1366
|
const usage = isRecord(parsed.usage) ? parsed.usage : void 0;
|
|
1367
|
+
for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
|
|
1368
|
+
const value = numberValue(usage?.[source]);
|
|
1369
|
+
if (value !== void 0) this.finalUsage[key] = value;
|
|
1370
|
+
}
|
|
1371
|
+
this.startUsage = omitUndefined({
|
|
1372
|
+
...this.startUsage,
|
|
1373
|
+
inputTokens: numberValue(messageUsage?.input_tokens) ?? this.startUsage.inputTokens,
|
|
1374
|
+
outputTokens: numberValue(messageUsage?.output_tokens) ?? this.startUsage.outputTokens,
|
|
1375
|
+
cacheReadInputTokens: numberValue(messageUsage?.cache_read_input_tokens) ?? this.startUsage.cacheReadInputTokens,
|
|
1376
|
+
cacheCreationInputTokens: numberValue(messageUsage?.cache_creation_input_tokens) ?? this.startUsage.cacheCreationInputTokens
|
|
1377
|
+
});
|
|
1378
|
+
const iterations = Array.isArray(usage?.iterations) ? usage.iterations : messageUsage?.iterations;
|
|
1379
|
+
if (Array.isArray(iterations)) this.compaction = claudeUsageFields({ iterations });
|
|
1231
1380
|
const metrics = isRecord(parsed["amazon-bedrock-invocationMetrics"]) ? parsed["amazon-bedrock-invocationMetrics"] : void 0;
|
|
1232
1381
|
const outputs = Array.isArray(parsed.outputs) && isRecord(parsed.outputs[0]) ? parsed.outputs[0] : void 0;
|
|
1233
1382
|
const generation = Array.isArray(parsed.generations) && isRecord(parsed.generations[0]) ? parsed.generations[0] : void 0;
|
|
1234
1383
|
this.finishReason = stringValue(delta?.stop_reason) ?? stringValue(parsed.stop_reason) ?? stringValue(outputs?.stop_reason) ?? stringValue(generation?.finish_reason) ?? stringValue(parsed.completionReason) ?? this.finishReason;
|
|
1235
1384
|
this.usage = omitUndefined({
|
|
1236
1385
|
...this.usage,
|
|
1237
|
-
inputTokens: numberValue(
|
|
1386
|
+
inputTokens: numberValue(usage?.input_tokens) ?? numberValue(metrics?.inputTokenCount) ?? numberValue(parsed.inputTextTokenCount) ?? this.usage?.inputTokens,
|
|
1238
1387
|
outputTokens: numberValue(usage?.output_tokens) ?? numberValue(metrics?.outputTokenCount) ?? numberValue(parsed.totalOutputTextTokenCount) ?? this.usage?.outputTokens,
|
|
1239
|
-
cacheReadInputTokens: numberValue(
|
|
1240
|
-
cacheCreationInputTokens: numberValue(
|
|
1388
|
+
cacheReadInputTokens: numberValue(usage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
|
|
1389
|
+
cacheCreationInputTokens: numberValue(usage?.cache_creation_input_tokens) ?? this.usage?.cacheCreationInputTokens
|
|
1241
1390
|
});
|
|
1242
1391
|
}
|
|
1243
1392
|
return providerChunkHasOutput(parsed);
|
|
1244
1393
|
}
|
|
1245
1394
|
finish() {
|
|
1395
|
+
const usage = {
|
|
1396
|
+
...this.startUsage,
|
|
1397
|
+
...this.usage,
|
|
1398
|
+
...this.finalUsage
|
|
1399
|
+
};
|
|
1400
|
+
for (const key of Object.keys(CLAUDE_USAGE_KEYS)) {
|
|
1401
|
+
const value = this.compaction.usage?.[key];
|
|
1402
|
+
const baseKey = key === "outputTokens" ? "outputTokens" : "inputTokens";
|
|
1403
|
+
if (value !== void 0 && (this.finalUsage[baseKey] !== void 0 || this.usage?.[baseKey] === void 0)) usage[key] = (usage[key] ?? 0) + value;
|
|
1404
|
+
}
|
|
1246
1405
|
return mergeFields(omitUndefined({
|
|
1247
1406
|
output: this.text ? [{
|
|
1248
1407
|
role: "assistant",
|
|
@@ -1253,7 +1412,8 @@ var InvokeModelStreamState = class {
|
|
|
1253
1412
|
finish_reason: this.finishReason
|
|
1254
1413
|
}] : this.chunks,
|
|
1255
1414
|
finishReason: this.finishReason,
|
|
1256
|
-
usage
|
|
1415
|
+
usage,
|
|
1416
|
+
attributes: this.compaction.attributes,
|
|
1257
1417
|
error: this.error
|
|
1258
1418
|
}), this.errorFields);
|
|
1259
1419
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/bedrock",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "AWS Bedrock instrumentation for telemetry.dev: Converse, InvokeModel, and Bedrock Agents spans for AWS SDK v3 clients.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aws",
|
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
"@aws-sdk/client-bedrock-agent-runtime": "3.1135.0",
|
|
45
45
|
"@aws-sdk/client-bedrock-runtime": "3.1135.0",
|
|
46
46
|
"@opentelemetry/api": "^1.9.1",
|
|
47
|
-
"@opentelemetry/sdk-logs": "^0.
|
|
48
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
47
|
+
"@opentelemetry/sdk-logs": "^0.223.0",
|
|
48
|
+
"@opentelemetry/sdk-trace-base": "^2.12.0",
|
|
49
49
|
"@smithy/eventstream-codec": "^4.2.5",
|
|
50
50
|
"@smithy/protocol-http": "^5.3.10",
|
|
51
51
|
"@smithy/types": "^4.12.0",
|
|
52
|
-
"@telemetry-dev/sdk": "^0.1.
|
|
52
|
+
"@telemetry-dev/sdk": "^0.1.11",
|
|
53
53
|
"@types/node": "^25.5.0",
|
|
54
54
|
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
55
55
|
"typescript": "^6.0.2",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@aws-sdk/client-bedrock-agent-runtime": ">=3.600.0 <4",
|
|
61
61
|
"@aws-sdk/client-bedrock-runtime": ">=3.600.0 <4",
|
|
62
|
-
"@telemetry-dev/sdk": "^0.1.
|
|
62
|
+
"@telemetry-dev/sdk": "^0.1.11"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"@aws-sdk/client-bedrock-agent-runtime": {
|
package/src/agent-handlers.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
awsMetadataFields,
|
|
6
6
|
basenameArn,
|
|
7
7
|
bytesToString,
|
|
8
|
+
captureMaskedBeforeStart,
|
|
8
9
|
isRecord,
|
|
9
10
|
mergeFields,
|
|
10
11
|
modeledStreamError,
|
|
@@ -55,6 +56,8 @@ export class AgentStreamState implements StreamState {
|
|
|
55
56
|
private textRejected = false;
|
|
56
57
|
private usage: NonNullable<SpanFields["usage"]> = {};
|
|
57
58
|
private traceEventCount = 0;
|
|
59
|
+
private failed = false;
|
|
60
|
+
private failureCode: number | undefined;
|
|
58
61
|
private failureReason: string | undefined;
|
|
59
62
|
private guardrailAction: string | undefined;
|
|
60
63
|
private returnControl: unknown;
|
|
@@ -65,6 +68,12 @@ export class AgentStreamState implements StreamState {
|
|
|
65
68
|
private errorFields: SpanFields | undefined;
|
|
66
69
|
private citationCount = 0;
|
|
67
70
|
private readonly outputFiles: unknown[] = [];
|
|
71
|
+
// Metadata bypasses the capture flags and the mask, so trace and file content is filtered here,
|
|
72
|
+
// and a mask that cannot see it withholds it.
|
|
73
|
+
private readonly contentCapture: AgentTraceCapture = {
|
|
74
|
+
input: captureEnabled("input") && !captureMaskedBeforeStart(),
|
|
75
|
+
output: captureEnabled("output") && !captureMaskedBeforeStart(),
|
|
76
|
+
};
|
|
68
77
|
|
|
69
78
|
constructor(private readonly options: BedrockInstrumentationOptions) {}
|
|
70
79
|
|
|
@@ -90,6 +99,7 @@ export class AgentStreamState implements StreamState {
|
|
|
90
99
|
if (this.retention.accept(piece)) this.text += piece;
|
|
91
100
|
else this.textRejected = true;
|
|
92
101
|
}
|
|
102
|
+
|
|
93
103
|
const attribution = isRecord(chunk?.attribution) ? chunk.attribution : undefined;
|
|
94
104
|
const citations = arrayValue<unknown>(attribution?.citations);
|
|
95
105
|
|
|
@@ -97,7 +107,9 @@ export class AgentStreamState implements StreamState {
|
|
|
97
107
|
|
|
98
108
|
const files = isRecord(raw.files) ? arrayValue<JsonValue>(raw.files.files) : undefined;
|
|
99
109
|
|
|
100
|
-
const sanitizedFiles = files?.map(
|
|
110
|
+
const sanitizedFiles = files?.map((file) =>
|
|
111
|
+
sanitizeOutputFile(file, this.contentCapture.output),
|
|
112
|
+
);
|
|
101
113
|
|
|
102
114
|
if (sanitizedFiles && this.retention.accept(sanitizedFiles)) {
|
|
103
115
|
this.outputFiles.push(...sanitizedFiles);
|
|
@@ -125,15 +137,35 @@ export class AgentStreamState implements StreamState {
|
|
|
125
137
|
if (traceEvent) {
|
|
126
138
|
this.traceEventCount += 1;
|
|
127
139
|
|
|
128
|
-
if (this.options.captureAgentTrace
|
|
129
|
-
this.
|
|
140
|
+
if (this.options.captureAgentTrace) {
|
|
141
|
+
const retained = projectTrace(traceEvent, TRACE_PART, this.contentCapture);
|
|
142
|
+
|
|
143
|
+
if (this.retention.accept(retained)) this.traces.push(retained);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Summaries are read only where the API puts them, so content and unknown branches can't
|
|
147
|
+
// feed them.
|
|
148
|
+
const trace = isRecord(traceEvent.trace) ? traceEvent.trace : {};
|
|
149
|
+
|
|
150
|
+
for (const step of MODEL_INVOCATION_STEPS) {
|
|
151
|
+
const member = isRecord(trace[step]) ? trace[step] : undefined;
|
|
152
|
+
const output = isRecord(member?.modelInvocationOutput) ? member.modelInvocationOutput : {};
|
|
153
|
+
this.addUsage(isRecord(output.metadata) ? output.metadata.usage : undefined);
|
|
130
154
|
}
|
|
131
|
-
collectTrace(traceEvent, (usage) => this.addUsage(usage));
|
|
132
|
-
const failure = findKey(traceEvent, "failureTrace");
|
|
133
155
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
const failure = trace.failureTrace;
|
|
157
|
+
|
|
158
|
+
if (isRecord(failure)) {
|
|
159
|
+
this.failed = true;
|
|
160
|
+
this.failureCode = numberValue(failure.failureCode) ?? this.failureCode;
|
|
161
|
+
|
|
162
|
+
// The reason is free-form text that can quote the prompt or a tool result.
|
|
163
|
+
if (this.contentCapture.input && this.contentCapture.output) {
|
|
164
|
+
this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const guardrail = trace.guardrailTrace;
|
|
137
169
|
|
|
138
170
|
if (isRecord(guardrail))
|
|
139
171
|
this.guardrailAction = stringValue(guardrail.action) ?? this.guardrailAction;
|
|
@@ -150,6 +182,8 @@ export class AgentStreamState implements StreamState {
|
|
|
150
182
|
finish(): SpanFields {
|
|
151
183
|
const metadata = omitUndefined({
|
|
152
184
|
trace_event_count: this.traceEventCount || undefined,
|
|
185
|
+
failure: this.failed ? true : undefined,
|
|
186
|
+
failure_code: this.failureCode,
|
|
153
187
|
failure_reason: this.failureReason,
|
|
154
188
|
guardrail_action: this.guardrailAction,
|
|
155
189
|
return_control: this.sawReturnControl ? true : undefined,
|
|
@@ -195,13 +229,13 @@ export class AgentStreamState implements StreamState {
|
|
|
195
229
|
}
|
|
196
230
|
}
|
|
197
231
|
|
|
198
|
-
function sanitizeOutputFile<T>(value: T): JsonValue {
|
|
232
|
+
function sanitizeOutputFile<T>(value: T, keepName: boolean): JsonValue {
|
|
199
233
|
const raw: unknown = value;
|
|
200
234
|
|
|
201
235
|
if (!isRecord(raw)) return undefined;
|
|
202
236
|
|
|
203
237
|
return omitUndefined({
|
|
204
|
-
name: stringValue(raw.name),
|
|
238
|
+
name: keepName ? stringValue(raw.name) : undefined,
|
|
205
239
|
type: stringValue(raw.type),
|
|
206
240
|
});
|
|
207
241
|
}
|
|
@@ -426,6 +460,7 @@ export class FlowStreamState implements StreamState {
|
|
|
426
460
|
if (inputRequest && this.retention.accept(inputRequest.content)) {
|
|
427
461
|
this.outputs.push(inputRequest.content);
|
|
428
462
|
}
|
|
463
|
+
|
|
429
464
|
const completion = isRecord(raw.flowCompletionEvent) ? raw.flowCompletionEvent : undefined;
|
|
430
465
|
this.completionReason = stringValue(completion?.completionReason) ?? this.completionReason;
|
|
431
466
|
}
|
|
@@ -450,35 +485,135 @@ export function ragName(input: JsonRecord): string {
|
|
|
450
485
|
return `retrieve_and_generate ${basenameArn(ragModel(input)) ?? "knowledge-base"}`;
|
|
451
486
|
}
|
|
452
487
|
|
|
453
|
-
|
|
454
|
-
const raw: unknown = value;
|
|
488
|
+
type AgentTraceCapture = Record<"input" | "output", boolean>;
|
|
455
489
|
|
|
456
|
-
|
|
457
|
-
|
|
490
|
+
/** Content kept as received when every listed capture is on; otherwise reduced to `withheld`. */
|
|
491
|
+
class TraceContent {
|
|
492
|
+
constructor(
|
|
493
|
+
readonly needs: ReadonlyArray<keyof AgentTraceCapture>,
|
|
494
|
+
readonly withheld?: TraceShape,
|
|
495
|
+
) {}
|
|
496
|
+
}
|
|
458
497
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
498
|
+
const SCALAR = "scalar";
|
|
499
|
+
|
|
500
|
+
// A record shape lists the only fields kept; a one-item array shape is a list of that shape.
|
|
501
|
+
type TraceShape =
|
|
502
|
+
| typeof SCALAR
|
|
503
|
+
| TraceContent
|
|
504
|
+
| readonly [TraceShape]
|
|
505
|
+
| { readonly [field: string]: TraceShape };
|
|
506
|
+
|
|
507
|
+
const TRACE_METADATA: TraceShape = {
|
|
508
|
+
usage: { inputTokens: SCALAR, outputTokens: SCALAR },
|
|
509
|
+
startTime: SCALAR,
|
|
510
|
+
endTime: SCALAR,
|
|
511
|
+
totalTimeMs: SCALAR,
|
|
512
|
+
operationTotalTimeMs: SCALAR,
|
|
513
|
+
clientRequestId: SCALAR,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// What the agent sends to a model, tool, or guardrail is input content; what comes back is output.
|
|
517
|
+
const MODEL_INVOCATION_INPUT = new TraceContent(["input"], {
|
|
518
|
+
traceId: SCALAR,
|
|
519
|
+
type: SCALAR,
|
|
520
|
+
foundationModel: SCALAR,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const INVOCATION_INPUT = new TraceContent(["input"], { traceId: SCALAR, invocationType: SCALAR });
|
|
524
|
+
|
|
525
|
+
const MODEL_INVOCATION_OUTPUT = new TraceContent(["output"], {
|
|
526
|
+
traceId: SCALAR,
|
|
527
|
+
metadata: TRACE_METADATA,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const OBSERVATION = new TraceContent(["output"], { traceId: SCALAR, type: SCALAR });
|
|
531
|
+
|
|
532
|
+
const MODEL_INVOCATION_STEPS = [
|
|
533
|
+
"preProcessingTrace",
|
|
534
|
+
"orchestrationTrace",
|
|
535
|
+
"postProcessingTrace",
|
|
536
|
+
"routingClassifierTrace",
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
const PROCESSING_TRACE: TraceShape = {
|
|
540
|
+
modelInvocationInput: MODEL_INVOCATION_INPUT,
|
|
541
|
+
modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Mirrors TracePart and the Trace union in the Bedrock Agent Runtime API; anything else, including
|
|
545
|
+
// the AWS SDK's `$unknown` union member, is dropped because nothing says what it holds.
|
|
546
|
+
const TRACE_PART: TraceShape = {
|
|
547
|
+
agentId: SCALAR,
|
|
548
|
+
agentAliasId: SCALAR,
|
|
549
|
+
agentVersion: SCALAR,
|
|
550
|
+
sessionId: SCALAR,
|
|
551
|
+
collaboratorName: SCALAR,
|
|
552
|
+
eventTime: SCALAR,
|
|
553
|
+
callerChain: [{ agentAliasArn: SCALAR }],
|
|
554
|
+
trace: {
|
|
555
|
+
preProcessingTrace: PROCESSING_TRACE,
|
|
556
|
+
postProcessingTrace: PROCESSING_TRACE,
|
|
557
|
+
orchestrationTrace: {
|
|
558
|
+
modelInvocationInput: MODEL_INVOCATION_INPUT,
|
|
559
|
+
invocationInput: INVOCATION_INPUT,
|
|
560
|
+
modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
|
|
561
|
+
rationale: new TraceContent(["output"], { traceId: SCALAR }),
|
|
562
|
+
observation: OBSERVATION,
|
|
563
|
+
},
|
|
564
|
+
routingClassifierTrace: {
|
|
565
|
+
modelInvocationInput: MODEL_INVOCATION_INPUT,
|
|
566
|
+
invocationInput: INVOCATION_INPUT,
|
|
567
|
+
modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
|
|
568
|
+
observation: OBSERVATION,
|
|
569
|
+
},
|
|
570
|
+
customOrchestrationTrace: { traceId: SCALAR, event: new TraceContent(["output"]) },
|
|
571
|
+
guardrailTrace: {
|
|
572
|
+
traceId: SCALAR,
|
|
573
|
+
action: SCALAR,
|
|
574
|
+
metadata: TRACE_METADATA,
|
|
575
|
+
inputAssessments: new TraceContent(["input"]),
|
|
576
|
+
outputAssessments: new TraceContent(["output"]),
|
|
577
|
+
},
|
|
578
|
+
failureTrace: {
|
|
579
|
+
traceId: SCALAR,
|
|
580
|
+
failureCode: SCALAR,
|
|
581
|
+
metadata: TRACE_METADATA,
|
|
582
|
+
// Free-form text that can quote either the prompt or a tool result.
|
|
583
|
+
failureReason: new TraceContent(["input", "output"]),
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
};
|
|
463
587
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
}
|
|
588
|
+
function projectTrace(value: JsonValue, shape: TraceShape, capture: AgentTraceCapture): JsonValue {
|
|
589
|
+
if (shape instanceof TraceContent) {
|
|
590
|
+
if (shape.needs.every((kind) => capture[kind])) return value ?? undefined;
|
|
469
591
|
|
|
470
|
-
|
|
471
|
-
|
|
592
|
+
return shape.withheld === undefined ? undefined : projectTrace(value, shape.withheld, capture);
|
|
593
|
+
}
|
|
472
594
|
|
|
473
|
-
if (
|
|
595
|
+
if (shape === SCALAR) {
|
|
596
|
+
const scalar = ["string", "number", "boolean"].includes(typeof value) || value instanceof Date;
|
|
474
597
|
|
|
475
|
-
|
|
598
|
+
return scalar ? value : undefined;
|
|
599
|
+
}
|
|
476
600
|
|
|
477
|
-
|
|
478
|
-
|
|
601
|
+
if (Array.isArray(shape)) {
|
|
602
|
+
if (!Array.isArray(value)) return undefined;
|
|
479
603
|
|
|
480
|
-
|
|
604
|
+
return value
|
|
605
|
+
.map((item) => projectTrace(item, shape[0], capture))
|
|
606
|
+
.filter((item) => item !== undefined);
|
|
481
607
|
}
|
|
482
608
|
|
|
483
|
-
return undefined;
|
|
609
|
+
if (!isRecord(value)) return undefined;
|
|
610
|
+
|
|
611
|
+
return omitUndefined(
|
|
612
|
+
Object.fromEntries(
|
|
613
|
+
Object.entries(shape).map(([field, child]) => [
|
|
614
|
+
field,
|
|
615
|
+
projectTrace(value[field], child, capture),
|
|
616
|
+
]),
|
|
617
|
+
),
|
|
618
|
+
);
|
|
484
619
|
}
|
package/src/internal.ts
CHANGED
|
@@ -182,7 +182,7 @@ function isCut(fields: SpanFields | undefined): boolean {
|
|
|
182
182
|
|
|
183
183
|
// Cores older than captureMasked expose no mask policy before a span starts; treat them as
|
|
184
184
|
// masked, like LEGACY_CAPTURE_POLICY. A namespace read keeps them loadable without the export.
|
|
185
|
-
function captureMaskedBeforeStart(): boolean {
|
|
185
|
+
export function captureMaskedBeforeStart(): boolean {
|
|
186
186
|
const read = (telemetrySdk as { captureMasked?: () => boolean }).captureMasked;
|
|
187
187
|
|
|
188
188
|
try {
|
|
@@ -351,7 +351,7 @@ export function parseJson<T>(value: T): JsonValue {
|
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
// Retention uses fixed bounds; the core applies maxAttributeLength after the mask.
|
|
354
|
-
|
|
354
|
+
const STREAM_CAPTURE_MAX_BYTES = 48 * 1024;
|
|
355
355
|
export const STREAM_CAPTURE_MAX_ITEMS = 1_000;
|
|
356
356
|
const CAPTURE_TRUNCATED_ATTRIBUTE = "telemetry.dev.capture.truncated";
|
|
357
357
|
|
package/src/invoke-model.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
omitUndefined,
|
|
11
11
|
parseJson,
|
|
12
12
|
PROVIDER,
|
|
13
|
+
STREAM_CAPTURE_MAX_ITEMS,
|
|
13
14
|
stringValue,
|
|
14
15
|
StreamRetention,
|
|
15
16
|
type JsonRecord,
|
|
@@ -165,12 +166,7 @@ function nativeResponseFields(
|
|
|
165
166
|
{
|
|
166
167
|
output: body,
|
|
167
168
|
finishReason: stringValue(body.stop_reason),
|
|
168
|
-
usage
|
|
169
|
-
inputTokens: numberValue(usage.input_tokens),
|
|
170
|
-
outputTokens: numberValue(usage.output_tokens),
|
|
171
|
-
cacheReadInputTokens: numberValue(usage.cache_read_input_tokens),
|
|
172
|
-
cacheCreationInputTokens: numberValue(usage.cache_creation_input_tokens),
|
|
173
|
-
}),
|
|
169
|
+
...claudeUsageFields(usage),
|
|
174
170
|
},
|
|
175
171
|
metadata,
|
|
176
172
|
);
|
|
@@ -216,6 +212,39 @@ function nativeResponseFields(
|
|
|
216
212
|
return withHeaderUsage({ output: body }, metadata);
|
|
217
213
|
}
|
|
218
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
|
+
|
|
219
248
|
function withHeaderUsage<T>(fields: SpanFields, metadata: T): SpanFields {
|
|
220
249
|
if (fields.usage && Object.keys(fields.usage).length > 0) return fields;
|
|
221
250
|
|
|
@@ -256,6 +285,9 @@ export class InvokeModelStreamState implements StreamState {
|
|
|
256
285
|
private readonly chunks: unknown[] = [];
|
|
257
286
|
private text = "";
|
|
258
287
|
private usage: SpanFields["usage"];
|
|
288
|
+
private finalUsage: NonNullable<SpanFields["usage"]> = {};
|
|
289
|
+
private startUsage: NonNullable<SpanFields["usage"]> = {};
|
|
290
|
+
private compaction: SpanFields = {};
|
|
259
291
|
private finishReason: string | undefined;
|
|
260
292
|
private error: Error | undefined;
|
|
261
293
|
private errorFields: SpanFields | undefined;
|
|
@@ -290,6 +322,28 @@ export class InvokeModelStreamState implements StreamState {
|
|
|
290
322
|
const messageUsage = isRecord(message?.usage) ? message?.usage : undefined;
|
|
291
323
|
const usage = isRecord(parsed.usage) ? parsed.usage : undefined;
|
|
292
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
|
+
|
|
293
347
|
const metrics = isRecord(parsed["amazon-bedrock-invocationMetrics"])
|
|
294
348
|
? parsed["amazon-bedrock-invocationMetrics"]
|
|
295
349
|
: undefined;
|
|
@@ -314,7 +368,7 @@ export class InvokeModelStreamState implements StreamState {
|
|
|
314
368
|
this.usage = omitUndefined({
|
|
315
369
|
...this.usage,
|
|
316
370
|
inputTokens:
|
|
317
|
-
numberValue(
|
|
371
|
+
numberValue(usage?.input_tokens) ??
|
|
318
372
|
numberValue(metrics?.inputTokenCount) ??
|
|
319
373
|
numberValue(parsed.inputTextTokenCount) ??
|
|
320
374
|
this.usage?.inputTokens,
|
|
@@ -324,10 +378,9 @@ export class InvokeModelStreamState implements StreamState {
|
|
|
324
378
|
numberValue(parsed.totalOutputTextTokenCount) ??
|
|
325
379
|
this.usage?.outputTokens,
|
|
326
380
|
cacheReadInputTokens:
|
|
327
|
-
numberValue(
|
|
381
|
+
numberValue(usage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
|
|
328
382
|
cacheCreationInputTokens:
|
|
329
|
-
numberValue(
|
|
330
|
-
this.usage?.cacheCreationInputTokens,
|
|
383
|
+
numberValue(usage?.cache_creation_input_tokens) ?? this.usage?.cacheCreationInputTokens,
|
|
331
384
|
});
|
|
332
385
|
}
|
|
333
386
|
|
|
@@ -335,6 +388,18 @@ export class InvokeModelStreamState implements StreamState {
|
|
|
335
388
|
}
|
|
336
389
|
|
|
337
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
|
+
}
|
|
338
403
|
return mergeFields(
|
|
339
404
|
omitUndefined({
|
|
340
405
|
output: this.text
|
|
@@ -347,7 +412,8 @@ export class InvokeModelStreamState implements StreamState {
|
|
|
347
412
|
]
|
|
348
413
|
: this.chunks,
|
|
349
414
|
finishReason: this.finishReason,
|
|
350
|
-
usage
|
|
415
|
+
usage,
|
|
416
|
+
attributes: this.compaction.attributes,
|
|
351
417
|
error: this.error,
|
|
352
418
|
}),
|
|
353
419
|
this.errorFields,
|