From 4e87fdb20c7c1c163a18552f7e83692d3ddca2e0 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 10 Jun 2026 10:47:13 +0200 Subject: [PATCH] fix(aibridge/intercept/messages): record user prompt before trailing system message (#26195) _Disclosure: produced using Opus 4.8_ I've noticed recently that some prompts are not displaying correctly. image The cause is the `mid-conversation-system-2026-04-07` beta. When enabled, the client appends a trailing `role: "system"` message after the user's text (e.g. an injected skills list): ```json "messages": [ { "role": "user", "content": [ "hey how are ya" ] }, { "role": "system", "content": "The following skills are available..." } ] ``` Our prompt detection algo was being subverted since we only check the last message if role=user. --- aibridge/intercept/messages/reqpayload.go | 11 ++++++ .../messages/reqpayload_internal_test.go | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+) 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":{}}`,