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):
{
"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
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 "$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.