Skip to content

Codex Responses→Chat Completions proxy forwards tool_choice without tools, causing strict upstream 503#3640

Merged
farion1231 merged 1 commit into
farion1231:mainfrom
Postroggy:fix/codex-chat-drop-tool-choice-when-no-tools
Jun 4, 2026
Merged

Codex Responses→Chat Completions proxy forwards tool_choice without tools, causing strict upstream 503#3640
farion1231 merged 1 commit into
farion1231:mainfrom
Postroggy:fix/codex-chat-drop-tool-choice-when-no-tools

Conversation

@Postroggy

Copy link
Copy Markdown
Contributor

Self Checks / 自检

  • I have read the FAQ section in README.
  • I have searched for existing issues, including closed ones.

Related App / 涉及应用

Codex

Problem or Motivation / 问题或动机

When a user configures a third-party provider that only supports the OpenAI Chat Completions API for Codex, CC Switch's proxy layer converts Codex's Responses API requests into Chat Completions requests before forwarding them upstream.

The request flow is:

Codex client
  → POST /v1/responses  (Responses API format)
  → CC Switch proxy (converts Responses → Chat Completions)
  → upstream /v1/chat/completions  (Chat Completions format)
  → upstream rejects with 503: "When using `tool_choice`, `tools` must be set."

The problem: during this Responses→Chat Completions conversion, tool_choice is unconditionally forwarded even when the tools array is absent or empty in the converted request. Strict OpenAI-compatible upstreams (vLLM, enterprise gateways, OneAPI instances, etc.) reject this field combination.

Root Cause / 根因分析

In src-tauri/src/proxy/providers/transform_codex_chat.rs, the function responses_to_chat_completions_with_reasoning() performs the request conversion. The relevant field handling:

  1. tools — only written to the output when the converted tool list is non-empty (lines 309-311). If no valid tools survive conversion (e.g., missing name, unsupported type, empty namespace), the field is omitted entirely.
  2. tool_choice — unconditionally copied from the original Responses request whenever it is present (lines 313-314).
  3. parallel_tool_calls — forwarded as a passthrough field via EXTRA_CHAT_PASSTHROUGH_FIELDS (lines 317-321).

These three fields are handled independently with no constraint that tool_choice and parallel_tool_calls require a non-empty tools array.

Triggering conditions (any one of the following):

  • Codex sends tool_choice in the Responses request, but tools is absent or empty
  • tools exists in the request but all entries are filtered out during conversion (e.g., function tool with no name, namespace tool with no children)
  • tool_search_output in input provides no loadable tools, yet tool_choice was set in the original request

Full Error Message / 完整报错

Unexpected status 503 Service Unavailable:
CC Switch local proxy failed while handling Codex endpoint /responses.
Provider: cvte-自己; model: qwen3-7-max;
upstream_status: HTTP 503;
cause: When using `tool_choice`, `tools` must be set.
cch_session_id: sess_mpxf1b6v_33026b582c6f
url: http://127.0.0.1:15721/v1/responses

Upstream Reproduction / 上游复现

Sending a Chat Completions request directly to a strict upstream with tool_choice but without tools reproduces the same rejection:

# Case 1: tool_choice without tools → upstream rejects
curl -X POST "$BASE_URL/v1/chat/completions" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-7-max",
    "messages": [{"role": "user", "content": "hi"}],
    "tool_choice": "auto"
  }'
# → 503: "When using `tool_choice`, `tools` must be set."

# Case 2: no tool_choice, no tools → upstream accepts normally
# → 200 OK

# Case 3: tool_choice with tools → upstream accepts normally
# → 200 OK

Proposed Solution / 建议方案

After forwarding all passthrough fields, add a guard: if tools is absent or empty in the converted request, remove tool_choice and parallel_tool_calls.

let has_tools = result
    .get("tools")
    .is_some_and(|v| v.as_array().is_some_and(|a| !a.is_empty()));
if !has_tools {
    if let Some(obj) = result.as_object_mut() {
        obj.remove("tool_choice");
        obj.remove("parallel_tool_calls");
    }
}

Why not send "tools": []? Some strict upstreams also reject an empty tools array when tool_choice is present. Removing tool_choice entirely is the safest approach.

Changes Made / 已实施的改动

File: src-tauri/src/proxy/providers/transform_codex_chat.rs

  1. Core fix: added a guard block after the EXTRA_CHAT_PASSTHROUGH_FIELDS loop that drops tool_choice and parallel_tool_calls when tools is absent or empty
  2. 9 regression tests covering:
    • tool_choice dropped when tools is absent
    • tool_choice dropped when tools is an empty array
    • parallel_tool_calls dropped when tools is absent
    • tool_choice dropped when all tools are filtered out during conversion (e.g., function tool with no name)
    • tool_choice preserved when tools is present and non-empty
    • Function-type tool_choice preserved and correctly name-mapped when tools is present
    • Clean output when neither tool_choice nor tools are present in the original request
    • tool_choice: "none" also dropped when no tools (strict upstreams reject any tool_choice value without tools)
    • tool_search_output in input providing tools keeps tool_choice intact

Test Results / 测试结果

Full test suite: 1426 passed, 0 failed

Unit tests: 1426 passed, 0 failed, 0 ignored

Integration tests (all passed):
  - app_config_load: 4 passed
  - app_type_parse: 2 passed
  - deeplink_import: 2 passed
  - hermes_roundtrip: 2 passed
  - import_export_sync: 25 passed
  - mcp_commands: 14 passed
  - provider_commands: 8 passed
  - provider_service: 22 passed
  - proxy_commands: 2 passed
  - skill_sync: 7 passed

Code formatting: cargo fmt --check passed

Environment / 环境

  • CC Switch version: 3.16.1 (commit 693c3872)
  • OS: macOS
  • Provider type: custom strict OpenAI-compatible gateway
  • Upstream API format: Chat Completions (openai_chat)
  • Codex local routing: Enabled

Related Issues / 相关 Issue

Additional Context / 补充信息

This bug affects any strict OpenAI-compatible upstream provider (vLLM, enterprise gateways, OneAPI, LiteLLM, etc.) when used as a Chat Completions backend for Codex through CC Switch's local routing. The fix is minimal — it only removes fields that would cause upstream rejection, without altering the conversion logic for requests that already have valid tools.

…nversion

Strict OpenAI-compatible upstreams (vLLM, enterprise gateways) reject
requests that carry tool_choice or parallel_tool_calls without a
non-empty tools array, returning 503/400 with:
  "When using `tool_choice`, `tools` must be set."

The Responses→Chat Completions converter unconditionally forwarded
tool_choice and parallel_tool_calls even when tools ended up absent
or empty after conversion. This commit adds a guard that removes both
fields when the resulting tools array is missing or empty.

Added 9 regression tests covering:
- tool_choice dropped when tools absent
- tool_choice dropped when tools is empty array
- parallel_tool_calls dropped when tools absent
- tool_choice dropped when all tools filtered (e.g., missing name)
- tool_choice preserved when tools present (auto + function type)
- clean output when neither tool_choice nor tools present
- tool_choice 'none' dropped when no tools
- tool_search_output providing tools keeps tool_choice

Refs farion1231#3557
@farion1231

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@farion1231 farion1231 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution!

@farion1231
farion1231 merged commit ea95f39 into farion1231:main Jun 4, 2026
3 checks passed
AnXiYiZhi pushed a commit to AnXiYiZhi/DevCLaw-old that referenced this pull request Jun 9, 2026
…nversion (farion1231#3640)

Strict OpenAI-compatible upstreams (vLLM, enterprise gateways) reject
requests that carry tool_choice or parallel_tool_calls without a
non-empty tools array, returning 503/400 with:
  "When using `tool_choice`, `tools` must be set."

The Responses→Chat Completions converter unconditionally forwarded
tool_choice and parallel_tool_calls even when tools ended up absent
or empty after conversion. This commit adds a guard that removes both
fields when the resulting tools array is missing or empty.

Added 9 regression tests covering:
- tool_choice dropped when tools absent
- tool_choice dropped when tools is empty array
- parallel_tool_calls dropped when tools absent
- tool_choice dropped when all tools filtered (e.g., missing name)
- tool_choice preserved when tools present (auto + function type)
- clean output when neither tool_choice nor tools present
- tool_choice 'none' dropped when no tools
- tool_search_output providing tools keeps tool_choice

Refs farion1231#3557

Co-authored-by: yueqi.guo <[email protected]>
gfunc pushed a commit to gfunc/cc-switch that referenced this pull request Jun 19, 2026
…nversion (farion1231#3640)

Strict OpenAI-compatible upstreams (vLLM, enterprise gateways) reject
requests that carry tool_choice or parallel_tool_calls without a
non-empty tools array, returning 503/400 with:
  "When using `tool_choice`, `tools` must be set."

The Responses→Chat Completions converter unconditionally forwarded
tool_choice and parallel_tool_calls even when tools ended up absent
or empty after conversion. This commit adds a guard that removes both
fields when the resulting tools array is missing or empty.

Added 9 regression tests covering:
- tool_choice dropped when tools absent
- tool_choice dropped when tools is empty array
- parallel_tool_calls dropped when tools absent
- tool_choice dropped when all tools filtered (e.g., missing name)
- tool_choice preserved when tools present (auto + function type)
- clean output when neither tool_choice nor tools present
- tool_choice 'none' dropped when no tools
- tool_search_output providing tools keeps tool_choice

Refs farion1231#3557

Co-authored-by: yueqi.guo <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants