Skip to content

[Provider compatibility] Bridged hosted web_search result is not replayed to the destination on the next turn #4587

Closed

Description

@lidge-jun

Client or integration

Codex App

Provider or upstream service

Any key-auth Responses passthrough provider running the opt-in providers.<name>.webSearchBridge. Originally observed on an internal OpenAI-shaped gateway serving Moonshot kimi-k3 (adapter: "openai-responses", authMode: "key").

OpenCodex version

dev, after the mixed-tool bridge change on branch codex/260914-l7-web-search-bridge.

Endpoint or capability

/v1/responses — hosted web_search bridged on a key-auth Responses passthrough route.

Current behaviour

The bridge intercepts the destination's web_search call, runs the search proxy-side, and shows the caller a hosted web_search_call cell. On a leg that also carries a client-executed tool call, the turn now ends on that leg so the caller can run its own tool — the turn no longer dies.

The destination never learns the result of that search. The caller replays the hosted web_search_call cell on the next turn, and that item carries the query and its sources but no result text. The function_call the destination actually emitted, and the matching function_call_output holding the search result, are not reconstructed in the outbound input. From the destination's point of view it asked to search, received nothing back, and sees an item type it never produced.

The practical effect is an extra round trip: the model usually searches again on the following turn, and that turn resolves normally when it does not also request a client tool.

Expected behaviour

When a bridged provider's next turn replays a hosted web_search_call cell this bridge synthesized, the outbound input should carry the destination's own function_call (same call_id, name web_search, original arguments) followed by a function_call_output holding the executed result, so the conversation the destination sees is the one it actually had.

Minimal redacted request or reproduction

{
  "providers": {
    "GATEWAY": {
      "adapter": "openai-responses",
      "baseUrl": "https://<internal-host>/v1",
      "authMode": "key",
      "apiKey": "<redacted>",
      "defaultModel": "kimi-k3",
      "webSearchBridge": { "enabled": true, "backend": "exa" }
    }
  }
}

Run a Codex App turn on that route where the model requests a web search and a shell command in the same assistant turn, then continue the conversation and observe the next outbound body.

Actual response or error

No error. The next outbound input contains the hosted web_search_call item and no function_call / function_call_output pair for the search the proxy executed.

Upstream documentation

https://developers.openai.com/api/docs/guides/tools-web-search — a hosted search is reported as a web_search_call output item; the result text is not part of the replayed item.

Suggested mapping or implementation notes

The rewrite has to happen before the first leg of the next turn is dispatched, so src/web-search/passthrough-bridge.ts cannot do it — by the time the bridge wraps a turn, that turn's first leg is already on the wire. The existing pre-dispatch rewrites of outbound input live in src/adapters/openai-responses.ts (backfillWebSearchQueries, repairOrphanedInputItems) and in src/server/responses/core.ts, which is where this belongs.

It needs a bounded, per-provider memo of what the bridge executed, keyed by the synthesized cell item id, holding the destination's call_id, the original arguments, and the result text. Bound it and expire it; a turn that cannot be resolved from the memo should leave the item alone rather than inventing a result.

Additional context and attachments

