Skip to content

[Platform] Keep forward slashes unescaped in tool call arguments - #2504

Merged
chr-hertel merged 1 commit into
symfony:mainfrom
solverat:unescape-tool-call-arguments
Sep 22, 2026
Merged

chr-hertel merged 1 commit into
symfony:mainfrom
solverat:unescape-tool-call-arguments

Conversation

@solverat

@solverat solverat commented Sep 7, 2026

Copy link
Copy Markdown
Contributor
Q A
Bug fix? yes
New feature? no
Docs? no
Issues Related to #1973 and #2087
License MIT

ToolCallNormalizer encodes a tool call's arguments with json_encode() and no flags. PHP escapes forward slashes by default, so /de/shop is written as \/de\/shop. That is valid JSON, and any decoder returns the original string.

The problem is that these arguments are not only sent to the provider. They also come back to the model as the text of its own previous call, and the model reads that text instead of decoding it. It sees a backslash, assumes the value contains one, and escapes it on the next attempt. The next call then arrives with a real backslash in the path.

This only happens after a failed call, because that is when the previous call is part of the history.

$payload = OpenAiContract::create()->createRequestPayload(new Gpt('gpt-5-mini'), new MessageBag(
    Message::ofUser('find them'),
    Message::ofAssistant(new ToolCall('call_1', 'query_assets',
        ['filters' => ['path' => ['startsWith' => '/de/shop/']]])),
));

// before: arguments === '{"filters":{"path":{"startsWith":"\/de\/shop\/"}}}'
// after:  arguments === '{"filters":{"path":{"startsWith":"/de/shop/"}}}'

#2087 asked how much impact this has: For model names the answer depends on the backend, because a router compares the string. For tool call arguments the reader is the model itself, so every provider is affected.

The fix is the same line in the two normalizers that encode arguments. The Ollama normalizer passes the array through and needs no change. Neither class had tests, so I added them.

@carsonbot carsonbot added Bug Something isn't working Platform Issues & PRs about the AI Platform component Status: Needs Review labels Sep 7, 2026
@chr-hertel

Copy link
Copy Markdown
Member

Hi @solverat, thanks for checking in, how did you discover the issue? never had a problem with that? is it reproducible?
Cheers!

@solverat

solverat commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Hi,

I ran into it while debugging a chat turn that kept failing on a path filter. The model sent /my-path/, the call failed for an unrelated reason, and its next attempt sent \/my-path\/ with a real backslash in it. I dumped the stored arguments byte by byte, the backslash was really there, so I went looking for who put it in.

The encoding side is fully reproducible, and that is the snippet in the PR. Call createRequestPayload() on any message bag that holds an assistant tool call and you get "\/my-path\/" back. No provider needed.

Whether the model then sends the backslash back varies from run to run. It needs a tool call that goes back into the history, an argument containing a slash, and a retry after a failed call. We saw it in three of five runs of the same prompt. With a single call it never happens, because the arguments never reach the model again.

@chr-hertel

Copy link
Copy Markdown
Member

Just super curious here because this change has impact horizontally across ... well, almost everywhere :D that's actually why i closed the mentioned #2087

@symfony/ai any opinion? never had that issue myself

@wachterjohannes

Copy link
Copy Markdown
Member

The encoding difference is 100% reproducible without a provider, and tests stay green. Different footing than #2087. That one's root cause was never confirmed and touched the whole body for one bridge. This touches one field but reaches most bridges through the shared normalizer. Narrower change, proven mechanism. I'd land it.

@chr-hertel

Copy link
Copy Markdown
Member

Thank you @solverat.

@chr-hertel
chr-hertel merged commit 84e51b2 into symfony:main Sep 22, 2026
62 of 64 checks passed
chr-hertel added a commit that referenced this pull request Sep 22, 2026
…n tool call arguments (chr-hertel)

This PR was merged into the main branch.

Discussion
----------

[Examples] Update replay cassettes for unescaped slashes in tool call arguments

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Docs?         | no
| Issues        | -
| License       | MIT

#2504 encodes tool call `arguments` with `JSON_UNESCAPED_SLASHES`, but the replay cassettes still hold `https:\/\/…`, so `Integration / Examples (replay)` fails on every PR since.

* Unescape slashes in the recorded tool call `arguments` of `toolbox/{tavily,brave,firecrawl-map,firecrawl-scrape}` and `openai/agent-stream-sources`
* Recompute the request signatures of the touched interactions, no re-recording needed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Commits
-------

499d0d5 [Examples] Update replay cassettes for unescaped slashes in tool call arguments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working Platform Issues & PRs about the AI Platform component Status: Reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants