Repository navigation
Conversation
Chat Completions history encoded each earlier tool call's arguments with
JSONEncoder, which writes GeneratedContent's Codable form ({"kind":…})
instead of the arguments' JSON. Models that see that shape in the history
copy it into their next tool calls, which then fail to decode. The
Responses variant converts from the same message, so it was affected too.
Use call.arguments.jsonString, as OpenResponsesLanguageModel does.
Fixes huggingface#324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat Completions sent a round of tool calls one way while the turn was in flight (the model's raw message: its argument text, content "") and another when the next turn replayed the transcript (re-encoded arguments, content null). The prompt stopped matching at the first tool call, so a provider's prompt cache (Anthropic's is an exact prefix match) missed everything after it on every later turn. Build both from the transcript's calls with one helper. Arguments are serialized with sorted keys: jsonString goes through an unordered dictionary, so the same arguments could come out in a different order from one request to the next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4WBsc1Jp8fGufe89MhQFB
There was a problem hiding this comment.
🟡 Changes recommended
Non-empty tool-call text still differs between in-flight and replayed messages, defeating prompt-cache stability.
1 open finding
What changed in this PR
Fixes OpenAI tool-call replay by using stable plain JSON and aligning in-flight/history message construction.
Changes:
- Canonicalizes replayed tool arguments with sorted JSON keys.
- Adds history and prompt-cache regression tests.
| File | Description |
|---|---|
OpenAILanguageModel.swift |
Unifies tool-call message serialization. |
OpenAIToolCallHistoryTests.swift |
Tests plain JSON replay and message consistency. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+1343
to
+1344
| /// `text` is what the model wrote before calling the tools in this round; the transcript | ||
| /// keeps it in the final response instead, so replayed rounds pass `nil`. |
Collaborator
|
Thanks, @gtokman. CI is green. Copilot caught one more case (here): when the model writes text alongside its tool calls, the in-flight message sends that text as |
When the model writes text alongside its tool calls, the in-flight message sent that text as content, but transcript replay passed nil. Store the per-round text on ToolCalls.providerMetadata so both paths use the same helper and send the same content (or null when empty). Cover the non-empty case in the streaming and non-streaming history tests. Co-authored-by: Gary Tokman <yo@garytokman.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes #324, and a prompt-caching miss it uncovered.
1. Replayed arguments were
GeneratedContent'sCodableformOpenAILanguageModelencoded each earlier tool call's arguments withJSONEncoder().encode(call.arguments), which writes{"kind":{"type":"structure","properties":…}}rather than the arguments' JSON. Models that see that shape in the history copy it into their next tool calls, which then fail to decode and end the response. The.responsesvariant builds itsfunction_callitems from the same message, so it was affected too.2. A tool round was sent differently in flight and in history
During a turn, the assistant message carrying tool calls was the model's raw message (its own argument text,
"content": ""). On the next turn, the same round was rebuilt from the transcript (re-encoded arguments,"content": null). The request stopped matching the previous one at the first tool call, so a provider's prompt cache (Anthropic's is an exact-prefix match) missed everything after it on every later turn. We measured Claude reading only the system prompt from cache on turn 2 of a conversation, instead of all of turn 1.Both are now built by one helper from the transcript's calls (Chat Completions; the Responses variant replays output items and is unchanged). Arguments are serialized with sorted keys, because
jsonStringgoes through an unordered[String: Any], so the same arguments could otherwise come out in a different key order from one request to the next. That is also why the second test failed intermittently before the sort, depending on which suites ran alongside it.Tests
OpenAIToolCallHistoryTests(ownURLProtocol, likeOllamaRequestHistoryTests):replayedToolCallArgumentsArePlainJSON: both API variants replay{"city":"Paris"}. Fails onmain.toolCallMessageIsTheSameInFlightAndInHistory: streaming and non-streaming, a tool whose arguments the model writes in the opposite order from its schema. The tool-call message in the in-flight request equals the one in the next turn's request. Fails without the change.swift test --filter "OpenAI|Transcript|ProviderUsage|ProviderSchema"passes (114 tests), andswift format lint --strictis clean on the changed files.🤖 Generated with Claude Code
https://claude.ai/code/session_01B4WBsc1Jp8fGufe89MhQFB