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.
This commit is contained in:
maxiaoyang 2026-04-29 21:05:07 +08:00
parent cd4372368e
commit 5b0634286c
2 changed files with 45 additions and 2 deletions

View file

@ -27,6 +27,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
"github.com/sipeed/picoclaw/pkg/providers/common" "github.com/sipeed/picoclaw/pkg/providers/common"
"github.com/sipeed/picoclaw/pkg/providers/messageutil"
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes" "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. // 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 { func buildUserContent(msg Message) []types.ContentBlock {
var content []types.ContentBlock var content []types.ContentBlock
// Add text content // Add text content
if msg.Content != "" { if textValue := messageutil.ApplyUserNamePrefix(msg); textValue != "" {
content = append(content, &types.ContentBlockMemberText{ content = append(content, &types.ContentBlockMemberText{
Value: msg.Content, Value: textValue,
}) })
} }

View file

@ -294,6 +294,42 @@ func TestBuildUserContent_SkipsNonBase64Data(t *testing.T) {
assert.Len(t, content, 1) 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) { func TestBuildAssistantContent_SkipsEmptyToolName(t *testing.T) {
msg := Message{ msg := Message{
Content: "Response", Content: "Response",