From 5b0634286c9ffdcfbc77a1a987e501f6629edd24 Mon Sep 17 00:00:00 2001 From: maxiaoyang <2768753269@qq.com> Date: Wed, 29 Apr 2026 21:05:07 +0800 Subject: [PATCH] feat(providers/bedrock): prefix user content with sender name Bedrock's Converse API multiplexes Claude, Llama, Titan, Mistral and others through a single content-block protocol. None of those families expose a per-message author identity in a unified way, so prefixing in the text content block keeps multi-user attribution consistent regardless of which underlying model the request lands on. buildUserContent now passes msg through messageutil.ApplyUserNamePrefix before constructing the text ContentBlockMemberText. Tool result paths go through a separate code path (toolResultBlock) and are unaffected. Image media handling is unchanged. Tests: - buildUserContent with Name set produces the prefixed text block - buildUserContent without Name preserves byte-identical behavior - existing image and base64 cases continue to pass The Bedrock build is gated behind //go:build bedrock so this path is only exercised in builds that opt in via that tag. Refs #2702. --- pkg/providers/bedrock/provider_bedrock.go | 11 ++++-- .../bedrock/provider_bedrock_test.go | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/pkg/providers/bedrock/provider_bedrock.go b/pkg/providers/bedrock/provider_bedrock.go index 3798c5fd8..b5221d4fc 100644 --- a/pkg/providers/bedrock/provider_bedrock.go +++ b/pkg/providers/bedrock/provider_bedrock.go @@ -27,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/sipeed/picoclaw/pkg/providers/common" + "github.com/sipeed/picoclaw/pkg/providers/messageutil" "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" ) @@ -319,13 +320,19 @@ func convertMessages(messages []Message) ([]types.Message, []types.SystemContent } // buildUserContent builds Bedrock content blocks for a user message. +// +// Sender attribution from msg.Name is rendered as a `[name] ` prefix on +// the text block. Bedrock's Converse API has no per-message author identity +// shared across all model families, so prefixing keeps the multi-user +// attribution behavior consistent with the Anthropic adapter regardless +// of which underlying model the request lands on. func buildUserContent(msg Message) []types.ContentBlock { var content []types.ContentBlock // Add text content - if msg.Content != "" { + if textValue := messageutil.ApplyUserNamePrefix(msg); textValue != "" { content = append(content, &types.ContentBlockMemberText{ - Value: msg.Content, + Value: textValue, }) } diff --git a/pkg/providers/bedrock/provider_bedrock_test.go b/pkg/providers/bedrock/provider_bedrock_test.go index 38a5e26da..c897177d0 100644 --- a/pkg/providers/bedrock/provider_bedrock_test.go +++ b/pkg/providers/bedrock/provider_bedrock_test.go @@ -294,6 +294,42 @@ func TestBuildUserContent_SkipsNonBase64Data(t *testing.T) { assert.Len(t, content, 1) } +// TestBuildUserContent_UserNamePrefixed verifies that sender attribution +// from Message.Name is rendered as a `[name] ` prefix on the Bedrock text +// block. Bedrock's Converse API has no per-message author identity shared +// across model families, so prefixing keeps multi-user disambiguation +// consistent regardless of the underlying model (Claude, Llama, etc.). +func TestBuildUserContent_UserNamePrefixed(t *testing.T) { + msg := Message{ + Role: "user", + Content: "My name is Alice", + Name: "U_alice", + } + + content := buildUserContent(msg) + + assert.Len(t, content, 1) + textBlock, ok := content[0].(*types.ContentBlockMemberText) + require.True(t, ok) + assert.Equal(t, "[U_alice] My name is Alice", textBlock.Value) +} + +// TestBuildUserContent_NoNameNoPrefix protects backward compatibility for +// direct (single-user) channels: no sender, no prefix. +func TestBuildUserContent_NoNameNoPrefix(t *testing.T) { + msg := Message{ + Role: "user", + Content: "Hello", + } + + content := buildUserContent(msg) + + assert.Len(t, content, 1) + textBlock, ok := content[0].(*types.ContentBlockMemberText) + require.True(t, ok) + assert.Equal(t, "Hello", textBlock.Value) +} + func TestBuildAssistantContent_SkipsEmptyToolName(t *testing.T) { msg := Message{ Content: "Response",