You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Feature: per-provider opt-out of code_mode_only tool mode for routed models (deepseek-v4-flash: undeclared exec_command aborts stream, reconnect then fails reasoning replay with DeepSeek 400) #2106
Routed models are unconditionally stamped tool_mode = "code_mode_only", but some models (observed: deepseek/deepseek-v4-flash) ignore the code-mode nested-helper contract and call exec_command at the top level. The undeclared-tool guard then aborts the stream, the Codex app reconnects, and the replayed turn fails reasoning replay with an upstream 400:
stream disconnected before completion: routed provider emitted undeclared client tool "exec_command"; only request-declared tools may be called
The `reasoning_text` in the thinking mode must be passed back to the API.
In Codex (app or CLI) select deepseek/deepseek-v4-flash
Ask the model to run any shell command (or let it decide to use a tool)
The model calls exec_command directly (not via the code-mode exec/tools.* helper)
Stream aborts, client reconnects, reasoning replay fails with the DeepSeek 400 above
Root cause analysis
applyRoutedCodexToolMode (src/codex/catalog/parsing.ts) sets tool_mode = "code_mode_only" for every routed model with no opt-out.
Under code_mode_only, codex-rs declares the unified exec (freeform) tool; exec_command/shell_command exist only as nested tools.* helpers inside it and are not top-level declared tools.
The undeclared-tool guard (src/server/responses-undeclared-tool-guard.ts, src/bridge.ts) correctly refuses a top-level exec_command call because the codex client cannot execute an undeclared call.
The tool-catalog nudge (src/adapters/tool-catalog-nudge.ts) is advisory and is only injected on the openai-chat adapter path; the openai-responses passthrough never injects it, and the model ignored it in both paths anyway.
After the guard aborts the stream, the Codex app reconnects and replays the interrupted turn; the reasoning part of that turn is then missing/incomplete on replay, which DeepSeek's thinking mode rejects with The reasoning_text in the thinking mode must be passed back to the API. The two errors are one chain: fix the tool-mode mismatch and the 400 disappears (verified).
Suggested fix
Add a per-provider (ideally per-model) config field so operators can keep the flat shell tool mode for models that do not honor code mode:
applyRoutedCodexToolMode would honor it: when "shell", leave tool_mode unset so codex-rs declares exec_command per shell_type: "shell_command", making the model's calls declared and executable. Default stays code_mode_only for all other routed models.
Optionally, also inject the tool-catalog nudge on the openai-responses passthrough path as defense-in-depth.
Local workaround (verified)
Patched applyRoutedCodexToolMode locally to honor providers.<name>.codexToolMode === "shell" (config schema is passthrough, so no schema change needed). Verified on opencodex 2.26.0:
deepseek/deepseek-v4-flash: exec_command calls now execute (zsh -lc 'echo ...' returns output), no stream abort, no reconnect, no 400
Other routed models (e.g. kimi/*) keep tool_mode = code_mode_only and are unaffected
Two-turn continuation with reasoning replay works on both openai-chat and openai-responses adapters
#304 - fix(kiro): restore hardening and complete text turns [open] #1361 - fix(responses): restore DeepSeek streaming and routed code-mode tools [merged] #1788 - fix(responses): fail closed when a routed provider calls an undeclared tool [merged] #1951 - fix(adapters): decide code mode from tool semantics, not from the name exec [merged] #1953 - fix(adapters): restore the un-namespaced requirement on the shell-bridge check [merged]
📝 Issue Planner
Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.
Create Plan
🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your .coderabbit.yaml:
issue_enrichment:
auto_enrich:
enabled: false
💬 Have feedback or questions? Drop into our discord!
라우팅된 DeepSeek 모델이 Codex code-mode 계약을 무시하고 최상위 exec_command를 부른다. OpenCodex 가드가 스트림을 끊고, 앱이 재접속한 뒤 reasoning을 재전송하지 못해 DeepSeek가 400을 준다. 기본 native 경로가 아니라 라우티드 프로바이더 도구 호환 문제라 52다. 로컬에서 codexToolMode=shell로 우회가 검증됐고, 다른 라우티드 모델의 기본값은 그대로 두면 된다.
원인은 src/codex/catalog/parsing.ts의 applyRoutedCodexToolMode()다. ROUTED_CODEX_TOOL_MODE = "code_mode_only"를 모든 라우티드 엔트리에 무조건 찍는다. src/codex/catalog/sync.ts가 카탈로그를 쓸 때 이 함수를 호출한다. opt-out 인자는 없다. code_mode_only 아래에서 codex-rs는 통합 exec(freeform)만 최상위로 선언하고, exec_command/shell_command는 tools.* 중첩 헬퍼로만 존재한다.
모델이 최상위 exec_command를 내면 src/server/responses-undeclared-tool-guard.ts와 src/bridge.ts가 맞다. 가드 문구가 리포터 로그와 같다. routed provider emitted undeclared client tool "exec_command"; only request-declared tools may be called. 가드가 틀린 게 아니라, 카탈로그가 선언한 표면과 모델이 부르는 이름이 어긋난 것이다.
src/adapters/tool-catalog-nudge.ts는 조언일 뿐이다. 주석대로 exec 옆에 납작한 exec_command가 있으면 브리지 모양이고, code mode면 await tools.exec_command(...)를 쓰라고 설명한다. 이 넛지는 openai-chat 경로에만 들어가고 openai-responses 패스스루에는 안 들어간다. 리포터도 두 경로 모두에서 모델이 무시했다고 한다. 넛지만으로는 이 버그를 못 닫는다.
스트림이 끊긴 뒤의 DeepSeek 400(reasoning_text in the thinking mode must be passed back)은 두 번째 사고다. 가드가 턴을 중단하면 재접속 재생에 thinking 조각이 빠진다. tool-mode만 맞추면 400이 사라진다는 로컬 검증이 이 체인을 지지한다. 가드를 느슨히 풀어 undeclared exec_command를 실행하게 하는 건 클라이언트 계약을 깨므로 답이 아니다.
해결방안
applyRoutedCodexToolMode()가 providers.<name>.codexToolMode를 읽게 한다. "shell"이면 tool_mode를 비워 두어 codex-rs가 shell_type: "shell_command"로 exec_command를 최상위 선언하게 한다. 없거나 "code_mode_only"면 지금과 같다. 기본값은 모든 라우티드 모델에 대해 현재 동작 유지. 스키마를 강제하지 않아도 config는 패스스루라는 리포터 검증이 있다. 가능하면 모델 단위 키를 두고, DeepSeek flash만 shell로 두고 Kimi 등은 code_mode_only를 유지한다. openai-responses 패스스루에 tool-catalog 넛지를 넣는 것은 방어일 뿐 본 패치가 아니다. 가드 문구와 responses-undeclared-tool-guard.ts의 fail-closed는 그대로 둔다.
Activity
coderabbitai commentedon Aug 19, 2026
🔗 Related PRs
#304 - fix(kiro): restore hardening and complete text turns [open]
#1361 - fix(responses): restore DeepSeek streaming and routed code-mode tools [merged]
#1788 - fix(responses): fail closed when a routed provider calls an undeclared tool [merged]
#1951 - fix(adapters): decide code mode from tool semantics, not from the name exec [merged]
#1953 - fix(adapters): restore the un-namespaced requirement on the shell-bridge check [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
fix(catalog): allow per-provider/model opt-out of code_mode_only tool…
lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 52 / 80
라우팅된 DeepSeek 모델이 Codex code-mode 계약을 무시하고 최상위
exec_command를 부른다. OpenCodex 가드가 스트림을 끊고, 앱이 재접속한 뒤 reasoning을 재전송하지 못해 DeepSeek가 400을 준다. 기본 native 경로가 아니라 라우티드 프로바이더 도구 호환 문제라 52다. 로컬에서codexToolMode=shell로 우회가 검증됐고, 다른 라우티드 모델의 기본값은 그대로 두면 된다.원인은
src/codex/catalog/parsing.ts의applyRoutedCodexToolMode()다.ROUTED_CODEX_TOOL_MODE = "code_mode_only"를 모든 라우티드 엔트리에 무조건 찍는다.src/codex/catalog/sync.ts가 카탈로그를 쓸 때 이 함수를 호출한다. opt-out 인자는 없다. code_mode_only 아래에서 codex-rs는 통합exec(freeform)만 최상위로 선언하고,exec_command/shell_command는tools.*중첩 헬퍼로만 존재한다.모델이 최상위
exec_command를 내면src/server/responses-undeclared-tool-guard.ts와src/bridge.ts가 맞다. 가드 문구가 리포터 로그와 같다.routed provider emitted undeclared client tool "exec_command"; only request-declared tools may be called. 가드가 틀린 게 아니라, 카탈로그가 선언한 표면과 모델이 부르는 이름이 어긋난 것이다.src/adapters/tool-catalog-nudge.ts는 조언일 뿐이다. 주석대로exec옆에 납작한exec_command가 있으면 브리지 모양이고, code mode면await tools.exec_command(...)를 쓰라고 설명한다. 이 넛지는 openai-chat 경로에만 들어가고 openai-responses 패스스루에는 안 들어간다. 리포터도 두 경로 모두에서 모델이 무시했다고 한다. 넛지만으로는 이 버그를 못 닫는다.스트림이 끊긴 뒤의 DeepSeek 400(
reasoning_text in the thinking mode must be passed back)은 두 번째 사고다. 가드가 턴을 중단하면 재접속 재생에 thinking 조각이 빠진다. tool-mode만 맞추면 400이 사라진다는 로컬 검증이 이 체인을 지지한다. 가드를 느슨히 풀어 undeclaredexec_command를 실행하게 하는 건 클라이언트 계약을 깨므로 답이 아니다.해결방안
applyRoutedCodexToolMode()가providers.<name>.codexToolMode를 읽게 한다."shell"이면tool_mode를 비워 두어 codex-rs가shell_type: "shell_command"로exec_command를 최상위 선언하게 한다. 없거나"code_mode_only"면 지금과 같다. 기본값은 모든 라우티드 모델에 대해 현재 동작 유지. 스키마를 강제하지 않아도 config는 패스스루라는 리포터 검증이 있다. 가능하면 모델 단위 키를 두고, DeepSeek flash만 shell로 두고 Kimi 등은 code_mode_only를 유지한다. openai-responses 패스스루에 tool-catalog 넛지를 넣는 것은 방어일 뿐 본 패치가 아니다. 가드 문구와responses-undeclared-tool-guard.ts의 fail-closed는 그대로 둔다.이 댓글은 grok-bot이 작성했습니다
Merge pull request #2112 from Yuxin-Qiao/fix/issue-2106-codex-tool-mode
lidge-jun commentedon Aug 19, 2026
리뷰 · 종결
dev에 이미 머지됨. #2112 (dbe260131) 가codexToolMode옵트아웃을 넣었음.origin/devHEAD에 있음. 다음 배포 예정.이 이슈는 닫음. 재현되면 버전 찍고 다시 열면 됨.
이 댓글은 grok-bot이 작성했습니다
feat(catalog): declare modelDisplayNames, and keep custom-model labels
feat(catalog): declare modelDisplayNames, and keep custom-model labels
feat(catalog): declare modelDisplayNames, and keep custom-model labels
feat(catalog): declare modelDisplayNames, and keep custom-model labels
feat(catalog): declare modelDisplayNames, and keep custom-model labels
fix(catalog): allow per-provider/model opt-out of code_mode_only tool…
Merge pull request lidge-jun#2112 from Yuxin-Qiao/fix/issue-2106-code…
fix(responses): let a provider declare the hosted tools it rejects (#…