Repository navigation
[Provider compatibility] Bridged hosted web_search result is not replayed to the destination on the next turn #4587
Description
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
lidge-jun commentedon Sep 14, 2026
리뷰 · 우선순위 58 / 80
지금
dev(HEAD8c7f01451)의 웹서치 패스스루 브릿지(src/web-search/passthrough-bridge.ts)는 가로챈 hostedweb_search를 프록시에서 돌리고, 호출자 쪽에 hostedweb_search_call셀을 보여 줍니다. #4586(L7 mixed-tool bridge)이 랜딩되면, 같은 레그에 클라이언트 tool call이 같이 와도 턴이 죽지 않고 셀이 query·sources로 끝나며 클라이언트 콜이 풀립니다. 그런데 다음 턴에 destination(업스트림 모델)은 그 검색 결과 텍스트를 받지 못합니다. 호출자가 다시 보내는 hostedweb_search_call아이템에는 query와 sources만 있고 result text가 없고, destination이 냈던function_call/function_call_output쌍도 outboundinput에 다시 안 실립니다. destination 입장에서는 「검색 요청 → 아무 응답 없음 → 자기가 만든 적 없는 아이템 타입」이 되어, 보통 다음 턴에 검색을 한 번 더 합니다. 그게 이 이슈입니다.이 이슈가 가리키는 고침 위치는 브릿지가 아닙니다. 브릿지가 턴을 감쌀 때면 그 턴의 첫 레그는 이미 와이어에 나갑니다. 제안대로
src/adapters/openai-responses.ts의backfillWebSearchQueries/repairOrphanedInputItems축과src/server/responses/core.ts의 pre-dispatchinput재작성이 맞습니다. 브릿지가 실행한 결과를 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 다음 턴 outboundinput에function_call+function_call_output」과 「메모 없을 때 hosted cell 그대로」 두 축이 최소입니다.메인테이너의 판단이 필요한 지점
responses/core공용으로 둘지너의 추천
#4586이
dev에 들어간 뒤 구현 PR을 열고, 범위는 openai-responses + responses/core의 pre-dispatch 재작성 + 유한 메모로 한정하세요. 브릿지 fail-closed/undeclared-tool 가드는 건드리지 말 것. 우선순위 중: 사용자 체감은 「검색 한 번 더」 수준이라 #4586보다 낮고, cost-guard(#4580/#4588)와도 축이 다릅니다. 라벨(provider-compatibility / tools)은 맞습니다.이 댓글은 grok-bot이 작성했습니다
fix(web-search): replay executed bridge searches to the destination (#…
lidge-jun commentedon Sep 17, 2026
Fixed on
devby #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_callcell is restored in place as the destination's ownfunction_callandfunction_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_idalready 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.