ai-proxy-advanced (llm_format: bedrock): 500 in bedrock adapter when a content block follows toolResult in the same message
Is there an existing issue for this?
- I have searched the existing issues
Kong version
Kong 3.14.0.13 (server: kong/3.14.0.13-enterprise-edition)Current Behavior
Enabling Bedrock explicit prompt caching (cache_control on ChatBedrockConverse, or a native cachePoint block) on a llm_format: bedrock Converse route works for the first model call, then breaks on every call that follows a tool call. Kong returns HTTP 500 instead of forwarding the request to Bedrock — so caching cannot be used at all for any tool-calling agent, which is the normal shape of agentic traffic.
Kong returns its generic error envelope and never calls Bedrock:
{
"message": "An unexpected error occurred",
"request_id": "bc01fe387b59e650a861eb310be46e4b"
}error_log:
[ai-proxy-advanced] /usr/local/share/lua/5.1/kong/llm/adapters/bedrock.lua:149:
bad argument #1 to 'insert' (table expected, got string)The access log shows Kong failed before any upstream attempt:
tries=[], upstream_status="", proxy=-1Controls on the same route isolate the cause:
[toolResult]only → 200[toolResult, text](nocachePoint) → 500- The same
[toolResult, text]body sent directly tobedrock-runtime→ 200
So this is not an AWS validation error, and it is not specific to prompt caching. Prompt caching just makes it common: LangChain ChatBedrockConverse emits [toolResult, cachePoint] on post-tool turns when caching is enabled.
Root cause is in bedrock_msg_to_openai_msg (kong/llm/adapters/bedrock.lua; line number varies by version — 149 on 3.14.0.13-enterprise, and the same code is present in current master):
- The
toolResultbranch setsnew_msg.contentto a string (part.content). - The next content block falls through to the generic
elsebranch. - That branch calls
table.insert(new_msg.content, part)on the string, raising:
bad argument #1 to 'insert' (table expected, got string)There is no cachePoint branch and no type guard on new_msg.content. Because the else branch handles every non-tool block, this reproduces with text and cachePoint (both verified) and by inspection applies to any other block (image, document, …).
parse-request.lua calls adapter:to_kong_req before the upstream request, so even native Bedrock format is parsed/converted rather than passed through untouched.
Reproduced on Converse (non-streaming); ConverseStream not yet verified.
Expected Behavior
Kong should accept a valid Bedrock Converse ContentBlock mix — including a toolResult followed by cachePoint, text, or another block — convert it for analytics without raising, and forward the native request to Bedrock:
[toolResult]→ 200[toolResult, cachePoint]→ 200, request reaches Bedrock[toolResult, text]→ 200, request reaches BedrockcacheWriteInputTokens/cacheReadInputTokensare preserved when Bedrock returns them
The adapter needs to stop assuming new_msg.content is always a table after a toolResult.
Guarding the table.insert so it only appends when new_msg.content is a table or nil prevents the crash; the native body can then continue unchanged to Bedrock.
Steps To Reproduce
- Configure
ai-proxy-advancedon a Bedrock Converse route:
llm_format: bedrock
route_type: llm/v1/chatPOST /bedrock/model/<claude-model>/conversewith:
{
"messages": [
{
"role": "user",
"content": [
{
"text": "Use the probe tool."
}
]
},
{
"role": "assistant",
"content": [
{
"toolUse": {
"toolUseId": "t1",
"name": "probe_tool",
"input": {}
}
}
]
},
{
"role": "user",
"content": [
{
"toolResult": {
"toolUseId": "t1",
"content": [
{
"json": {
"ok": true
}
}
]
}
},
{
"cachePoint": {
"type": "default"
}
}
]
}
],
"toolConfig": {
"tools": [
{
"toolSpec": {
"name": "probe_tool",
"description": "Diagnostic probe",
"inputSchema": {
"json": {
"type": "object",
"properties": {}
}
}
}
}
]
},
"inferenceConfig": {
"maxTokens": 16
}
}Result: 500
bedrock.lua: bad argument #1 to 'insert' (table expected, got string)No upstream request is made.
Repeat with only the
toolResultblock in the final message → 200.Repeat step 2 but replace
cachePointwith:
{
"text": "Reply only with OK."
}Result: 500.
- Send the exact body from step 4 directly to AWS Bedrock Converse → 200.
Anything else?
Same defect class as #14837 (Gemini driver drops cachedContent during the OpenAI-shape transformation): in both cases, the adapter's internal analytics conversion corrupts or chokes on a valid native field.
Related:
- #14802 - recording cache token counts in metrics/logging
- #13760 - prior adapter tool-handling fix
A general safeguard so adapters never mangle or crash on valid native content during the internal copy would cover all of these.
Source: Kong/kong