AWS Bedrock un-escapes tool-call arguments in transit

medium · inference · probed live 2026-07-26 · re-run 2026-08-25 · 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. (Through July both lanes also emitted a stray content fragment beside the call; that stopped by 2026-08-25, and this finding now covers the un-escape alone.)

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

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.

Why it matters

JSON.parse(arguments) gives your tool different data on Bedrock than everywhere else.

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.