fix(translator): normalize Cursor Anthropic tool blocks on OpenAI endpoint#3804
fix(translator): normalize Cursor Anthropic tool blocks on OpenAI endpoint#3804HatriGt wants to merge 10 commits into
Conversation
|
This pull request targeted The base branch has been automatically changed to |
There was a problem hiding this comment.
Code Review
This pull request introduces normalization of Anthropic-native request blocks (such as tool_use, tool_result, and bare tool definitions) into standard OpenAI shapes within the ConvertOpenAIRequestToClaude translation pipeline, along with corresponding unit tests. The reviewer suggests optimizing performance in the normalization logic by using string types with sjson.Set and sjson.SetRaw instead of []byte with sjson.SetBytes and sjson.SetRawBytes to avoid unnecessary byte slice allocations and conversions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d5f0f77b0a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
d5f0f77 to
162200e
Compare
|
Thanks for the reviews. Addressed both in the latest push (rebased onto @chatgpt-codex-connector (P2 — preserve typed Anthropic tools): Fixed. The wrapper now only treats tools with no @gemini-code-assist (perf — string vs []byte): Done. Switched the message/tool accumulators to
|
|
To use Codex here, create a Codex account and connect to github. |
|
Thanks for the update, HatriGt. The approach of using |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 162200e794
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Heads up: this PR fails the
I've followed that process and filed #3805 with the full context, repro, rationale, and the complete proposed diff inlined. I'll leave this PR open as ready-to-merge reference code (build + |
…onversion Addresses the follow-up Codex review on PR router-for-me#3804. The normalizer already left typed Anthropic server tools (e.g. web_search_20250305) untouched, but the downstream tool mapper in ConvertOpenAIRequestToClaude only emitted type=="function" tools, silently dropping typed server tools from the final Claude request when sent alongside a bare custom tool. Pass typed server tools (type present, not "function") through verbatim into the Claude tools array. Add TestConvertOpenAIRequestToClaude_TypedServerToolPreserved to assert end-to-end survival through the full conversion, not just the normalizer.
80d61a3 to
ac4017e
Compare
|
@sususu98 @luispater can you review this? |
|
Please rebase on the dev branch first |
…onversion Addresses the follow-up Codex review on PR router-for-me#3804. The normalizer already left typed Anthropic server tools (e.g. web_search_20250305) untouched, but the downstream tool mapper in ConvertOpenAIRequestToClaude only emitted type=="function" tools, silently dropping typed server tools from the final Claude request when sent alongside a bare custom tool. Pass typed server tools (type present, not "function") through verbatim into the Claude tools array. Add TestConvertOpenAIRequestToClaude_TypedServerToolPreserved to assert end-to-end survival through the full conversion, not just the normalizer.
ca61338 to
7226b49
Compare
|
Rebased onto latest Ready for another look — thanks @sususu98. |
|
Thanks for rebasing. Before we review this further, please drop commit The Antigravity Claude Code built-in WebSearch path is broader than this fix, and we plan to support that through the plugin mechanism instead. It should not be merged as part of this PR. Once the branch is narrowed, please open or refresh the PR and we can review it again. Thanks! |
…point Cursor in agent/tool mode sends Anthropic-native content blocks (tool_use / tool_result) and bare tool definitions to the OpenAI Chat Completions endpoint. These are not valid OpenAI format, so ConvertOpenAIRequestToClaude produced an empty/invalid Claude request, causing errors such as "messages.N: user messages must have non-empty content" and silently broken tool calling (see router-for-me#1165). Add normalizeAnthropicRequestBlocks as a pre-processing pass that rewrites: - user content tool_result blocks -> standalone role:"tool" messages - assistant content tool_use blocks -> assistant tool_calls[].function - bare {name, description, input_schema} tools -> {type:"function", function:{...}} Standard OpenAI payloads are detected and pass through untouched. This removes the need for an external request-rewriting proxy/shim when using Cursor agent mode with Claude via OAuth.
…tion - Preserve typed Anthropic server tools (e.g. web_search_20250305): only wrap tools that have NO "type" field as OpenAI function tools, so typed server tools are left intact for downstream mapping instead of being rewritten into a custom tool with no schema (codex review P2). - Use string accumulators with sjson.Set/SetRaw instead of []byte + SetBytes/SetRawBytes to avoid unnecessary allocations (gemini review). - Add TestNormalizeAnthropicRequestBlocks_TypedToolNotWrapped regression test.
…onversion Addresses the follow-up Codex review on PR router-for-me#3804. The normalizer already left typed Anthropic server tools (e.g. web_search_20250305) untouched, but the downstream tool mapper in ConvertOpenAIRequestToClaude only emitted type=="function" tools, silently dropping typed server tools from the final Claude request when sent alongside a bare custom tool. Pass typed server tools (type present, not "function") through verbatim into the Claude tools array. Add TestConvertOpenAIRequestToClaude_TypedServerToolPreserved to assert end-to-end survival through the full conversion, not just the normalizer.
7226b49 to
15b29e2
Compare
|
Done — dropped commit The branch is now rebased onto
No Antigravity / WebSearch code remains. Ready for review again — thanks @sususu98. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15b29e2a02
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Cursor/Anthropic parallel tool calls arrive as consecutive role:"tool" messages. The Claude mapper emitted a separate user turn per tool_result, breaking the tool_use/tool_result pairing Claude requires (all results for the preceding assistant tool_use turn must share one user message). Append consecutive tool_result blocks to the prior user/tool_result turn instead of creating a new message. Add a parallel-tool-results test.
|
Addressed the Codex P2 about parallel tool results (commit Problem: Cursor runs parallel tool calls — one assistant turn with multiple Fix: When mapping a Added Branch still contains only the Cursor/Anthropic tool-block normalization changes — no Antigravity code. Thanks @sususu98. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e19366d1d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Cursor forwards failed tool executions as Anthropic tool_result blocks with is_error:true. The normalizer dropped that flag when synthesizing the OpenAI role:"tool" message, so the downstream Claude tool_result lost it and a failed tool call looked like a success. Carry is_error through the synthesized message and reconstruct it on the Claude tool_result block. Add a test.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dcd87bd806
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
A Cursor tool_result.content can be an array of Claude-native blocks (image with source, tool_reference, etc.). Routing it through the OpenAI content-part converter dropped unknown blocks or stringified them. Flag native arrays in the normalizer and copy them verbatim into the Claude tool_result instead. Add a test.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 05be4cb271
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Extended-thinking models emit thinking / redacted_thinking blocks before the tool_use in an assistant turn. The normalizer rebuilt the assistant message from only text and tool_use, dropping the signed thinking blocks. Anthropic requires prior thinking blocks to be returned unchanged on subsequent tool-result requests, so carry them through and lead the Claude assistant content with them. Add a test.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa4a488675
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa4a488675
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The typed-tool passthrough forwarded any non-function OpenAI tool type to
Claude. Unversioned OpenAI built-ins (e.g. {"type":"web_search"}) are not
Claude-native and trigger an upstream 400 instead of being ignored as before.
Restrict passthrough to Anthropic versioned server tools (type ending in
_YYYYMMDD); drop other non-function types. Add a test.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 279bd73bc8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
After a Claude server tool (e.g. web_search_20250305) runs, the assistant turn carries server_tool_use and *_tool_result blocks. The normalization predicate ignored them, so the OpenAI content converter later dropped them, erasing server-tool context on the next turn. Recognize these block types, carry them through verbatim, and re-emit them in the Claude assistant content. Add a test.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 84a08e4a82
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Cursor sends bare Anthropic tools alongside a native forced tool_choice
{"type":"tool","name":...}, plus auto/any/none. The mapper only handled
OpenAI's {"type":"function",...} object form, so the forced selection was
silently dropped and fell back to auto. Handle the Anthropic shapes directly.
Add a test.
|
@codex review |
1 similar comment
|
@codex review |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
|
@sususu98 this is ready for review. All Codex findings are addressed and its latest pass on What it doesCursor agent mode sends Anthropic-native shapes to the OpenAI endpoint. This normalizes them into valid Claude requests: Core normalization
Edge cases hardened from review
TestingDedicated tests for each case in Thanks for the thorough review — happy to adjust anything. |
|
@luispater @sususu98 kindly review this. |
@luispater @sususu98 any update |
|
Thanks for the reference implementation. We are closing this PR because sending Anthropic-native content blocks to the OpenAI Chat Completions endpoint is a client-side protocol error. CLIProxyAPI will not rewrite or correct that invalid client behavior on this endpoint. Please report this to Cursor so they can send a valid OpenAI Chat Completions payload when using an OpenAI-compatible endpoint. Community feedback also suggests a possible workaround: alias |
Problem
Cursor in agent / tool mode sends Anthropic-native content blocks to the OpenAI
/v1/chat/completionsendpoint instead of OpenAI-format tool messages. Specifically:user.contentarrays containing{ "type": "tool_result", "tool_use_id": ..., "content": ... }assistant.contentarrays containing{ "type": "tool_use", "id": ..., "name": ..., "input": {...} }{ "name", "description", "input_schema" }(notype: "function"wrapper)ConvertOpenAIRequestToClaudeassumes valid OpenAI shapes (tool_calls,role:"tool",tools[].type=="function"), so these payloads were dropped, producing an invalid Claude request and errors like:or silently broken tool calling. This matches #1165 and the Cursor community report "Wrong tools handling on OpenAI compatible endpoint".
Fix
Add
normalizeAnthropicRequestBlocks, a pre-processing pass at the top ofConvertOpenAIRequestToClaudethat rewrites Anthropic-native shapes into standard OpenAI shapes before the existing translation runs:usercontenttool_resultblocks -> standalonerole:"tool"messages (tool_call_id,content); sibling text blocks are preserved as a trailingusermessage.assistantcontenttool_useblocks ->assistant.tool_calls[].function(text blocks merged intocontent).{ name, description, input_schema }tools ->{ type:"function", function:{ name, description, parameters } }.Standard OpenAI payloads are detected via
anthropicBlocksPresentand pass through untouched (no behavior change for existing clients).Why this matters
Without this, Cursor users must run an external request-rewriting proxy/shim in front of CLIProxyAPI to use agent mode with Claude over OAuth. This fix makes the OpenAI endpoint accept Cursor's payloads natively.
Tests
Added
claude_openai_normalize_test.go:TestConvertOpenAIRequestToClaude_CursorToolResultBlock- full assistanttool_use+ usertool_resultround tripTestConvertOpenAIRequestToClaude_CursorToolResultWithText- mixedtool_result+ trailing textTestConvertOpenAIRequestToClaude_BareAnthropicTools- bare tool definition wrappingTestConvertOpenAIRequestToClaude_StandardOpenAIUnchanged- regression guard for normal OpenAI payloadsRefs #1165