This is the remainder of the mixed-tool work. The client-facing half — the turn surviving, the hosted cell completing, and the client's own call being released with its call_id and ordering intact — landed separately. The bridge's fail-closed posture and the undeclared-tool guard are unchanged.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Activity

  1. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Sep 14, 2026
  2. lidge-jun commented on Sep 14, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 58 / 80

    지금 dev(HEAD 8c7f01451)의 웹서치 패스스루 브릿지(src/web-search/passthrough-bridge.ts)는 가로챈 hosted web_search를 프록시에서 돌리고, 호출자 쪽에 hosted web_search_call 셀을 보여 줍니다. #4586(L7 mixed-tool bridge)이 랜딩되면, 같은 레그에 클라이언트 tool call이 같이 와도 턴이 죽지 않고 셀이 query·sources로 끝나며 클라이언트 콜이 풀립니다. 그런데 다음 턴에 destination(업스트림 모델)은 그 검색 결과 텍스트를 받지 못합니다. 호출자가 다시 보내는 hosted web_search_call 아이템에는 query와 sources만 있고 result text가 없고, destination이 냈던 function_call / function_call_output 쌍도 outbound input에 다시 안 실립니다. destination 입장에서는 「검색 요청 → 아무 응답 없음 → 자기가 만든 적 없는 아이템 타입」이 되어, 보통 다음 턴에 검색을 한 번 더 합니다. 그게 이 이슈입니다.

    이 이슈가 가리키는 고침 위치는 브릿지가 아닙니다. 브릿지가 턴을 감쌀 때면 그 턴의 첫 레그는 이미 와이어에 나갑니다. 제안대로 src/adapters/openai-responses.ts의 backfillWebSearchQueries / repairOrphanedInputItems 축과 src/server/responses/core.ts의 pre-dispatch input 재작성이 맞습니다. 브릿지가 실행한 결과를 cell item id로 키 잡은 제공자별·유한 만료 메모가 필요하고, 메모에 없으면 아이템을 건드리지 말고 결과를 지어내지 말라는 가드도 맞습니다. #4429의 「결과 재구성」 나머지이고, #4586 PR 본문이 이 이슈(#4587)로 분리한다고 적어 둔 그대로입니다. types/config 스플릿에 통째로 무효화될 종류의 요청이 아닙니다.

    경로/심볼 - #4586이 아직 open이면 이 이슈의 「Current behaviour」앞단(턴이 안 죽음)은 그 PR 머지 전제입니다. #4586 머지 전에는 재현이 mixed fail-closed에 가려질 수 있으니, 구현 PR은 #4586이 dev에 들어간 뒤에 잡는 편이 안전합니다.

    경로/심볼 - 메모 키·만료·제공자 범위가 설계 핵심입니다. cell item id만으로 부족하면 destination call_id + 원본 arguments + result text를 같이 묶어야 하고, 만료·상한 없이 쌓이면 다른 세션/턴에 결과가 새거나 메모리가 셉니다. 「메모 미스면 no-op」가 안전 기본값입니다.

    경로/심볼 - 재구성이 Responses core / openai-responses에 들어가면, Codex App뿐 아니라 같은 webSearchBridge를 켠 다른 클라이언트에도 영향이 갑니다. 회귀 핀은 「mixed leg 다음 턴 outbound input에 function_call+function_call_output」과 「메모 없을 때 hosted cell 그대로」 두 축이 최소입니다.

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

    너의 추천
    #4586이 dev에 들어간 뒤 구현 PR을 열고, 범위는 openai-responses + responses/core의 pre-dispatch 재작성 + 유한 메모로 한정하세요. 브릿지 fail-closed/undeclared-tool 가드는 건드리지 말 것. 우선순위 중: 사용자 체감은 「검색 한 번 더」 수준이라 #4586보다 낮고, cost-guard(#4580/#4588)와도 축이 다릅니다. 라벨(provider-compatibility / tools)은 맞습니다.

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

  3. added a commit that references this issue on Sep 17, 2026
    4f8656c
  4. lidge-jun commented on Sep 17, 2026

    @lidge-jun
    OwnerAuthor

    Fixed on dev by #4919.

    Bridge-executed searches are now memoed per destination scope and hosted cell id, and before the first leg of the next turn the replayed web_search_call cell is restored in place as the destination's own function_call and function_call_output. The recorded text matches what the continuation leg would have carried, so a replayed turn and a continuation turn look like the same conversation to the destination.

    Every memo miss is a no-op rather than a guess: an unknown id, an expired entry, a different destination, or a call_id already in the body all leave the item untouched. The search is not re-run and no result text is synthesized — either would answer with something other than what the history claims happened.

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

    providerProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reportstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions