anthropics / anthropics/anthropic-sdk-php

Bedrock: body round-trip flattens empty JSON objects ({} -> []), so a no-argument tool_use gets a 400

Open
#70 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
PHP
Stars
180
Forks
54
Avg merge
12h 17m
Merged PRs (30d)
6

Description

**Environment**

- `anthropic-ai/sdk` v0.42.0 (latest; the code is unchanged on `main`)
- PHP 8.3, Bedrock transport (`Anthropic\Bedrock`), EU inference profile for Claude Sonnet

**Summary**

`RequestTransformer::decodedBody()` decodes the request body with `json_decode($body, true)` ([src/Core/RequestTransformer.php#L98](https://github.com/anthropics/anthropic-sdk-php/blob/main/src/Core/RequestTransformer.php#L98)). A PHP associative array cannot represent an empty JSON object. When `serialize()` re-encodes the body ([#L162](https://github.com/anthropics/anthropic-sdk-php/blob/main/src/Core/RequestTransformer.php#L162)), every `{}` in the request becomes `[]`.

The direct Anthropic API path does not show this: the transformer re-encodes only when the body was modified (`bodyDirty`). But `BedrockMiddleware` always rewrites the body (it moves `model` into the URL and injects `anthropic_version`), so on Bedrock every request is re-encoded and every empty object flattens.

**Impact**

A multi-turn conversation that replays an assistant `tool_use` block with an empty `input` (any tool that takes no arguments) is rejected by Bedrock with a 400 whose validation error points at `messages.N.content.M.tool_use.input`. The same request succeeds on `api.anthropic.com`.

The same flattening hits every other empty object in the body, for example a no-parameter tool's `input_schema.properties: {}` or an empty `metadata: {}`.

This makes the failure intermittent and hard to diagnose in an agent loop: simple turns work, and only a turn whose history contains a no-argument tool call fails.

**Repro**

The core defect in isolation:

```php
$body = '{"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"list_stores","input":{}}]}]}';
echo json_encode(json_decode($body, true));
// ...,"input":[]}]}]} — Bedrock rejects this
```

Full path: send a Messages request through the Bedrock client with a conversation history that contains an assistant `tool_use` block whose `input` is `{}`. Bedrock returns a 400; the direct API accepts the identical request.

**Suggested fix**

Decode without `assoc` and keep nested values as `stdClass`:

```php
$decoded = (array) json_decode((string) $stream, false);
```

The transformer reads and writes only top-level params (`model`, `anthropic_version`, `anthropic_beta`, `stream`), so the top-level array cast keeps the current API, and nested `stdClass` values re-encode as `{}` correctly. (`BedrockMiddleware`'s `base64_encode(json_encode($r->getBody(), ...))` for streaming also re-encodes nested objects correctly then.)

**Workaround we use**

A PSR-18 decorator on the Bedrock transporter restores the structural `"input":[]` back to `"input":{}` and then re-signs the request with SigV4 — the middleware signs before the transporter runs, so any body change after it must re-sign or AWS rejects the payload hash.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.