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.

<img width="1388" height="509" alt="image"
src="https://github.com/user-attachments/assets/fad3812f-e42d-4398-a16c-a0e7f924455d"
/>

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.
This commit is contained in:
Danny Kopping
2026-06-10 10:47:13 +02:00
committed by GitHub
parent 35a7dc8ab9
commit 4e87fdb20c
2 changed files with 46 additions and 0 deletions
+11
View File
@@ -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
}
@@ -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":{}}`,