#14996·kong

ai-proxy-advanced (llm_format: bedrock): 500 in bedrock adapter when a content block follows toolResult in the same message

Author: davidgr-monCreated Sep 10, 2026Updated Sep 14, 2026

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:

json
{
  "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=-1

Controls on the same route isolate the cause:

  • [toolResult] only → 200
  • [toolResult, text] (no cachePoint) → 500
  • The same [toolResult, text] body sent directly to bedrock-runtime200

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):

  1. The toolResult branch sets new_msg.content to a string (part.content).
  2. The next content block falls through to the generic else branch.
  3. 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 Bedrock
  • cacheWriteInputTokens / cacheReadInputTokens are 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

  1. Configure ai-proxy-advanced on a Bedrock Converse route:
yaml
llm_format: bedrock
route_type: llm/v1/chat
  1. POST /bedrock/model/<claude-model>/converse with:
json
{
  "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.

  1. Repeat with only the toolResult block in the final message → 200.

  2. Repeat step 2 but replace cachePoint with:

json
{
  "text": "Reply only with OK."
}

Result: 500.

  1. 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.