# AWS Bedrock un-escapes tool-call arguments in transit

medium · inference · probed live 2026-07-26 · affects AWS Bedrock (Chat Completions),
AWS Bedrock (Responses) ·
HTML version: [https://inferencecanary.com/bedrock-argument-corruption](https://inferencecanary.com/bedrock-argument-corruption)

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
([https://inferencecanary.com/assets/repro/record-payload.chat.json](https://inferencecanary.com/assets/repro/record-payload.chat.json)):

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

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

```
line1␊line2␉"quoted" and \backslash      (␊ = 0x0A, ␉ = 0x09)
```

## 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](https://inferencecanary.com/bedrock-parallel-tool-call.md) 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 "$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.
