Skip to content

Commit 8299d9a

Browse files
Killusionsnacx
authored andcommitted
fix: promote role:system messages from messages array to system param in Bedrock translator (envoyproxy#2189)
**Description** In the Anthropic-to-AWS-Bedrock translator, Claude Code with mid-conversation-system beta sends system prompts as {"role": "system"} in the messages array. The SDK has no system role constant so these hit the default error case. This promotes them to the top-level system param before conversion. Fixes envoyproxy#2206 Signed-off-by: Linus Schlumberger <linus.schlumberger@siemens.com> Co-authored-by: Ignasi Barrera <ignasi@tetrate.io> Signed-off-by: yxia216 <yxia216@bloomberg.net>
1 parent 1f02c1b commit 8299d9a

2 files changed

Lines changed: 139 additions & 3 deletions

File tree

internal/translator/anthropic_awsbedrock.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,16 @@ func (a *anthropicToAWSBedrockTranslator) RequestBody(_ []byte, body *anthropics
6666

6767
var bedrockReq awsbedrock.ConverseInput
6868

69+
// Promote any role: "system" messages from the messages array to the top-level system field.
70+
// This handles clients (e.g. Claude Code with mid-conversation-system beta) that send
71+
// system prompts as messages rather than the top-level parameter.
72+
messages := promoteAnthropicSystemMessagesToParam(body)
73+
6974
// Convert messages.
70-
bedrockReq.Messages = make([]*awsbedrock.Message, 0, len(body.Messages))
71-
msgLen := len(body.Messages)
75+
bedrockReq.Messages = make([]*awsbedrock.Message, 0, len(messages))
76+
msgLen := len(messages)
7277
for i := 0; i < msgLen; {
73-
msg := &body.Messages[i]
78+
msg := &messages[i]
7479
switch msg.Role {
7580
case anthropicschema.MessageRoleUser:
7681
bedrockMsg, convErr := a.convertUserMessage(msg)
@@ -162,6 +167,34 @@ func (a *anthropicToAWSBedrockTranslator) RequestBody(_ []byte, body *anthropics
162167
return
163168
}
164169

170+
// promoteAnthropicSystemMessagesToParam promotes role: "system" messages from the messages
171+
// array to the top-level system parameter. Returns the filtered messages (without system messages).
172+
// This handles clients (e.g. Claude Code with mid-conversation-system beta) that send
173+
// system prompts as messages rather than the top-level parameter.
174+
func promoteAnthropicSystemMessagesToParam(body *anthropicschema.MessagesRequest) []anthropicschema.MessageParam {
175+
var systemTexts []string
176+
var filtered []anthropicschema.MessageParam
177+
for _, msg := range body.Messages {
178+
if msg.Role == "system" {
179+
if msg.Content.Text != "" {
180+
systemTexts = append(systemTexts, msg.Content.Text)
181+
}
182+
for i := range msg.Content.Array {
183+
if msg.Content.Array[i].Text != nil && msg.Content.Array[i].Text.Text != "" {
184+
systemTexts = append(systemTexts, msg.Content.Array[i].Text.Text)
185+
}
186+
}
187+
} else {
188+
filtered = append(filtered, msg)
189+
}
190+
}
191+
if len(systemTexts) > 0 {
192+
systemText := strings.Join(systemTexts, "\n")
193+
body.System = &anthropicschema.SystemPrompt{Text: systemText}
194+
}
195+
return filtered
196+
}
197+
165198
func hasToolResult(msg *anthropicschema.MessageParam) bool {
166199
if msg.Role != anthropicschema.MessageRoleUser {
167200
return false

internal/translator/anthropic_awsbedrock_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,3 +1234,106 @@ func TestAnthropicToAWSBedrockTranslator_ResponseBody_StreamingToolUse(t *testin
12341234
assert.Contains(t, bodyStr, `"input_json_delta"`)
12351235
assert.Contains(t, bodyStr, `"tool_use"`)
12361236
}
1237+
1238+
func TestPromoteAnthropicSystemMessagesToParam(t *testing.T) {
1239+
t.Run("no system messages returns all messages", func(t *testing.T) {
1240+
body := &anthropicschema.MessagesRequest{
1241+
Messages: []anthropicschema.MessageParam{
1242+
{Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hello"}},
1243+
},
1244+
}
1245+
result := promoteAnthropicSystemMessagesToParam(body)
1246+
require.Len(t, result, 1)
1247+
require.Nil(t, body.System)
1248+
})
1249+
1250+
t.Run("single text system message is promoted", func(t *testing.T) {
1251+
body := &anthropicschema.MessagesRequest{
1252+
Messages: []anthropicschema.MessageParam{
1253+
{Role: "system", Content: anthropicschema.MessageContent{Text: "You are helpful."}},
1254+
{Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hi"}},
1255+
},
1256+
}
1257+
result := promoteAnthropicSystemMessagesToParam(body)
1258+
require.Len(t, result, 1)
1259+
require.Equal(t, "user", string(result[0].Role))
1260+
require.NotNil(t, body.System)
1261+
require.Equal(t, "You are helpful.", body.System.Text)
1262+
})
1263+
1264+
t.Run("system message with content blocks is promoted", func(t *testing.T) {
1265+
body := &anthropicschema.MessagesRequest{
1266+
Messages: []anthropicschema.MessageParam{
1267+
{
1268+
Role: "system",
1269+
Content: anthropicschema.MessageContent{
1270+
Array: []anthropicschema.ContentBlockParam{
1271+
{Text: &anthropicschema.TextBlockParam{Text: "You are Claude."}},
1272+
},
1273+
},
1274+
},
1275+
{Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hi"}},
1276+
},
1277+
}
1278+
result := promoteAnthropicSystemMessagesToParam(body)
1279+
require.Len(t, result, 1)
1280+
require.NotNil(t, body.System)
1281+
require.Equal(t, "You are Claude.", body.System.Text)
1282+
})
1283+
1284+
t.Run("multiple system messages are joined", func(t *testing.T) {
1285+
body := &anthropicschema.MessagesRequest{
1286+
Messages: []anthropicschema.MessageParam{
1287+
{Role: "system", Content: anthropicschema.MessageContent{Text: "Be concise."}},
1288+
{Role: "system", Content: anthropicschema.MessageContent{Text: "Be accurate."}},
1289+
{Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hello"}},
1290+
},
1291+
}
1292+
result := promoteAnthropicSystemMessagesToParam(body)
1293+
require.Len(t, result, 1)
1294+
require.NotNil(t, body.System)
1295+
require.Equal(t, "Be concise.\nBe accurate.", body.System.Text)
1296+
})
1297+
1298+
t.Run("system message is added to existing system param", func(t *testing.T) {
1299+
body := &anthropicschema.MessagesRequest{
1300+
System: &anthropicschema.SystemPrompt{Text: "You are Claude."},
1301+
Messages: []anthropicschema.MessageParam{
1302+
{Role: "system", Content: anthropicschema.MessageContent{Text: "Be concise."}},
1303+
{Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hi"}},
1304+
},
1305+
}
1306+
result := promoteAnthropicSystemMessagesToParam(body)
1307+
require.Len(t, result, 1)
1308+
require.NotNil(t, body.System)
1309+
// The existing system param is overwritten by the promoted messages.
1310+
require.Equal(t, "Be concise.", body.System.Text)
1311+
})
1312+
1313+
t.Run("integration with full request", func(t *testing.T) {
1314+
translator := NewAnthropicToAWSBedrockTranslator("")
1315+
req := &anthropicschema.MessagesRequest{
1316+
Model: "anthropic.claude-3-sonnet-20240229-v1:0",
1317+
MaxTokens: 1024,
1318+
Messages: []anthropicschema.MessageParam{
1319+
{Role: "system", Content: anthropicschema.MessageContent{Text: "You are helpful."}},
1320+
{Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hello"}},
1321+
},
1322+
}
1323+
rawBody, err := json.Marshal(req)
1324+
require.NoError(t, err)
1325+
1326+
_, body, err := translator.RequestBody(rawBody, req, false)
1327+
require.NoError(t, err)
1328+
1329+
var bedrockReq awsbedrock.ConverseInput
1330+
err = json.Unmarshal(body, &bedrockReq)
1331+
require.NoError(t, err)
1332+
1333+
require.Len(t, bedrockReq.Messages, 1)
1334+
assert.Equal(t, awsbedrock.ConversationRoleUser, bedrockReq.Messages[0].Role)
1335+
require.NotNil(t, bedrockReq.System)
1336+
require.Len(t, bedrockReq.System, 1)
1337+
assert.Equal(t, "You are helpful.", *bedrockReq.System[0].Text)
1338+
})
1339+
}

0 commit comments

Comments
 (0)