AWS Bedrock un-escapes tool-call arguments in transit

medium · inference · probed live 2026-07-26 · affects AWS Bedrock (Chat Completions), AWS Bedrock (Responses)

At temperature 0, every other provider agrees — across independent serving stacks — on the exact bytes the model returns for a tricky string argument: escape sequences as literal two-character text (\ + n), matching the JSON text the model was shown. Both Bedrock lanes alone return different bytes: real control characters, one un-escape pass beyond the consensus. And beside the call, both lanes emit a stray content fragment that the model's trained output grammar cannot produce.

What was sent

A record tool and the instruction to pass the payload byte-for-byte (download):

record-payload.chat.json
{
  "model": "google.gemma-4-31b",
  "messages": [
    {
      "role": "system",
      "content": "You are a data-entry agent. Call the `record` tool exactly once, passing EXACTLY the arguments given — byte for byte, no additions, no reformatting. Do not repeat the arguments in your reply."
    },
    {
      "role": "user",
      "content": "Record this payload:\n{\n  \"note\": \"line1\\nline2\\t\\\"quoted\\\" and \\\\backslash\",\n  \"unicode\": \"héllo — 世界 🚀\",\n  \"nested\": {\n    \"a\": [\n      1,\n      2.5,\n      false,\n      null\n    ],\n    \"b\": {\n      \"c\": \"deep\"\n    }\n  },\n  \"long\": \"LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL-END\"\n}"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "record",
        "description": "Records a data payload verbatim.",
        "parameters": {
          "type": "object",
          "properties": {
            "note": {
              "type": "string"
            },
            "unicode": {
              "type": "string"
            },
            "nested": {
              "type": "object"
            },
            "long": {
              "type": "string"
            }
          },
          "required": [
            "note",
            "unicode",
            "nested",
            "long"
          ]
        }
      }
    }
  ],
  "temperature": 0,
  "max_tokens": 800
}

What came back

The other providers' note, decoded from the returned arguments JSON — 38 bytes, escapes preserved as text:

note · nine-provider consensus (38 bytes)
line1\nline2\t"quoted" and \\backslash

Both Bedrock lanes, byte-identical to each other — 35 bytes, real control characters: a real line feed (0x0A) after line1, a real tab (0x09) after line2, and a single backslash (0x5C):

note · both Bedrock APIs (35 bytes; ␊ = 0x0A, ␉ = 0x09)
line1␊line2␉"quoted" and \backslash

The stray content

Chat lane: "content": "thought" beside the call. Responses lane: a second output item with text " grenades" (leading space included). The gemma-4 template's output grammar makes assistant text alongside a tool call unrepresentable in either order — so these fragments cannot be model output; they are the provider's decoder mis-splitting the template's channel markers (thought is the bare word from the reasoning-block opener). Reproducible: the same stray thought appears on both Bedrock lanes in the parallel-calls probe too. All eight other providers return content: "".

How this breaks expectations

An argument's bytes are the application's data. Two lanes disagreeing with the nine-provider consensus means the same model call yields different bytes depending on provider — any argument whose bytes matter (embedded JSON, code, regexes) changes meaning in transit. The stray content additionally pollutes the answer channel on every tool call.

Why it matters

JSON.parse(arguments) gives your tool different data on Bedrock than everywhere else; agents that render or log content show fragments of template markup.

Run it yourself

curl
curl "$BASE_URL/chat/completions" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d @record-payload.chat.json

POST the body to both a Bedrock lane and any other provider; diff the decoded note bytes, and watch content on Bedrock come back non-empty.