Skip to content

bedrockruntime: Converse API cannot round-trip numeric tool parameters through map[string]any #3382

@spenczar

Description

@spenczar

When using the Converse API for multi-turn tool-use conversations, numeric values in tool arguments are corrupted if the caller stores arguments as map[string]any between turns. The second Converse call fails with a 500 InternalServerException. I believe aws/smithy-go#596 is the ultimate root cause. I'm making a separate issue because that's a pretty old issue, and the customer impact might not have been clear enough from it, and also it's possible that I'm simply holding the bedrockruntime API wrong.

As I understand it, I have to unmarshal the document that the LLM provides as tool calls, and use it myself. I'd like to re-marshal that decoded input for when I construct my next turn in the conversation, rather than have to keep track of the historical documents, because I'm making a provider-agnostic bit of code here.

Here's a reproducer main.go (and associated go.mod). For me, it produces:

Model called tool "add" (ID: tooluse_EQqEboQbnMTI7bOTOdK1JV)

Turn 2 Converse FAILED: operation error Bedrock Runtime: Converse, exceeded maximum number of attempts, 3, https response error StatusCode: 500, RequestID: 1e5051f7-ccc0-4c5d-9b1f-7feb707d126b, InternalServerException: The system encountered an unexpected error during processing. Try your request again.
Input document bytes: {"a":3,"a":"3","b":4,"b":"4"}
exit status 1
// Run with:
//   go mod tidy && go run .
//
// Requires AWS credentials in the environment (standard credential chain:
// env vars, ~/.aws/credentials, IAM role, etc.) and an AWS region with
// Bedrock access.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/document"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
)

const modelID = "us.amazon.nova-micro-v1:0"

func main() {
	ctx := context.Background()

	cfg, err := config.LoadDefaultConfig(ctx)
	assertNoError(err)

	rt := bedrockruntime.NewFromConfig(cfg)

	// Define a tool that accepts two numeric values - "add(a: int, b: int)".
	toolConfig := &types.ToolConfiguration{
		Tools: []types.Tool{
			&types.ToolMemberToolSpec{Value: types.ToolSpecification{
				Name:        aws.String("add"),
				Description: aws.String("Add two integers together."),
				InputSchema: &types.ToolInputSchemaMemberJson{
					Value: document.NewLazyDocument(map[string]any{
						"type": "object",
						"properties": map[string]any{
							"a": map[string]any{"type": "integer", "description": "first operand"},
							"b": map[string]any{"type": "integer", "description": "second operand"},
						},
						"required": []any{"a", "b"},
					}),
				},
			}},
		},
	}

	//  Ask the model to call the add tool ---
	out1, err := rt.Converse(ctx, &bedrockruntime.ConverseInput{
		ModelId:    aws.String(modelID),
		ToolConfig: toolConfig,
		Messages: []types.Message{{
			Role: types.ConversationRoleUser,
			Content: []types.ContentBlock{
				&types.ContentBlockMemberText{Value: "What is 3 plus 4? Use the add tool."},
			},
		}},
	})
	assertNoError(err)

	// Pull out the tool use from the response so we can evaluate it.
	msg1 := out1.Output.(*types.ConverseOutputMemberMessage)
	var toolUse *types.ContentBlockMemberToolUse
	for _, block := range msg1.Value.Content {
		if tu, ok := block.(*types.ContentBlockMemberToolUse); ok {
			toolUse = tu
			break
		}
	}
	fmt.Printf("Model called tool %q (ID: %s)\n", *toolUse.Value.Name, *toolUse.Value.ToolUseId)

	// Deserialize into map[string]any - in my application, this is important for
	// dynamic tool dispatch patterns.
	var args map[string]any
	err = toolUse.Value.Input.UnmarshalSmithyDocument(&args)
	assertNoError(err)

	// Compute our answer...
	result, err := eval(*toolUse.Value.Name, args)
	assertNoError(err)

	// Turn 2: send tool result and expect a natural-language answer ---
	_, err = rt.Converse(ctx, &bedrockruntime.ConverseInput{
		ModelId:    aws.String(modelID),
		ToolConfig: toolConfig,
		Messages: []types.Message{
			{
				Role: types.ConversationRoleUser,
				Content: []types.ContentBlock{
					&types.ContentBlockMemberText{Value: "What is 3 plus 4? Use the add tool."},
				},
			},
			{
				Role: types.ConversationRoleAssistant,
				Content: []types.ContentBlock{
					// Reconstruct the tool use block from the extracted args —
					// the idiomatic approach when args have been stored as
					// map[string]any between turns.
					&types.ContentBlockMemberToolUse{Value: types.ToolUseBlock{
						ToolUseId: toolUse.Value.ToolUseId,
						Name:      toolUse.Value.Name,
						// I believe this following line is the bug:
						Input: document.NewLazyDocument(args),
					}},
				},
			},
			{
				Role: types.ConversationRoleUser,
				Content: []types.ContentBlock{
					&types.ContentBlockMemberToolResult{Value: types.ToolResultBlock{
						ToolUseId: toolUse.Value.ToolUseId,
						Content: []types.ToolResultContentBlock{
							&types.ToolResultContentBlockMemberText{Value: result},
						},
					}},
				},
			},
		},
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "\nTurn 2 Converse FAILED: %v\n", err)

		inputDoc := document.NewLazyDocument(args)
		inputBytes, err := inputDoc.MarshalSmithyDocument()
		if err != nil {
			fmt.Fprintf(os.Stderr, "MarshalSmithyDocument: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("Input document bytes: %s\n", string(inputBytes))
		os.Exit(1)
	}
}

// minor helpers below

func eval(toolName string, args map[string]any) (string, error) {
	if toolName == "add" {
		a, _ := args["a"].(float64)
		b, _ := args["b"].(float64)
		sum := a + b
		return fmt.Sprintf("%v", sum), nil
	}

	return "", fmt.Errorf("unknown tool: %s", toolName)
}

func fatal(err error) {
	fmt.Fprintf(os.Stderr, "%v\n", err)
	os.Exit(1)
}

func assertNoError(err error) {
	if err != nil {
		fatal(err)
	}
}

go.mod:

module smithy596reproducer

go 1.24

require (
	github.com/aws/aws-sdk-go-v2 v1.41.5
	github.com/aws/aws-sdk-go-v2/config v1.29.14
	github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4
)

require (
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
	github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
	github.com/aws/smithy-go v1.24.2 // indirect
)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions