Repository navigation
[Provider compatibility] Key-auth Responses gateway (Kimi K3) echoes hosted web_search as client function_call; webSearchBridge executor is Ollama-only #4429
Description
Client or integration
Codex App
Provider or upstream service
AI2API - an internal corporate gateway speaking the OpenAI Responses API (opencodex provider config: adapter: "openai-responses", authMode: "key"), serving Moonshot kimi-k3. The endpoint is internal-only; there is no public hostname or public specification for the gateway itself.
OpenCodex version
2.51.0 (npm @bitkyc08/opencodex). Also source-checked against dev: webSearchBridge (#4142) is present, but src/web-search/passthrough-bridge.ts still states that only the ollama backend has a shipped executor today.
Endpoint or capability
/v1/responses - hosted web_search tool on a key-auth Responses passthrough route
Current behaviour
Codex App declares the hosted {type: "web_search"} tool on the request. On this route the model answers with a function_call item named web_search. A hosted declaration never authorizes a client function of that name, so the undeclared-tool guard fails the stream, Codex App retries up to 5/5, and the turn dies.
This is the same failure class as #3761 (Ollama Cloud). #4142 shipped providers.<name>.webSearchBridge for exactly this shape, but its only shipped executor posts to Ollama's /api/web_search. PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS admits openai/anthropic/xai/gemini/exa as explicit-but-inert, so a non-Ollama key-auth gateway cannot arm the bridge at all.
Expected behaviour
A key-auth Responses passthrough provider that is not Ollama Cloud can opt into the #4142 bridge with a working search executor - for example by reusing the existing sidecar executors (openai/anthropic/xai/gemini/exa) in the bridge path - so the intercepted web_search call is executed proxy-side, the call and its result continue the upstream conversation, and Codex receives the hosted web_search_call cell, exactly as the Ollama backend already does.
Minimal redacted request or reproduction
// ~/.opencodex/config.json (redacted)
{
"providers": {
"AI2API": {
"adapter": "openai-responses",
"baseUrl": "https://<internal-host>/v1",
"authMode": "key",
"apiKey": "<redacted>",
"defaultModel": "kimi-k3"
}
}
}Then run any Codex App turn on that route with web search enabled where the model decides to search (for example, asking for the latest status of an external project). Non-search turns on the same route complete normally.
Actual response or error
stream disconnected before completion: routed provider emitted undeclared client tool "web_search"; only request-declared tools may be called
Codex App shows "Reconnecting 1/5 ... 5/5" and the turn fails. Screenshot available on request.
Upstream documentation
https://developers.openai.com/api/docs/guides/tools-web-search - hosted web_search returns web_search_call output items; a provider-side search is not a client function_call. The gateway itself is internal and publishes no specification.
Suggested mapping or implementation notes
- Keep the undeclared-tool guard closed (per the [Provider compatibility] Ollama Cloud Responses passthrough skips web-search sidecar; hosted web_search is echoed as a client function_call #3761 discussion); the fix is the bridge, not guard relaxation.
- Concrete gap: ship a non-Ollama executor for
webSearchBridge. The sidecar executors already implement the search itself; wiring them into the bridge path would cover arbitrary key-auth Responses gateways without new search-provider integrations. The credential/endpoint scoping policy called out in [Provider compatibility] Ollama Cloud Responses passthrough skips web-search sidecar; hosted web_search is echoed as a client function_call #3761 still applies. - Gateway-specific unknown: Kimi's builtin
$web_searchmay auto-execute server-side before the relay emits the call. Whether the relay expects a tool result back (the bridge continuation leg fits) or would have continued within the same response needs a live probe against the gateway; happy to run one and report back.
Additional context and attachments
- [Provider compatibility] Ollama Cloud Responses passthrough skips web-search sidecar; hosted web_search is echoed as a client function_call #3761 (closed by fix(web-search): opt-in hosted web-search bridge for the key-auth Responses passthrough #4142) was the Ollama-Cloud-specific slice of this failure class; this report is the generic-gateway remainder.
- Observed on a turn requiring current external information, which makes the model reliably attempt a search.
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 12, 2026
리뷰 · 우선순위 70 / 80
이 이슈는 Codex App이 호스티드
web_search도구를 켠 채로, key-authopenai-responses패스스루(여기서는 사내 AI2API → Moonshot kimi-k3)로 보낼 때 생기는 실패입니다. 게이트웨이가 호스티드 검색을 대신 실행하지 않고function_call이름web_search를 그대로 돌려주면, 호스티드 선언은 클라이언트 함수 이름을 허가하지 않으므로 undeclared-tool 가드가 스트림을 끊고 Codex App이 5/5 재연결 후 턴이 죽습니다. 에러 문구는 이슈에 적힌routed provider emitted undeclared client tool "web_search"그대로입니다.지금
devHEAD는01e7d746d(#4422 Muse 64자 tool-name alias 직후)입니다. 같은 실패 유형은 이미 #3761(Ollama Cloud)로 보고됐고, #4142가providers.<name>.webSearchBridge옵트인을 넣었습니다. 다만 그 첫 슬라이스는 Ollama 전용입니다.src/types/provider.ts의PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS는ollama|openai|anthropic|xai|gemini|exa를 받지만, 주석과planPassthroughWebSearchBridge가 명시한 대로 실제 executor가 있는 건ollama뿐이고 나머지는 명시해도 브리지를 무장하지 않습니다(유료 Luna/Exa로 조용히 넘어가지 않으려는 의도).코드 경로를 짧게 따라가면:
src/server/responses/core.ts패스스루 분기에서planPassthroughWebSearchBridge를 부르고, 계획이 있을 때만createPassthroughWebSearchBridgeStream+createOllamaBridgeExecutor로 감쌉니다. planner는authMode === "key",webSearchBridge.enabled === true,backend === "ollama", 그리고 Ollama Cloud origin(또는 명시endpoint)이 있을 때만 계획을 돌려줍니다. AI2API처럼 baseUrl이https://ollama.com이 아닌 사내 게이트웨이에서는, 설정을 켜도 지금 HEAD에서는 브리지가 절대 안 켜집니다. 그래서 이 이슈는 #3761의 “나머지 일반 게이트웨이” 조각이 맞고, 가드를 풀자는 제안이 아니라 브리지 executor를 넓히자는 제안입니다.이미 사이드카 쪽에는
src/web-search/executor.ts(openai),anthropic-executor.ts,xai-executor.ts,gemini-executor.ts,exa-executor.ts가 있습니다. 제보자가 말한 대로 그 실행기를 브리지 경로에 연결하면 “새 검색 벤더 통합” 없이 key-auth Responses 게이트웨이 일반 케이스를 커버할 수 있습니다. 다만 사이드카는 보통 별도 OAuth/키(ChatGPT forward, Anthropic stored OAuth 등)를 쓰고, 브리지의 ollama 경로는 “이 provider 자신의 apiKey를 검색 endpoint로 보낸다”는 계약이라 자격증명·과금 경계가 다릅니다. #3761에서 말한 credential/endpoint 스코핑을 그대로 가져가야 합니다.Kimi 쪽 추가 불확실성도 이슈에 잘 적혀 있습니다. 게이트웨이가 서버에서
$web_search를 이미 실행한 뒤 call만 에코하는지, 아니면 진짜로 tool result continuation을 기다리는지 live probe가 필요합니다. 전자면 브리지 continuation이 중복 검색/턴 꼬임을 만들 수 있고, 후자면 #4142와 같은 “가로채기 → 실행 → 같은 upstream에 결과 붙여 재POST → Codex엔web_search_call셀” 패턴이 맞습니다. 제보자가 probe 가능하다고 했으니 그걸 먼저 받는 게 설계 리스크를 줄입니다.types.ts/config.ts 분할 캠페인과 직접 충돌하는 PR은 아니고, 중복 close 대상도 아닙니다. #3761은 #4142로 이미 닫혔고 이 이슈는 그 위의 일반화 요청입니다. 지금
dev최적화 축(Muse device OAuth, Muse tool alias, Devin reasoning replay, quota-avoid)과도 직교합니다. 우선순위 70은 “실사용 Codex App 턴이 죽고, 수정 경로가 이미 #4142로 검증된 패턴”이기 때문이고, 100에 가깝지 않은 이유는 Muse/릴리즈 열차에 끼어들 필수는 아니고 설계(백엔드 선택·자격증명·Kimi 자동실행) 판단이 한 번 더 필요하기 때문입니다.src/types/provider.tsPROVIDER_WEB_SEARCH_BRIDGE_BACKENDS / 주석 - openai|anthropic|xai|gemini|exa 는 설정만 받고 executor는 없음. 사내 key-auth 게이트웨이는 켜도 무장 불가.src/web-search/passthrough-bridge.tsplanPassthroughWebSearchBridge (~141행) -bridge.backend !== "ollama"이면 즉시 undefined. 이슈 재현 설정으로는 절대 plan이 안 나옴.src/web-search/passthrough-bridge.tsresolveOllamaWebSearchEndpoint - endpoint 없을 때 origin이 ollama.com 일 때만 자동 유도. AI2API baseUrl은 여기서 탈락.src/server/responses/core.ts(~6081행) - 브리지 execute가createOllamaBridgeExecutor하드코딩. sidecar executor 재사용 경로가 아직 없음.undeclared-tool 가드 - 이 이슈에서 가드를 느슨히 하면 “실행할 사람 없는 web_search”를 클라이언트로 통과시켜 더 나빠짐. 가드는 유지가 맞음(제보자·#3761과 동일).
Kimi/AI2API 동작 - 서버측 자동 검색 후 call 에코인지 continuation 대기인지 미확인. 잘못된 가정으로 브리지를 붙이면 중복 검색·턴 손상 가능.
메인테이너의 판단이 필요한 지점
너의 추천
이슈는 열어 두고
needs-design/help wanted방향이 맞습니다. 가드는 풀지 마세요. 다음 단계는 (1) 제보자에게 AI2API가 tool result continuation을 기대하는지 live probe 부탁, (2) probe 결과에 따라 #4142 브리지에 non-ollama executor 배선 PR을 별도 유닛으로 잡기. types/config 분할에 무효화될 성격이 아니고, #3761 중복 close 대상도 아닙니다. PR이 오면 ollama 전용 계약을 깨지 않는지·credential 스코핑·혼합 tool fail-closed를 중점 리뷰하면 됩니다.이 댓글은 grok-bot이 작성했습니다
mdwsk88 commentedon Sep 12, 2026
Live probe results against the gateway (2026-09-13)
I probed the AI2API
/v1/responsesendpoint directly (bypassing opencodex) with the route's own credential. Two controlled requests, streamed,store: false:Probe A - only hosted web_search declared (tools: [{type:"web_search"}], plus the same search-requiring prompt):
Probe B - Codex-like mixed catalog (tools: [{type:"web_search"}, function exec, function update_plan], plus tool_choice: auto, parallel_tool_calls: true, reasoning {effort, summary}):
Conclusions:
Probe scripts available on request (plain python3 + urllib, no dependencies).
jaychou0642-create commentedon Sep 12, 2026
Additional live reproduction: official DeepSeek
deepseek-flashon OpenCodex 2.52.0I reproduced the same missing non-Ollama bridge path with the built-in DeepSeek provider, so this is not limited to Kimi or a private gateway.
Environment
/v1/responsesrequest2.52.0deepseek / deepseek-flashopenai-responsesfrom the 2.52.0 registry default{ "backend": "gemini" }Reproduction
deepseek/deepseek-flash.webSearchSidecar.backend = "gemini".tools: [{ "type": "web_search" }]and force/select that tool for a current-pricing query.sensenova/deepseek-v4-flash(chat adapter) with the same request as a positive control.Actual result
The DeepSeek Responses route returned HTTP 200 and a normal terminal event sequence, but it performed no search:
The positive-control chat route completed two real searches and returned a sourced answer. The service log recorded:
Source-path confirmation
planWebSearch(...)returnsundefinedwhenisPassthroughis true.deepseek-flashtoopenai-responses, whose adapter is passthrough.planPassthroughWebSearchBridge(...)returnsundefinedfor every backend exceptollama.createOllamaBridgeExecutor(...).tools: [{ google_search: {} }]plusgroundingMetadata; it is simply never planned for this passthrough route.Expected result
A key-auth Responses passthrough route whose upstream does not execute hosted
web_searchshould be able to opt into one of the existing non-Ollama search executors, including Gemini, without switching the main model back to the chat adapter.All credentials, account details, request headers, and local paths have been removed.
Ingwannu commentedon Sep 13, 2026
The new probes narrow the problem, but they show two different contracts rather than only one missing executor.
For the AI2API mixed-tool result, adding a non-Ollama executor alone would still not make the current bridge accept it. On dev 4a49d7f, src/web-search/passthrough-bridge.ts deliberately returns web_search_bridge_mixed_tools when intercepted search calls coexist with another client-executed call. Your pending exec + web_search result hits that boundary. A follow-up needs a mixed-tool continuation design that preserves the client's exec call/call_id and ordering without executing it proxy-side or losing already completed hosted-search items.
The DeepSeek example is different: it reports zero function calls and only XML-like assistant text. The bridge intercepts structured function_call/custom_tool_call items, not arbitrary text. A Gemini executor cannot execute a call that was never emitted. Please keep that fixture separate; do not turn matching assistant text into executable search instructions.
The hosted-only positive control remains useful. For the implementation proposal, keep three synthetic cases: hosted-only execution unchanged, mixed structured exec/search continuation, and plain-text pseudo-search with no dispatch. Backend credential ownership and explicit billing opt-in remain required. No live gateway requests were made in this review, and the undeclared-tool/mixed-tool guards should remain closed.
Merge pull request #4515 from lidge-jun/codex/260913-4429-passthrough…
lidge-jun commentedon Sep 13, 2026
Partly addressed on dev, and left open on purpose for the part that matters most in your report.
#4515 landed (merge commit
cb2e15ba6f8ac17af0620d6ff04fcfa7d88e3dcd). It arms the existing sidecar executors —openai,anthropic,xai,gemini,exa— behind an explicitwebSearchBridge.backend, where before onlyollamaarmed the bridge even though the type already accepted all six. Credentials stay on the named backend and a missing credential leaves the bridge disarmed rather than falling through to a paid search.That does not fix your probe B, and saying otherwise would be wrong. Your live probe ends with two pending client calls,
execandweb_search, and the bridge still refuses that withweb_search_bridge_mixed_tools. @Ingwannu's point stands: adding an executor was never going to be enough, because the boundary is the mixed-tool leg rather than the missing backend. A continuation there has to preserve yourexeccall and itscall_idand their ordering, must not execute it proxy-side, and must not drop hosted search items the relay already completed. That is a design, not a patch, and it is why this issue stays open.The DeepSeek XML case in this thread also stays separate, deliberately. It emits no function calls at all, only assistant text, and the bridge intercepts structured
function_callandcustom_tool_callitems. Making matching prose executable would turn model output into tool execution, which is a boundary worth keeping even when it is inconvenient.Your probe A is what made the scoping possible — it proved the gateway can express hosted search correctly, so the failure is in the mixed catalog rather than in the relay's capability. Thanks for running both controls.
lidge-jun commentedon Sep 13, 2026
Staying open, with the residual named so the next triage does not re-derive it.
Tonight's web-search work landed next door to this issue without touching it. #4548 (
1a942346997397a994ba4992d5a3d97c2184d147) bound the bridge's sidecar model to the bridge backend, so a global OpenAI sidecar model can no longer be handed to an Anthropic bridge executor. That was a real defect found in the #4515 review, but it is model selection, not continuation.The thing this issue is actually about is still fail-closed: a turn with both
web_searchand a pendingexecstill returnsweb_search_bridge_mixed_tools. The executor side is armed; the missing piece is a continuation design that preserves the call ids and ordering of theexecthe client still has to run. That is a design question, not a wiring gap, which is why nothing tonight moved it.Also relevant and separately tracked: #4519 covers the destination policy for
webSearchBridge.endpoint, whose fix is open in #4555 pending security review.Merge pull request lidge-jun#4515 from lidge-jun/codex/260913-4429-pa…
docs(devlog): record the lane's closure evidence for #4429 and #3719
fix(web-search): replay executed bridge searches to the destination (#…
lidge-jun commentedon Sep 17, 2026
Closed. Both things this issue asked for are on
dev, and the last remaining piece landed just now.Arming the bridge on non-Ollama key-auth gateways:
5b707d3a5c.planPassthroughWebSearchBridgeno longer returns early for a non-ollamabackend;openai,anthropic,xai,geminiandexaare planned on their own credentials, and no Ollama Cloud origin is required, so a self-hosted gateway on an arbitrary base URL arms as you asked. The credential boundary #3761 established still holds: only theollamabackend uses the provider's own API key, and a backend without credentials stays unarmed rather than silently falling through to a different paid search.Intercept, proxy-side execution, and continuing the upstream conversation:
024e43ddf1and3557ada7de. Not dying on a leg mixed with client tools:2e6a0316b9(#4586).Replaying the executed result to the destination on the next turn was the piece still missing, and it is now in as #4919. The memo is keyed by destination scope and hosted cell id, and every miss is a no-op — unknown id, expiry, a different destination, or a
call_idalready present in the body all leave the item alone. It does not silently re-run the search, which would both bill twice and answer with a different search than the history claims, and it does not synthesize result text.On the hosted versus client distinction: a namespaced
ns__web_searchis a separate tool identity the client declares and executes, so it is not intercepted, andweb_searchwas not added to the undeclared-tool guard's allowed names — that would approve a call nobody can execute rather than remove it.One separate symptom is worth keeping in view, and it is not a code defect here: whether your gateway executes Kimi's builtin
$web_searchserver-side and echoes only the call. That is the live probe you offered to run; if it turns out that way, reply on this thread rather than opening a new issue, since the disposition would be different.