openai / openai/codex

Custom Responses provider: Codex can terminate after progress message before required tool call; `previous_response_id` + `tool_choice=required` recovers the turn

Open
#45,096 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug CLI custom-model model-behavior
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

Summary

I am reporting a reproducible premature-turn-completion failure when Codex CLI is used with a custom OpenAI-compatible Responses provider.

After one or more real tool calls, the model can emit a progress/future-intent message such as:

“Şimdi mevcut klasör yapısını görüntüleyip durumu analiz edeceğim...”

or:

“Önce ... inceleyeceğim, ardından ... gerçekleştireceğim.”

The response is nevertheless marked completed, with no function/tool call, even though the active task still requires another tool action.

In my case this caused Codex to return control to the user while the requested multi-step task was incomplete. A manual continue/follow-up was normally required.

I built a small local Responses proxy as a diagnostic/workaround. When it detects this state, it continues the same Responses conversation using previous_response_id and forces tool_choice: "required". This successfully caused the model to emit the missing real tool call and allowed the original task to continue to completion.

This appears closely related to existing reports such as #27352, #36679, #36596 and #37278, but the distinguishing point here is the custom Responses-provider path plus a working wire-level recovery using previous_response_id and tool_choice=required.

Environment

  • Platform: macOS, Apple Silicon (MacBook Air M4, 16 GB RAM)
  • Codex CLI: 0.154.0-alpha.6.2 (bundled with ChatGPT app)
  • Provider: custom OpenAI-compatible Responses API
  • Model: foresee-qwen3-8-9b-heretic-uncensored-4bit-mtplx
  • Model server: local MTPLX
  • Direct upstream model endpoint: http://127.0.0.1:8000/v1
  • Custom Responses relay: 127.0.0.1:8002
  • Recovery proxy: 127.0.0.1:8003
  • wire_api = "responses"
  • model_reasoning_effort = "none"
  • Codex approval policy: never
  • Sandbox: danger-full-access

The local model is not OpenAI-hosted. The issue is being reported because Codex's Responses-provider/agent-loop behavior appears to accept a non-terminal-looking progress response as a terminal successful turn.

Original task

The task given to Codex was essentially:

Masaüstünde gkh klasörünü incele. İçeriğine bak ve gkh altında recovery-test adında yeni bir klasör oluştur. İçine index.php adında basit bir PHP başlangıç dosyası koy. PHP dosyasının gerçekten çalıştığını kontrol et. Bir hata çıkarsa kendin düzelt. Ara onay isteme; işi tamamen bitir ve en sonunda sonucu bildir.

The required work clearly involved multiple tool calls and verification.

Observed premature completion

In an earlier run, Codex successfully performed initial filesystem operations and then produced a progress response instead of making the next tool call.

The recovery proxy logged:

SSE request tool_history=True
SSE response=resp_0b257e323fd44156adfde58afc8cb6db tool_call=False text='Anladım. Önce mevcut klasör yapısını ve içeriğini detaylıca inceledikten sonra, yeni bir PHP projesini tamamen kuracağım...'

RECOVERY 1/2 previous_response_id=resp_0b257e323fd44156adfde58afc8cb6db

The next response again contained future-intent/progress text but no real tool call:

SSE response=resp_fa4a10c648004cb697fe75cb9b04085c tool_call=False text='Tamam, kaldığım yerden devam ediyorum. Önce klasör yapısını görüntüleyip durumu analiz edeceğim...'

A second recovery attempt also produced progress/Python code rather than a tool call:

SSE response=resp_d243eef2eefc42fc93dafce7af57b895 tool_call=False text='Tamam, şimdi mevcut klasör yapısını görüntüleyelim: ... Python ...'
MAX_RECOVERY reached; returning original response

This was not a context exhaustion event. The model knew another action was required but the Responses turn was accepted as complete without a function call.

Controlled reproduction

I then constructed a deliberately controlled request with:

  • existing function_call_output history, so the request represented an ongoing tool-driven turn;
  • one available list_files function;
  • initial tool_choice: "none", deliberately preventing a tool call;
  • a prompt explicitly asking the model to describe the next step without calling a tool.

The recovery proxy observed the expected premature response:

SSE request tool_history=True
SSE response=resp_36369b1f35b3419daf54925e9e4afae5 tool_call=False text='Şu ana kadar mevcut yapının bir kısmını inceledik ... Sonraki Adım: Önce list_files aracı...'

It then performed recovery with:

{
  "previous_response_id": "resp_36369b1f35b3419daf54925e9e4afae5",
  "tool_choice": "required"
}

The following response emitted a genuine Responses function call:

SSE response=resp_b3da398369cc4c329fe237e473cb6be1 tool_call=True text='{"path":"~/Desktop/gkh"}'

Wire-level events included:

event: response.output_item.added
data: {"item":{"name":"list_files","status":"in_progress","type":"function_call",...}}

event: response.function_call_arguments.delta
data: {"delta":"{\"path\":\"~/Desktop/gkh\"}"}

event: response.output_item.done

event: response.completed

So the model/tool-call capability itself was working. The failure was the first turn being accepted as complete without the required function call.

Real-task validation of the workaround

I then routed the actual Codex session through the recovery proxy.

The resulting recovery log showed normal tool calls and continued execution:

SSE response=resp_3ff22b7424814f0c827aca8c51d83a58 tool_call=True text='{"cmd":"mkdir ~/Desktop/gkh/recovery-test"}'

