Skip to content

[Bug][2.39.0] Responses Lite exec is returned with namespace "exec", causing an execexec tool-call loop #3217

Closed

Description

@alex-jordan547

Client or integration

Codex CLI

The same endless “Thinking” behavior was first observed in Codex App.

Area

Tools / MCP / web search

Summary

When gpt-5.3-codex-spark is used through OpenCodex, text-only turns complete, but any turn that invokes the Codex exec tool enters an endless loop.

The Codex request uses the Responses Lite additional_tools format and correctly declares a reserved functions namespace containing a custom tool named exec. OpenCodex returns the tool call with both name: "exec" and namespace: "exec". Codex combines those fields into execexec, cannot route that tool, and repeatedly reports:

unsupported custom tool call: execexec

The equivalent native Codex request, with the OpenCodex configuration bypassed using --ignore-user-config, executes pwd successfully and completes. I expected the proxied request to preserve the reserved functions namespace semantics and return the custom exec call without inventing an exec namespace.

This reproduces consistently after upgrading OpenCodex from 2.24.2 to 2.39.0, restarting the service, running ocx sync --restart-codex, and starting a fresh ephemeral task.

Reproduction

  1. Configure Codex to use OpenCodex with the native OpenAI forward route.

  2. Confirm the current versions:

    ocx --version
    # opencodex 2.39.0
    
    codex --version
    # codex-cli 0.150.1
  3. Restart and resynchronize OpenCodex/Codex:

    ocx service restart
    ocx sync --restart-codex
  4. Run a minimal tool-use turn through OpenCodex:

    codex exec \
      --ephemeral \
      --json \
      --sandbox read-only \
      --skip-git-repo-check \
      --model gpt-5.3-codex-spark \
      'Use the shell tool exactly once to execute pwd, then return only the resulting path.'
  5. Codex announces the command, then repeatedly logs:

    ERROR codex_core::tools::router: error=unsupported custom tool call: execexec
    
  6. Run the same request while bypassing the OpenCodex configuration:

    codex exec \
      --ignore-user-config \
      --ephemeral \
      --json \
      --sandbox read-only \
      --skip-git-repo-check \
      --model gpt-5.3-codex-spark \
      'Use the shell tool exactly once to execute pwd, then return only the resulting path.'
  7. The native request succeeds immediately:

    {"type":"item.started","item":{"type":"command_execution","command":"/bin/zsh -lc pwd","status":"in_progress"}}
    {"type":"item.completed","item":{"type":"command_execution","command":"/bin/zsh -lc pwd","exit_code":0,"status":"completed"}}
    {"type":"turn.completed"}

Redacted wire evidence

A local pass-through capture between Codex and OpenCodex recorded only protocol types and tool names—no prompts, credentials, headers, account identifiers, or full request bodies.

The inbound Responses Lite request contains an additional_tools item with the expected reserved namespace:

{
  "type": "additional_tools",
  "tools": [
    {
      "type": "namespace",
      "name": "functions",
      "tools": [
        { "type": "custom", "name": "exec" },
        { "type": "function", "name": "wait" },
        { "type": "function", "name": "request_user_input" }
      ]
    }
  ]
}

The response emitted by OpenCodex contains an unexpected namespace equal to the tool name:

{
  "type": "response.output_item.added",
  "item": {
    "type": "custom_tool_call",
    "name": "exec",
    "namespace": "exec",
    "status": "in_progress"
  }
}

The same incorrect pair is present in response.output_item.done:

{
  "type": "response.output_item.done",
  "item": {
    "type": "custom_tool_call",
    "name": "exec",
    "namespace": "exec",
    "status": "completed"
  }
}

Codex then reports unsupported custom tool call: execexec and starts another model turn, producing the same invalid call repeatedly.

Version

@bitkyc08/opencodex 2.39.0

Operating system

macOS 27.0 (build 26A5425a), arm64

Provider and model

openai / gpt-5.3-codex-spark

  • Adapter: openai-responses
  • Authentication mode: native forward route
  • Codex CLI: 0.150.1

Logs or error output

ERROR codex_core::tools::router: error=unsupported custom tool call: execexec
ERROR codex_core::tools::router: error=unsupported custom tool call: execexec
ERROR codex_core::tools::router: error=unsupported custom tool call: execexec

OpenCodex request logs show the initial Spark response completing upstream with HTTP 200, while Codex rejects the returned client tool call locally.

Screenshots and supporting files

A Codex App task shows “Thinking” indefinitely after announcing that it will inspect the worktree. The minimal CLI reproduction above isolates the same failure without relying on the UI.

Redacted configuration

