Skip to content

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

Description

@davidgr-mon

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

  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:
llm_format: bedrock
route_type: llm/v1/chat
  1. POST /bedrock/model/<claude-model>/converse with:
{
  "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:

{
  "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:

A general safeguard so adapters never mangle or crash on valid native content during the internal copy would cover all of these.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions