Skip to content

[Bug]: Hosted web-search forced-answer pass accepts a malformed tool call and completes without an assistant message #1001

Description

@nbsp1221

Client or integration

Codex App

Area

Tools / MCP / web search

Summary

The hosted Web Search loop can emit a successful response.completed with no assistant message after the search budget is exhausted and the routed model's forced-answer pass returns a malformed tool call.

In the observed DeepSeek V4 Flash turn, a batched web_search consumed the three-search budget. OpenCodex then made the forced-answer request without the synthetic search tool. The upstream response was HTTP 200 with stop_reason=tool_calls, content=null, an empty function name, an empty call ID, and arguments shaped like another batched search. OpenCodex treated that response as terminal success. Codex App showed the completed search cell but no final answer.

This is distinct from #875/#946: the client turn does not remain pending and a continuation request is not missing. The internal hosted-search iterations and forced-answer request all occur, after which OpenCodex sends task_complete / response.completed with no visible assistant message.

Expected: a malformed forced-answer tool call must not become a successful empty turn. OpenCodex should either recover with a bounded final-answer retry or surface an explicit in-stream error.

Reproduction

Provider-level reproduction (the model's malformed output is not guaranteed on every prompt):

  1. Run OpenCodex 2.10.0 with a routed openai-chat provider backed by an OpenAI-compatible Bifrost endpoint and select deepseek-v4-flash.
  2. Use Codex App and request a broad research task that causes the synthetic hosted web_search function to batch at least three queries. For example: ask for a current comparison of OpenClaw and Hermes Agent based on community reports.
  3. Let the batch consume the configured/default maxSearches=3 budget.
  4. Observe the forced-answer request. In the failing case, the upstream response is HTTP 200 and ends with stop_reason=tool_calls, but contains no text and has an empty tool-call name/ID.
  5. Codex App displays the completed search cell and then stops with no assistant answer. The rollout records task_complete, not a pending/stalled or failed turn.

Deterministic local regression probe on current dev (e44d234f08e03dd4dbf0c4aa13af43046d86b0a6):

  1. Use the existing runWithWebSearch test harness and maxSearches: 2.
  2. Make adapter pass 1 emit:
tool_call_start id="call_search" name="web_search"
tool_call_delta {"queries":["one","two"]}
tool_call_end
done
  1. Make the forced-answer pass emit:
tool_call_start id="" name=""
tool_call_delta {"queries":["three"]}
tool_call_end
done
  1. Drain the response SSE. Current dev emits response.completed with status="completed" and no output item of type message. The focused probe passes when asserting this current, incorrect behaviour:
[web-search-loop] done — 2 searches, 2 iterations
[web-search-loop] cancelled — 2 real searches, 0 placeholders
response.completed status=completed
assistant message present=false

Relevant code path on current dev:

  • src/web-search/loop.ts:72-75: every non-web_search name, including an empty name, sets hasRealToolCall=true.
  • src/web-search/loop.ts:681-705: forceAnswer=true makes shouldLoop=false; split.passthrough is replayed and the loop returns without checking for visible text or a valid tool call.
  • The forced-answer tests use helpers whose later pass always returns text ("final answer"), so the malformed/empty terminal case is not covered.

Version

@bitkyc08/opencodex 2.10.0 (f9b9440c551e3d7f3e2041098caa2ee4de57698e). The deterministic regression probe also reproduces on current dev at e44d234f08e03dd4dbf0c4aa13af43046d86b0a6 (2026-08-04).

Operating system

OpenCodex host: Ubuntu 24.04 x86_64. Client: Codex App on Windows.

Provider and model

OpenAI-compatible Bifrost 1.6.5 route to Alibaba Token Plan International / deepseek-v4-flash; OpenCodex adapter: openai-chat.

Logs or error output

# Failing live turn, redacted and condensed
initial routed request: tools=17
model output: stop_reason=tool_calls, batched web_search queries
hosted searches executed: 3

forced-answer routed request: tools=16 (synthetic web_search removed)
upstream HTTP status: 200
stop_reason: tool_calls
content: null
tool call id: ""
function name: ""
arguments: {"queries":["..."]}

[web-search-loop] done — 3 searches, 2 iterations
[web-search-loop] cancelled — 3 real searches, 0 placeholders

Codex lifecycle:
web_search_end
task_complete
last_agent_message=null

The Bifrost request log shows that both failing forced-answer requests returned HTTP 200 within seconds. One earlier attempt encountered a transient 429, but the final request succeeded with HTTP 200, so the 429 is not the terminal cause.

Screenshots and supporting files

A deterministic focused test was run against current dev: 1 test passed, confirming that the loop currently returns a completed response without a message for the event sequence above. A minimal regression patch can be provided if useful.

Related but distinct reports:

Redacted configuration

{
  "providers": {
    "redacted-provider": {
      "adapter": "openai-chat",
      "baseUrl": "https://redacted.example/v1",
      "authMode": "key",
      "defaultModel": "deepseek-v4-flash",
      "models": ["deepseek-v4-flash"],
      "preserveReasoningContentModels": ["deepseek-v4-flash"]
    }
  }
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, personal data, local paths, session IDs, and request IDs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproviderProvider adapters, OpenAI-compat presets, upstream API quirksstreamingSSE, WebSocket, terminal stream framestoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions