Skip to content

Commit 47b7770

Browse files
committed
Fix Bedrock instrumentation TextDecoder error with robust type handling
- 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
1 parent 629faf5 commit 47b7770

File tree

2 files changed

+173
-2
lines changed

2 files changed

+173
-2
lines changed

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

Lines changed: 18 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,23 @@ 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+
let decodedResponseBody: string;
335+
336+
if (typeof response.data.body === 'string') {
337+
// Already converted by AWS SDK middleware by Uint8ArrayBlobAdapter
338+
decodedResponseBody = response.data.body;
339+
} else if (response.data.body instanceof Uint8Array) {
340+
// Raw Uint8Array from AWS
341+
decodedResponseBody = new TextDecoder().decode(response.data.body);
342+
} else if (Buffer.isBuffer(response.data.body)) {
343+
// Node.js Buffer convert with toString('utf8')
344+
decodedResponseBody = response.data.body.toString('utf8');
345+
} else {
346+
// Handle unexpected types - log and skip processing
347+
diag.debug(`Unexpected body type in Bedrock response: ${typeof response.data.body}`, response.data.body);
348+
return;
349+
}
350+
335351
const responseBody = JSON.parse(decodedResponseBody);
336352
if (currentModelId.includes('amazon.titan')) {
337353
if (responseBody.inputTextTokenCount !== undefined) {

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

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,5 +736,160 @@ 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 string response body 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+
// Mock response body as already converted string
750+
const mockResponseBodyString = JSON.stringify({
751+
stop_reason: 'end_turn',
752+
usage: { input_tokens: 15, output_tokens: 13 },
753+
});
754+
755+
nock(`https://bedrock-runtime.${region}.amazonaws.com`)
756+
.post(`/model/${encodeURIComponent(modelId)}/invoke`)
757+
.reply(200, mockResponseBodyString);
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 despite body being a string
774+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS]).toBe(15);
775+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(13);
776+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toEqual(['end_turn']);
777+
});
778+
779+
it('handles Anthropic Claude response body correctly', 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 - use standard object format (AWS SDK will handle type conversion)
788+
const mockResponseBodyObj = {
789+
stop_reason: 'end_turn',
790+
usage: { input_tokens: 20, output_tokens: 15 },
791+
};
792+
793+
nock(`https://bedrock-runtime.${region}.amazonaws.com`)
794+
.post(`/model/${encodeURIComponent(modelId)}/invoke`)
795+
.reply(200, mockResponseBodyObj);
796+
797+
await bedrock
798+
.invokeModel({
799+
modelId: modelId,
800+
body: mockRequestBody,
801+
})
802+
.catch((err: any) => {});
803+
804+
const testSpans: ReadableSpan[] = getTestSpans();
805+
const invokeModelSpans: ReadableSpan[] = testSpans.filter((s: ReadableSpan) => {
806+
return s.name === 'BedrockRuntime.InvokeModel';
807+
});
808+
expect(invokeModelSpans.length).toBe(1);
809+
const invokeModelSpan = invokeModelSpans[0];
810+
811+
// Verify attributes are set correctly - this tests our type handling logic works
812+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS]).toBe(20);
813+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(15);
814+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toEqual(['end_turn']);
815+
});
816+
817+
it('handles Buffer response body correctly', async () => {
818+
const modelId: string = 'anthropic.claude-3-5-sonnet-20240620-v1:0';
819+
const mockRequestBody: string = JSON.stringify({
820+
anthropic_version: 'bedrock-2023-05-31',
821+
max_tokens: 1000,
822+
messages: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }],
823+
});
824+
825+
// Mock response body as Buffer
826+
const mockResponseBodyObj = {
827+
stop_reason: 'max_tokens',
828+
usage: { input_tokens: 25, output_tokens: 18 },
829+
};
830+
const mockResponseBodyBuffer = Buffer.from(JSON.stringify(mockResponseBodyObj), 'utf8');
831+
832+
nock(`https://bedrock-runtime.${region}.amazonaws.com`)
833+
.post(`/model/${encodeURIComponent(modelId)}/invoke`)
834+
.reply(200, mockResponseBodyBuffer);
835+
836+
await bedrock
837+
.invokeModel({
838+
modelId: modelId,
839+
body: mockRequestBody,
840+
})
841+
.catch((err: any) => {});
842+
843+
const testSpans: ReadableSpan[] = getTestSpans();
844+
const invokeModelSpans: ReadableSpan[] = testSpans.filter((s: ReadableSpan) => {
845+
return s.name === 'BedrockRuntime.InvokeModel';
846+
});
847+
expect(invokeModelSpans.length).toBe(1);
848+
const invokeModelSpan = invokeModelSpans[0];
849+
850+
// Verify attributes are set correctly when body is Buffer
851+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS]).toBe(25);
852+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(18);
853+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toEqual([
854+
'max_tokens',
855+
]);
856+
});
857+
858+
it('handles unexpected body type gracefully', async () => {
859+
const modelId: string = 'anthropic.claude-3-5-sonnet-20240620-v1:0';
860+
const mockRequestBody: string = JSON.stringify({
861+
anthropic_version: 'bedrock-2023-05-31',
862+
max_tokens: 1000,
863+
messages: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }],
864+
});
865+
866+
// Mock response body as unexpected type - using reply function to return a number
867+
nock(`https://bedrock-runtime.${region}.amazonaws.com`)
868+
.post(`/model/${encodeURIComponent(modelId)}/invoke`)
869+
.reply(200, () => 12345 as any);
870+
871+
await bedrock
872+
.invokeModel({
873+
modelId: modelId,
874+
body: mockRequestBody,
875+
})
876+
.catch((err: any) => {});
877+
878+
const testSpans: ReadableSpan[] = getTestSpans();
879+
const invokeModelSpans: ReadableSpan[] = testSpans.filter((s: ReadableSpan) => {
880+
return s.name === 'BedrockRuntime.InvokeModel';
881+
});
882+
expect(invokeModelSpans.length).toBe(1);
883+
const invokeModelSpan = invokeModelSpans[0];
884+
885+
// Verify that no AI attributes are set when body type is unexpected
886+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS]).toBeUndefined();
887+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS]).toBeUndefined();
888+
expect(invokeModelSpan.attributes[AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS]).toBeUndefined();
889+
890+
// Note: We can't easily test diag.debug() output in unit tests, but the important part
891+
// is that the function returns early and doesn't crash when encountering unexpected types
892+
});
893+
});
739894
});
740895
});

0 commit comments

Comments
 (0)