ai-proxy Bedrock: Claude Opus 5.5 chat responses lose `content` when reasoningContent is the first block
Thanks for the Bedrock Converse driver (#13354) and for the multi-part Gemini handling in #14785. The same first-block pattern hits Bedrock, intermittently.
Claude Opus 5.5 (and Opus 5) on Converse often put a reasoningContent block before the text block without thinking being requested; how often depends on the prompt. from_bedrock_chat_openai reads only content[1].text, so those llm/v1/chat responses have no content. A quick Say hi comes back text-only and works.
Is there an existing issue for this?
- I have searched the existing issues
Kong version ($ kong version)
master 3091de0 (3.10.0-dev); same line in 3.9.3. No Kong node: I ran kong/llm/drivers/bedrock.lua under LuaJIT 2.1 + lua-cjson with its Kong/nginx imports stubbed, on live Bedrock responses (us-east-1).
Current Behavior
First block is reasoningContent (live Converse, body from the driver's to_format):
| prompt | Opus 5.5 (us. + global.) |
Opus 5 (us.) |
|---|---|---|
Say hi |
0/6 | 0/3 |
What is 2+2? |
1/6 | 2/3 |
Explain TCP slow start in two sentences. |
5/6 | 3/3 |
Weather in Paris? brief |
12/12 | 6/6 |
Converse response, us.anthropic.claude-opus-5-5 (signature and text trimmed):
{"output":{"message":{"content":[{"reasoningContent":{"reasoningText":{"signature":"CAQSvwMKEggS...","text":""}}},{"text":"I don't have live data, so I can't give you today's weather...."}],"role":"assistant"}},"stopReason":"end_turn"}
bedrock.from_format(<that response>, model, "llm/v1/chat") on master:
{"model":"us.anthropic.claude-opus-5-5","usage":{"completion_tokens":194,"prompt_tokens":18,"total_tokens":212},"choices":[{"index":0,"message":{"role":"assistant"},"finish_reason":"stop"}],"object":"chat.completion"}kong/llm/drivers/bedrock.lua L429:
content = response.output.message.content[1].text, -- may be nilFrom reading kong/llm/init.lua L95 and L173-175 (not run), ai-request/response-transformer would then fail with no 'choices' in upstream AI service response.
Expected Behavior
content holds the text. Measured with the change below:
| case (live Bedrock, us-east-1) | Before (master) | After |
|---|---|---|
Opus 5.5 / Opus 5, reasoningContent first (29 of 45 plain runs) |
❌ no content |
✅ answer text |
Opus 5.5 / Opus 5, text block only (16 of 45, e.g. Say hi) |
✅ text | ✅ unchanged |
| Sonnet 4.5 / Haiku 4.5 (text block only) | ✅ text | ✅ unchanged |
| Opus 5.5 / Opus 5 with one tool | ✅ 1 tool_calls |
✅ unchanged |
| Streaming Opus 5.5 / Opus 5 | ✅ text deltas | ✅ unchanged (not touched) |
unit/real-responses/bedrock/llm-v1-chat.json |
✅ | ✅ unchanged |
Steps To Reproduce
- Run
bedrock.to_formaton{"messages":[{"role":"user","content":"Weather in Paris? brief"}]}. - POST it to
/model/us.anthropic.claude-opus-5-5/converse. bedrock.from_format(<response>, ..., "llm/v1/chat"):choices[1].messagehas onlyrole.
Anything else?
A possible change, following extract_text_from_content in kong/llm/drivers/anthropic.lua L319-331 (keeps every block with text, joins with "\n"):
-- Claude Opus 5 / 5.5 can put a reasoningContent block before the text;
-- join the text blocks with "\n" like the anthropic driver does
local function join_text(content)
local texts = {}
for _, block in ipairs(content) do
if block.text then
table_insert(texts, block.text)
end
end
return #texts > 0 and table.concat(texts, "\n") or nil
end
-- L429
content = join_text(response.output.message.content), -- may be nilI can open a PR with a fixture and changelog entry, or leave it for the next cumulative AI Gateway PR, whichever is easier for you.
RED / GREEN on the recorded live responsesOn a node: ai-proxy, provider: bedrock, route_type: llm/v1/chat. luacheck (repo .luacheckrc) on the changed file: 0 warnings / 0 errors. A case passes when content equals the text blocks joined with "\n" and tool_calls matches the toolUse count. synthetic-two-text-blocks (reasoningContent, "a", "b", expects "a\nb") covers the join; no live reply had two text blocks.
### RED (master 3091de0), "Weather in Paris? brief" + tool + other models
FAIL global.anthropic.claude-opus-5-5-1 reasoningContent,text content=missing tool_calls=0
FAIL global.anthropic.claude-opus-5-5-2 reasoningContent,text content=missing tool_calls=0
FAIL global.anthropic.claude-opus-5-5-3 reasoningContent,text content=missing tool_calls=0
ok tools-us.anthropic.claude-opus-5-5 toolUse content=missing tool_calls=1
ok tools-us.anthropic.claude-opus-5 text,toolUse content=24 chars tool_calls=1
ok us.anthropic.claude-haiku-4-5-20251001-v1:0-1 text content=308 chars tool_calls=0
FAIL us.anthropic.claude-opus-5-1 reasoningContent,text content=missing tool_calls=0
FAIL us.anthropic.claude-opus-5-2 reasoningContent,text content=missing tool_calls=0
FAIL us.anthropic.claude-opus-5-3 reasoningContent,text content=missing tool_calls=0
FAIL us.anthropic.claude-opus-5-5-1 reasoningContent,text content=missing tool_calls=0
FAIL us.anthropic.claude-opus-5-5-2 reasoningContent,text content=missing tool_calls=0
FAIL us.anthropic.claude-opus-5-5-3 reasoningContent,text content=missing tool_calls=0
ok us.anthropic.claude-sonnet-4-5-20250929-v1:0-1 text content=166 chars tool_calls=0
FAIL synthetic-two-text-blocks reasoningContent,text,text content=missing
4/14 passed
### GREEN (join_text), same set
ok global.anthropic.claude-opus-5-5-1 reasoningContent,text content=378 chars tool_calls=0
ok global.anthropic.claude-opus-5-5-2 reasoningContent,text content=377 chars tool_calls=0
ok global.anthropic.claude-opus-5-5-3 reasoningContent,text content=409 chars tool_calls=0
ok tools-us.anthropic.claude-opus-5-5 toolUse content=missing tool_calls=1
ok tools-us.anthropic.claude-opus-5 text,toolUse content=24 chars tool_calls=1
ok us.anthropic.claude-haiku-4-5-20251001-v1:0-1 text content=308 chars tool_calls=0
ok us.anthropic.claude-opus-5-1 reasoningContent,text content=324 chars tool_calls=0
ok us.anthropic.claude-opus-5-2 reasoningContent,text content=290 chars tool_calls=0
ok us.anthropic.claude-opus-5-3 reasoningContent,text content=301 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-1 reasoningContent,text content=406 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-2 reasoningContent,text content=450 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-3 reasoningContent,text content=425 chars tool_calls=0
ok us.anthropic.claude-sonnet-4-5-20250929-v1:0-1 text content=166 chars tool_calls=0
ok synthetic-two-text-blocks reasoningContent,text,text content="a\nb"
14/14 passedPrompt set, 4 prompts x 3 runs x 3 models (p1 Say hi, p2 2+2, p3 TCP, p4 Paris weather). Master fails exactly the reasoningContent-first replies:
### RED (master 3091de0), excerpt of 37 lines
ok global.anthropic.claude-opus-5-5-p1-1 text content=34 chars tool_calls=0
ok global.anthropic.claude-opus-5-5-p3-1 text content=463 chars tool_calls=0
FAIL global.anthropic.claude-opus-5-5-p3-2 reasoningContent,text content=missing tool_calls=0
ok us.anthropic.claude-opus-5-5-p1-1 text content=34 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-p2-2 text content=13 chars tool_calls=0
FAIL us.anthropic.claude-opus-5-5-p2-3 reasoningContent,text content=missing tool_calls=0
FAIL us.anthropic.claude-opus-5-5-p3-1 reasoningContent,text content=missing tool_calls=0
FAIL us.anthropic.claude-opus-5-p2-1 reasoningContent,text content=missing tool_calls=0
ok us.anthropic.claude-opus-5-p2-3 text content=9 chars tool_calls=0
16/37 passed
### GREEN (join_text), same excerpt
ok global.anthropic.claude-opus-5-5-p1-1 text content=34 chars tool_calls=0
ok global.anthropic.claude-opus-5-5-p3-1 text content=463 chars tool_calls=0
ok global.anthropic.claude-opus-5-5-p3-2 reasoningContent,text content=493 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-p1-1 text content=34 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-p2-2 text content=13 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-p2-3 reasoningContent,text content=13 chars tool_calls=0
ok us.anthropic.claude-opus-5-5-p3-1 reasoningContent,text content=492 chars tool_calls=0
ok us.anthropic.claude-opus-5-p2-1 reasoningContent,text content=9 chars tool_calls=0
ok us.anthropic.claude-opus-5-p2-3 text content=9 chars tool_calls=0
37/37 passed
### Streaming, ConverseStream events through handle_stream_event (same before and after)
us.anthropic.claude-opus-5-5: 31 content chunks, joined: I can't check live weather, so for today's forecast try weat
us.anthropic.claude-opus-5: 24 content chunks, joined: I can't check live weather — I don't have real-time data aStubs used to load the driver outside Kong:
_G.ngx = { log = function() end, ERR = 4, WARN = 5, DEBUG = 8 }
package.loaded["kong.llm.drivers.shared"] = {
merge_config_defaults = function(req) return req end,
_CONST = { SSE_TERMINATOR = "[DONE]" },
}
package.loaded["socket.url"] = {}
package.loaded["resty.aws.request.sign"] = function() end
package.loaded["kong.llm.plugin.ctx"] = {
get_global_accessors = function() return function() end, function() end end,
}
local bedrock = require("kong.llm.drivers.bedrock")References: Claude Opus 5.5 announcement · migration guide · models overview
Source: Kong/kong