diff --git a/aibridge/intercept/messages/reqpayload.go b/aibridge/intercept/messages/reqpayload.go index 12f25758ae..ce5f0dfdb0 100644 --- a/aibridge/intercept/messages/reqpayload.go +++ b/aibridge/intercept/messages/reqpayload.go @@ -57,6 +57,7 @@ var ( constTool = string(constant.ValueOf[constant.Tool]()) constToolResult = string(constant.ValueOf[constant.ToolResult]()) constUser = string(anthropic.MessageParamRoleUser) + constSystem = "system" // bedrockUnsupportedFields are top-level fields present in the Anthropic Messages // API that are absent from the Bedrock request body schema. Sending them results @@ -166,6 +167,16 @@ func (p RequestPayload) lastUserPrompt() (string, bool, error) { } lastMessage := messageItems[len(messageItems)-1] + // Clients using the mid-conversation system beta (e.g. Claude Code with + // anthropic-beta: mid-conversation-system-*) append a trailing role=system + // message after the user's prompt, such as an injected skills list. When the + // last message is that system message, step back exactly one message to find + // the user's prompt. We only step back past a single trailing system message + // so we never re-record a stale prompt from an earlier turn that contained no + // new user input. See https://docs.claude.com/en/api/beta-headers. + if lastMessage.Get(messagesReqFieldRole).String() == constSystem && len(messageItems) >= 2 { + lastMessage = messageItems[len(messageItems)-2] + } if lastMessage.Get(messagesReqFieldRole).String() != constUser { return "", false, nil } diff --git a/aibridge/intercept/messages/reqpayload_internal_test.go b/aibridge/intercept/messages/reqpayload_internal_test.go index 1ef50223c5..3dc5c1262f 100644 --- a/aibridge/intercept/messages/reqpayload_internal_test.go +++ b/aibridge/intercept/messages/reqpayload_internal_test.go @@ -202,6 +202,41 @@ func TestRequestPayloadLastUserPrompt(t *testing.T) { expectedFound: true, expectError: false, }, + { + name: "trailing system message steps back to user prompt", + requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":"hello"},{"role":"system","content":"available skills: ..."}]}`, + expectedPrompt: "hello", + expectedFound: true, + expectError: false, + }, + { + name: "trailing system message with typed user content returns last text block", + requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":[{"type":"text","text":"first"},{"type":"text","text":"last"}]},{"role":"system","content":"available skills: ..."}]}`, + expectedPrompt: "last", + expectedFound: true, + expectError: false, + }, + { + name: "trailing system message after non user does not record", + requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"assistant","content":"response"},{"role":"system","content":"available skills: ..."}]}`, + expectedPrompt: "", + expectedFound: false, + expectError: false, + }, + { + name: "only system message does not step out of bounds", + requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"system","content":"available skills: ..."}]}`, + expectedPrompt: "", + expectedFound: false, + expectError: false, + }, + { + name: "two trailing system messages only steps back once", + requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":"hello"},{"role":"system","content":"a"},{"role":"system","content":"b"}]}`, + expectedPrompt: "", + expectedFound: false, + expectError: false, + }, { name: "messages wrong type returns error", requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":{}}`,