SSE response=resp_6b21895495ce4c3bb5fa66a770afe50d tool_call=True text='{"cmd":"cat > ~/Desktop/gkh/recovery-test/index.php ..."}'

SSE response=resp_0e78ca02d7674028b4b61a48ef116720 tool_call=True text='{"cmd":"php ~/Desktop/gkh/recovery-test/index.php"}'

SSE response=resp_14a051d69fac4a9a80da2b666b1f1810 tool_call=True text='{"cmd":"php -l ~/Desktop/gkh/recovery-test/index.php"}'

SSE response=resp_5c1b89528025490e8dc31df23c01a6d0 tool_call=False text='## ✅ Tamamlandı ...'

The resulting PHP file existed and was independently executed afterward:

Hello, recovery-test!
PHP çalışıyor!
Tarih: 2026-09-12 18:46:55
Hedef klasör: /Users/gokhanakkiz/Desktop/gkh/recovery-test

And php -l reported:

No syntax errors detected

This is important because the workaround was not only successful in a synthetic test. It recovered a real multi-step Codex task and allowed it to reach its final verification.

Workaround implementation

The recovery proxy watches streamed Responses events.

When all of these are true:

  1. the request contains prior tool history;
  2. the Responses stream reaches response.completed;
  3. no function_call/custom_tool_call was emitted;
  4. the response text matches a future-intent/progress pattern such as “şimdi”, “sonra”, “devam edeceğim”, “oluşturalım”, etc.;
  5. the task therefore appears to have ended while an executable action is still being promised;

it sends a follow-up using the completed response ID:

def make_followup_payload(original, response_id):
    followup = dict(original)
    followup["previous_response_id"] = response_id
    followup["stream"] = True

    followup["tool_choice"] = "required"

    followup["input"] = [
        {
            "role": "user",
            "content": [
                {
                    "type": "input_text",
                    "text": (
                        "Önceki yanıt görevi tamamlamadı. "
                        "Bu turda açıklama veya Python kodu yazma. "
                        "Sağlanan gerçek araçlardan uygun olanını "
                        "MUTLAKA çağır ve işlemi gerçekten gerçekleştir. "
                        "Araç çağrısı yapmadan tamamlandı deme. "
                        "Gerekli araçları kullanarak görevi bitir. "
                        "Ara onay isteme."
                    ),
                }
            ],
        }
    ]
    return followup

The critical part is not the Turkish wording. The important mechanism is:

"previous_response_id": "<completed-but-incomplete-response>",
"tool_choice": "required"

The proxy keeps the original tool definitions and continues the same Responses chain rather than starting an unrelated new conversation.

Why I think this deserves investigation

The existing Codex issues around premature completion describe similar symptoms, but this case gives a useful additional observation:

The exact same model can immediately produce the missing function call when the next Responses request is continued with previous_response_id and tool_choice=required.

That suggests there may be a gap between:

  • model output classification,
  • Responses turn termination,
  • Codex's decision that a response is terminal,
  • and the existence of a pending executable action/tool call.

A progress message containing future intent should not necessarily be treated as a terminal successful turn when the current tool-driven task remains incomplete.

Expected behavior

For a tool-driven task, if the assistant emits a message that clearly announces an immediately pending executable action, Codex should either:

  1. continue the agent loop and request the appropriate tool call;
  2. emit a genuine blocker requiring user input; or
  3. explicitly mark the task incomplete rather than silently treating the progress message as the successful terminal result.

A message equivalent to:

“Next I will call list_files...”

should not end the active task when the tool is available and no user input is required.

Suggested implementation / regression test

A regression test could simulate:

  1. tool-driven task with existing tool history;
  2. Responses provider returns a successful response.completed containing only a progress/future-intent assistant message and no tool call;
  3. Codex should not treat this as a valid final task completion when the active task still has executable work;
  4. verify that the agent either requests another model turn or otherwise continues;
  5. separately test that a genuine final answer containing no pending intent still completes normally.

It would also be useful to test the custom OpenAI-compatible Responses provider path specifically, including previous_response_id continuation.

Important distinction from a generic “continue” request

This is not a request for Codex to continue running after a legitimate final answer.

The problem occurs while the agent has an active multi-step task and the model itself has just emitted a progress message describing work that still needs to happen.

The desired behavior is to keep the current turn/agent loop alive until the pending work is executed or a real blocker exists.

Related issues

This appears related to, but is not identical to:

  • #27352 — Codex marks a turn complete while follow-up is still needed after a progress message.
  • #36679 — app-server can finalize an identical progress message before promised tool work starts.
  • #36596 — Codex repeatedly terminates active autonomous work despite explicit instructions to continue.
  • #37278 — GPT-5.6 Codex can replace the requested deliverable with meta-work and terminate before explicit acceptance gates.
  • #32389 — empty successful final response after tool use can prematurely end agent loops.

The distinguishing evidence here is the custom Responses provider reproduction and the successful recovery using previous_response_id plus tool_choice=required.

Privacy / scope

No OpenAI credentials, private repository contents, account identifiers, or proprietary project files are included here. The real-task paths above are local test paths on my own machine. The model server is local and the reproduction does not depend on OpenAI-hosted inference.

I can provide the minimal recovery proxy or additional sanitized Responses event traces if maintainers want a more focused reproduction.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No repository files or regression-test paths are named. Start at the custom Responses-provider path and agent-loop handling of response.completed, using the controlled sequence with prior tool history and no function call. Done means pending progress messages no longer count as successful completion, while genuine final answers still do.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.