Skip to content

Commit 7c0cdeb

Browse files
authored
Fix Bedrock instrumentation TextDecoder error with robust type handling (#235)
*Issue #, if available:* *Description of changes:* OTel Instrumentation throws TextDecoder error on `BedrockRuntime.InvokeModelWithResponseStream` API response - Fix "The 'list' argument must be an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView" error in BedrockRuntimeServiceExtension.responseHook() - Add type checking to handle different response body types from AWS SDK middleware: - String: Already processed by AWS SDK (Uint8ArrayBlobAdapter) - Uint8Array: Raw binary from AWS (decode with TextDecoder) - Buffer: Node.js Buffer (convert with toString('utf8')) - Unexpected types: Log debug message and skip processing gracefully ## Test 1. Reproduced the issue by using latest release with `InvokeModelWithResponseStream` ``` events: [ { name: 'exception', attributes: { 'exception.type': 'ERR_INVALID_ARG_TYPE', 'exception.message': 'The "list" argument must be an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView.', 'exception.stacktrace': 'TypeError: The "list" argument must be an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView.\n' + ' at TextDecoder.decode (node:internal/encoding:443:16)\n' + ' at BedrockRuntimeServiceExtension.responseHook (/Users/xiami/Documents/workspace/apm/aws-otel-js-instrumentation/sample-applications/simple-express-server/node_modules/@aws/aws-distro-opentelemetry-node-autoinstrumentation/build/src/patches/aws/services/bedrock.js:303:59)\n' + ' at ServicesExtensions.responseHook (/Users/xiami/Documents/workspace/apm/aws-otel-js-instrumentation/sample-applications/simple-express-server/node_modules/@opentelemetry/instrumentation-aws-sdk/build/src/services/ServicesExtensions.js:37:154)\n' + ' at /Users/xiami/Documents/workspace/apm/aws-otel-js-instrumentation/sample-applications/simple-express-server/node_modules/@opentelemetry/instrumentation-aws-sdk/build/src/aws-sdk.js:257:53\n' + ' at process.processTicksAndRejections (node:internal/process/task_queues:95:5)' }, time: [ 1753815032, 337792 ], droppedAttributesCount: 0 } ], links: [] ``` 2. Tested with the fix and the issue is gone ``` name: 'BedrockRuntime.InvokeModelWithResponseStream', id: '1d23a4c6f2498f7f', kind: 2, timestamp: 1753815031313000, duration: 687392.625, attributes: { 'rpc.system': 'aws-api', 'rpc.method': 'InvokeModelWithResponseStream', 'rpc.service': 'BedrockRuntime', 'gen_ai.system': 'aws.bedrock', 'gen_ai.request.model': 'anthropic.claude-3-5-sonnet-20240620-v1:0', 'gen_ai.request.max_tokens': 1000, 'gen_ai.request.temperature': 0.7, 'gen_ai.request.top_p': 0.9, 'aws.is.local.root': false, ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent cec6770 commit 7c0cdeb

File tree

2 files changed

+142
-2
lines changed

2 files changed

+142
-2
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/patches/aws/services/bedrock.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { Attributes, DiagLogger, Span, SpanKind, Tracer } from '@opentelemetry/api';
4+
import { Attributes, DiagLogger, Span, SpanKind, Tracer, diag } from '@opentelemetry/api';
55
import {
66
AwsSdkInstrumentationConfig,
77
NormalizedRequest,
@@ -331,7 +331,31 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension {
331331
responseHook(response: NormalizedResponse, span: Span, tracer: Tracer, config: AwsSdkInstrumentationConfig): void {
332332
const currentModelId = response.request.commandInput?.modelId;
333333
if (response.data?.body) {
334-
const decodedResponseBody = new TextDecoder().decode(response.data.body);
334+
// Check if this is a streaming response (SmithyMessageDecoderStream)
335+
// Intend to not using instanceOf to avoid import smithy as new dep for this file
336+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/bedrock-runtime/command/InvokeModelWithResponseStreamCommand
337+
if (response.data.body.constructor?.name === 'SmithyMessageDecoderStream') {
338+
// TODO: support InvokeModel Streaming API and Converse APIs later
339+
diag.debug('Streaming API for invoking model is not supported', response.request.commandName);
340+
return;
341+
}
342+
343+
let decodedResponseBody: string;
344+
// For InvokeModel API which should always have reponse body with Uint8Array type
345+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/bedrock-runtime/command/InvokeModelCommand/
346+
if (response.data.body instanceof Uint8Array) {
347+
// Raw Uint8Array from AWS
348+
decodedResponseBody = new TextDecoder().decode(response.data.body);
349+
} else {
350+
// Handle unexpected types - log and skip processing
351+
diag.debug(
352+
`Unexpected body type in Bedrock response: ${typeof response.data.body} for commandName ${
353+
response.request.commandName
354+
}`
355+
);
356+
return;
357+
}
358+
335359
const responseBody = JSON.parse(decodedResponseBody);
336360
if (currentModelId.includes('amazon.titan')) {
337361
if (responseBody.inputTextTokenCount !== undefined) {

aws-distro-opentelemetry-node-autoinstrumentation/test/patches/aws/services/bedrock.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,5 +736,121 @@ describe('BedrockRuntime', () => {
736736
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toEqual(['stop']);
737737
expect(invokeModelSpan.kind).toBe(SpanKind.CLIENT);
738738
});
739+
740+
describe('Response Body Type Handling', () => {
741+
it('handles normal Anthropic Claude response correctly', async () => {
742+
const modelId: string = 'anthropic.claude-3-5-sonnet-20240620-v1:0';
743+
const mockRequestBody: string = JSON.stringify({
744+
anthropic_version: 'bedrock-2023-05-31',
745+
max_tokens: 1000,
746+
messages: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }],
747+
});
748+
749+
// Use standard object format - AWS SDK and instrumentation will handle the conversion
750+
const mockResponseBodyObj = {
751+
stop_reason: 'end_turn',
752+
usage: { input_tokens: 20, output_tokens: 15 },
753+
};
754+
755+
nock(`https://bedrock-runtime.${region}.amazonaws.com`)
756+
.post(`/model/${encodeURIComponent(modelId)}/invoke`)
757+
.reply(200, mockResponseBodyObj);
758+
759+
await bedrock
760+
.invokeModel({
761+
modelId: modelId,
762+
body: mockRequestBody,
763+
})
764+
.catch((err: any) => {});
765+
766+
const testSpans: ReadableSpan[] = getTestSpans();
767+
const invokeModelSpans: ReadableSpan[] = testSpans.filter((s: ReadableSpan) => {
768+
return s.name === 'BedrockRuntime.InvokeModel';
769+
});
770+
expect(invokeModelSpans.length).toBe(1);
771+
const invokeModelSpan = invokeModelSpans[0];
772+
773+
// Verify attributes are set correctly - this tests our type handling logic works
774+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS]).toBe(20);
775+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(15);
776+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toEqual(['end_turn']);
777+
});
778+
779+
it('handles unexpected body type gracefully', async () => {
780+
const modelId: string = 'anthropic.claude-3-5-sonnet-20240620-v1:0';
781+
const mockRequestBody: string = JSON.stringify({
782+
anthropic_version: 'bedrock-2023-05-31',
783+
max_tokens: 1000,
784+
messages: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }],
785+
});
786+
787+
// Mock response body as unexpected type - using reply function to return a number
788+
nock(`https://bedrock-runtime.${region}.amazonaws.com`)
789+
.post(`/model/${encodeURIComponent(modelId)}/invoke`)
790+
.reply(200, () => 12345 as any);
791+
792+
await bedrock.invokeModel({
793+
modelId: modelId,
794+
body: mockRequestBody,
795+
});
796+
797+
const testSpans: ReadableSpan[] = getTestSpans();
798+
const invokeModelSpans: ReadableSpan[] = testSpans.filter((s: ReadableSpan) => {
799+
return s.name === 'BedrockRuntime.InvokeModel';
800+
});
801+
expect(invokeModelSpans.length).toBe(1);
802+
const invokeModelSpan = invokeModelSpans[0];
803+
804+
// Verify that no AI attributes are set when body type is unexpected
805+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS]).toBeUndefined();
806+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS]).toBeUndefined();
807+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toBeUndefined();
808+
809+
// Note: We can't easily test diag.debug() output in unit tests, but the important part
810+
// is that the function returns early and doesn't crash when encountering unexpected types
811+
// Debug message will be: "Unexpected body type in Bedrock response: number for commandName InvokeModelCommand"
812+
});
813+
814+
it('handles streaming response (SmithyMessageDecoderStream) gracefully', async () => {
815+
const modelId: string = 'anthropic.claude-3-5-sonnet-20240620-v1:0';
816+
const mockRequestBody: string = JSON.stringify({
817+
anthropic_version: 'bedrock-2023-05-31',
818+
max_tokens: 1000,
819+
messages: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }],
820+
});
821+
822+
// Mock response body as streaming object (constructor name matching)
823+
const mockStreamingBody = {
824+
constructor: { name: 'SmithyMessageDecoderStream' },
825+
[Symbol.asyncIterator]: function* () {
826+
yield { chunk: { bytes: new TextEncoder().encode('{"type":"chunk"}') } };
827+
},
828+
};
829+
830+
nock(`https://bedrock-runtime.${region}.amazonaws.com`)
831+
.post(`/model/${encodeURIComponent(modelId)}/invoke-with-response-stream`)
832+
.reply(200, mockStreamingBody);
833+
834+
await bedrock.invokeModelWithResponseStream({
835+
modelId: modelId,
836+
body: mockRequestBody,
837+
});
838+
839+
const testSpans: ReadableSpan[] = getTestSpans();
840+
const invokeModelSpans: ReadableSpan[] = testSpans.filter((s: ReadableSpan) => {
841+
return s.name === 'BedrockRuntime.InvokeModelWithResponseStream';
842+
});
843+
expect(invokeModelSpans.length).toBe(1);
844+
const invokeModelSpan = invokeModelSpans[0];
845+
846+
// Verify that no AI attributes are set when body is streaming (metrics not available in initial response)
847+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS]).toBeUndefined();
848+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS]).toBeUndefined();
849+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toBeUndefined();
850+
851+
// Streaming responses should be skipped gracefully without crashing
852+
// TODO: support InvokeModel Streaming API and Converse APIs later
853+
});
854+
});
739855
});
740856
});

0 commit comments

Comments
 (0)