{
  "defaultProvider": "openai",
  "providers": {
    "openai": {
      "adapter": "openai-responses",
      "authMode": "forward",
      "baseUrl": "https://chatgpt.com/backend-api/codex"
    }
  }
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, personal paths, call IDs, conversation IDs, and full request bodies.

Activity

  1. added
    serviceService lifecycle (WinSW/launchd/scheduler)
    toolstool_calls, MCP, web-search / sidecar tools
    on Sep 1, 2026
  2. github-actions commented on Sep 1, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  3. lidge-jun commented on Sep 1, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 76 / 80

    이 이슈는 Codex CLI/App가 OpenCodex 네이티브 forward(openai-responses + authMode: forward)로 gpt-5.3-codex-spark를 쓸 때, Responses Lite의 additional_tools 안에 예약 네임스페이스 functions로 선언된 커스텀 도구 exec이 왕복하면서 name: "exec"와 함께 namespace: "exec"로 돌아오고, Codex 라우터가 둘을 이어 붙여 execexec로 해석한 뒤 unsupported custom tool call: execexec를 반복해 턴이 끝나지 않는 버그다. 같은 명령을 --ignore-user-config로 OpenCodex를 우회하면 pwd가 한 번에 끝나는 대조가 이미 붙어 있어서, 원인은 업스트림 Spark 모델 자체보다 프록시가 Spark용으로 요청 도구 카탈로그를 다시 쓰는 경로에 가깝다. 현재 dev(HEAD f84dbf91e, package 2.40.0)에서도 재현 골격은 그대로다.

    현재 체크아웃에서 핵심은 src/adapters/openai-responses.ts의 stripSparkCompatibility다. 모델 이름에 codex-spark가 들어가면 body.tools와 input 안의 additional_tools에 대해 type: "namespace" 그룹을 풀어서 자식 도구만 위로 올린다. 이슈에 적힌 요청 모양(functions → custom exec + function wait/request_user_input)을 forward 어댑터로 돌려 보면, 아웃바운드 additional_tools.tools는 예약 그룹이 사라지고 맨 위가 { type: "custom", name: "exec" }인 평면 목록이 된다. 반면 src/responses/parser.ts의 buildTools와 customToolNamespaces는 Codex 0.147 계약을 알아채고 functions 자식은 네임스페이스 없이 납작하게 두도록 이미 짜여 있다. 즉 파서/가드 쪽은 functions를 특수 취급하는데, Spark 호환 스트립만 그 예약을 무시하고 그룹을 해체한다. 네이티브 Codex가 ChatGPT 백엔드로 같은 Lite 모양을 그대로 보내면 성공하니, canonical forward에서는 Spark 스트립이 네임스페이스를 건드리면 안 된다는 쪽이 설득력 있다.

    응답 쪽도 같이 보면 된다. buildToolBridgeMaps(src/server/responses/collaboration.ts)는 functions에서 풀린 exec에 대해 toolNsMap 항목을 만들지 않는다(로컬에서 동일 입력으로 toolNsMap이 비는 것을 확인). 그래서 restoreRoutedNamespaceCalls / authorizedBareNamespaceToolAliases가 namespace: "functions"를 되살릴 기회도 없고, 업스트림이 namespace: "exec"를 실어 오면 그대로 클라이언트로 나간다. Codex는 MCP식 namespace+name 결합으로 execexec를 만들고, 라우터가 거절 → 모델이 같은 호출을 다시 내는 루프가 된다. 텍스트만 있는 턴은 도구 호출이 없어 통과하고, exec가 필요한 턴만 죽는 증상과도 맞다.

    라인 ~477-570 (src/adapters/openai-responses.ts · stripSparkCompatibility) - 모델명에 codex-spark가 있으면 additional_tools 안의 type: "namespace", name: "functions"까지 자식만 끌어올려 예약 그룹을 없앤다. canonical forward에서도 이 함수가 공통 sanitize 체인에 들어 있어(대략 buildRequest 끝의 stripSparkCompatibility(...) 호출), ChatGPT가 이해하는 Codex private 모양이 깨진다.

    라인 ~490-534 (stripSparkCompatibility 네임스페이스 펼치기) - 상위 tools 배열에서는 SPARK_SAFE_TOOL_TYPES에 없는 custom을 버리지만, 네임스페이스 자식은 검사 없이 filteredInner/flattened에 넣는다. 그래서 functions 안의 custom exec는 “지원 안 함” 주석과 달리 살아남은 채 그룹 정보만 잃는다. Spark 정책(커스텀 드롭 vs 그룹 유지)이 한쪽으로 정리되어 있지 않다.

    라인 ~221-231 (src/responses/parser.ts · buildTools) / ~385-388 (customToolNamespaces) - 여기는 반대로 functions 자식을 네임스페이스 없이 두어 “없는 네임스페이스를 발명하지 말라”고 명시한다. Spark 스트립이 이 계약을 어기면, 요청은 평면 custom exec가 되고 응답의 잘못된 namespace: "exec"를 고칠 별칭도 없다.

    심볼 restoreRoutedNamespaceCalls / authorizedBareNamespaceToolAliases (src/responses/namespace-tool-compat.ts, src/server/responses/core.ts) - functions 자식은 alias 맵에 안 들어가므로, 클라이언트로 나가는 custom_tool_call의 namespace: "exec"를 여기서 바로잡지 못한다. 응답 sanitize(예약 builtin이면 namespace 생략 또는 functions로 정규화, namespace === name 거부)가 빠져 있다.

    테스트 공백 - tests/openai-responses-passthrough.test.ts에 Spark hosted tool 제거 등은 있으나, Responses Lite additional_tools + functions/exec + gpt-5.3-codex-spark + forward 조합에서 아웃바운드가 예약 네임스페이스를 유지하는지, 인바운드 custom_tool_call이 namespace: "exec"로 나가지 않는지를 고정한 회귀가 없다. 이슈의 최소 codex exec ... pwd 재현을 그 테스트의 계약으로 옮기면 된다.

    메인테이너의 판단이 필요한 지점

    • Spark 스트립을 canonical OpenAI forward에서는 통째로 건너뛸지, 아니면 forward에서도 돌리되 예약 functions 네임스페이스만 펼치지 않을 정해야 한다. 전자는 ChatGPT private 모양을 최대한 보존하고, 후자는 Spark가 정말 못 받는 다른 네임스페이스/tool_search만 남긴다.
    • 응답 경로에 방어를 둘지: 업스트림이 이미 namespace: "exec"를 준 뒤에도 builtin exec/wait 등에 대해 namespace를 제거하거나 functions로 고칠지. 요청 수정만으로 업스트림이 고쳐진다는 증거가 아직 와이어에 없다.
    • 이슈 본문 버전은 2.39.0이고 dev는 2.40.0이다. Spark 스트립 자체는 HEAD에 남아 있으니 재현 가능성은 높지만, 릴리즈 노트/사용자에게 2.40 재확인을 한 줄 부탁할지는 선택이다.
    • types.ts/config.ts 분할 캠페인과는 무관한 런타임 버그라서 close-don't-rebase 대상이 아니다.

    너의 추천

    1. stripSparkCompatibility에서 예약 functions 그룹은 펼치지 않거나, isCanonicalOpenAiForwardProvider일 때는 네임스페이스/additional_tools 변형을 no-op으로 둔다. 2) 회귀 테스트로 Lite additional_tools + functions/exec + gpt-5.3-codex-spark + forward 아웃바운드에 name: "functions" 네임스페이스가 남는지 단언한다. 3) 가능하면 응답 sanitize로 custom_tool_call의 namespace === "exec"(또는 namespace===name인 builtin)를 제거하고, 가드로 execexec가 클라이언트에 안 나가게 한다. 4) 수정 PR이 dev에 닿으면 이 이슈를 닫고, 사용자에게 2.40+ / ocx sync 후 동일 pwd 재현으로 확인을 요청한다.

    이 댓글은 grok-bot이 작성했습니다

  4. lidge-jun commented on Sep 1, 2026

    @lidge-jun
    Owner

    Fixed on dev by #3224 → d23eab43a. Thank you for the precise wire capture — it pointed straight at the cause.

    What was happening: for *codex-spark* models the proxy's Spark compatibility layer flattened every namespace group in additional_tools, including the reserved functions group Codex 0.147+ uses on Responses Lite. Given the flat custom exec declaration, the backend answered custom_tool_call { name: "exec", namespace: "exec" }, and codex-rs concatenates a non-default namespace into execexec. Bypassing the proxy sends the group intact, which is why --ignore-user-config worked. Reproduced locally with ocx 2.40.0 + codex 0.150.1 and traced on both sides of the proxy.

    The fix keeps the functions group as a group (MCP-style groups still flatten) and additionally scrubs any tool-call namespace that merely repeats the call's own name on the way back. With the patched proxy the same codex exec … --model gpt-5.3-codex-spark turn runs pwd and completes.

    It will ship in the next release; until then bunx @bitkyc08/opencodex@dev or a checkout of dev carries it. Closing as landed on dev.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingserviceService lifecycle (WinSW/launchd/scheduler)toolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions