Skip to content

fix(responses): let a provider declare the hosted tools it rejects - #5021

Merged
lidge-jun merged 2 commits into
devfrom
codex/lane-s-5002-capability-filter
Sep 18, 2026
Merged

lidge-jun merged 2 commits into
devfrom
codex/lane-s-5002-capability-filter

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Summary

An OpenAI-compatible Responses gateway does not necessarily accept every capability OpenAI accepts, and until now it had no way to say so. The only mechanism was UNSUPPORTED_HOSTED_TOOLS in src/responses/hosted-tool-policy.ts, a table of (model, baseUrl) predicates, so supporting a narrower destination meant shipping a proxy release that named its endpoint.

The reported destination accepts plain Responses requests and function tools but rejects hosted web_search with HTTP 400 unsupported_request. Codex forwards its hosted declaration with every request, so even Reply exactly with OK failed before the model answered:

ERROR: { "error": { "message": "The request uses an invalid or unsupported capability.",
                    "type": "invalid_request_error", "code": "unsupported_request" } }

The only workaround was web_search="disabled", which turns hosted web search off for every provider including the ones that support it.

This adds unsupportedHostedTools to the provider config. stripUnsupportedHostedTools now consults it alongside the built-in table and removes denied declarations from tools, from client-loaded additional_tools, and from tool_choice before serialization. The reporter's own suggested configuration now works as written, on a provider the registry has never heard of:

{ "providers": { "agent-space": {
    "adapter": "openai-responses",
    "supportsResponsesCustomTools": false,
    "unsupportedHostedTools": ["web_search", "web_search_preview"]
} } }

The declaration is additive, not a replacement. The built-in table still covers destinations that reject a tool regardless of configuration, so an operator who never heard of the field stays protected, and a declaration can only deny more — it can never re-enable a known-broken pairing such as grok-4.6 on OpenCode Go.

Two deliberate design choices, both called out because they are the kind of thing that is easier to review than to rediscover:

  • Spelling variants of one capability are aliased. Declaring web_search also denies web_search_preview. The rest of the proxy already folds that pair into a single tool — the parser maps both to one name, Chat ingress accepts them together, and the canonical-field strip lists them in one toolTypes set. Honouring only the spelling the operator happened to write would reproduce the original 400 while the config claimed to have prevented it.
  • The value is validated against a closed vocabulary. The provider schema ends in .passthrough(), so an unvalidated web_serch would be accepted, persisted, and then match no tool — leaving the operator with the exact upstream rejection this field exists to prevent, and nothing explaining why. This is the codexToolMode lesson recorded at src/config/schema/leaf-validators.ts for Feature: per-provider opt-out of code_mode_only tool mode for routed models (deepseek-v4-flash: undeclared exec_command aborts stream, reconnect then fails reasoning replay with DeepSeek 400) #2106.

A provider can also no longer both deny a hosted tool and prefer it in modelPreferHostedTools. The denial wins at request time, so accepting the pair would silently ignore the preference.

The custom-tool half of the report needs no code change: supportsResponsesCustomTools already exists as a provider capability with registry inheritance and editor exposure, and the reporter confirmed it works. The two capabilities are independent and are denied independently; a gateway that rejects both sets both.

Scope note: this is provider-wide, matching the reported need and the supportsResponsesCustomTools precedent. The repository's model* convention is available if a per-model denial is ever required; no registry entry declares the new field, so nothing ships dead.

Closes #5002

Verification

Local verification was not run, because this lane forbids it. No test, focused test, typecheck, build, install, or ocx invocation was executed. Hosted CI on this branch is the executable verification for this change.

Static verification performed instead:

  • Traced the only two callers of isHostedToolUnsupportedForModel. src/adapters/openai-responses/tool-schema.ts passes the expanded set; src/config/schema/leaf-validators.ts still calls it with two arguments, which the new optional parameters preserve.
  • Confirmed the sole caller of stripUnsupportedHostedTools (src/adapters/openai-responses/passthrough.ts) already passes the full provider object, so only the Pick needed widening.
  • Confirmed the four management round-trip points a provider field needs are all present: the zod schema, providerManagementConfigError, PROVIDER_CONFIG_FIELD_POLICY (satisfies Record<keyof OcxProviderConfig, ...>, so omitting it would fail typecheck), and the provider-routes PATCH handler plus read DTO.
  • Read the rest of the passthrough tool chain to confirm nothing else alters a bare hosted declaration for an unclassified key-auth gateway: normalizeFunctionToolSchema returns non-function tools unchanged, and the xAI, Muse, and OpenCode Go normalizers are all destination-gated.
  • Verified DECLARABLE_HOSTED_TOOL_TYPES covers every inbound hostedToolType; the new test asserts that superset relation against the now-exported HOSTED_TOOL_TYPES rather than a copied list, so the two cannot drift silently.

Ratchet and layout, checked because both have broken other pull requests this cycle:

  • The natural host for these cases, tests/responses/openai-responses-passthrough.test.ts, sits at exactly its recorded cap (4,809 lines in tests/fixtures/file-size-baseline.json) and the cap only moves downward, so appending there would have failed the ratchet for every later PR. The regression is a sibling file instead, following d3ca5522db, test(server): hold the prototype-named override case in a sibling file #5011 and test: hold the newest catalog and provider cases in sibling files #5018.
  • responses-hosted-tool-declaration.test.ts is registered in both scripts/test-layout/layout.json (explicit) and tests/fixtures/test-layout-expected.json; the two maps were compared key-by-key and are exactly equal at 1,356 entries.
  • No touched source file is in the ratchet baseline, and the largest one modified (src/server/management/provider-routes.ts, now 1,860 lines) stays under the 2,000-line threshold for unbaselined files.

Union-with-dev check, since a green PR is not a green merge result: this change adds one provider config key and one exported set. It touches no roster, no hard-coded count, no exhaustive adapter or provider map, no locale catalog, and no generated or golden file. #4871 landed the same provider-capability shape across the same files earlier today and is an ancestor of this branch, so the two patterns agree rather than collide.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Docs: the field is documented in docs-site/src/content/docs/reference/configuration/providers.md and the capability boundary in structure/providers/chat-compat.md, whose manifest entry owns src/adapters/ and src/responses/. Security: the change only ever removes a tool declaration from an outbound body. It adds no credential handling, no logging of request contents, and no network or auth behaviour, and it cannot widen a caller's tool selection.

Summary by CodeRabbit

  • New Features

    • Providers can declare hosted tool types they do not support.
    • Unsupported hosted tools are removed from Responses requests, including tool selections and additional tools.
    • Provider settings can be viewed and updated through the provider management API.
    • Recognized tool aliases are handled consistently, with validation for invalid names and conflicting preferences.
  • Documentation

    • Added provider configuration and compatibility guidance.
  • Tests

    • Added coverage for filtering, aliases, validation, and destination-specific behavior.

An OpenAI-compatible Responses gateway does not necessarily accept every
capability OpenAI accepts, and until now it had no way to say so. The only
mechanism was UNSUPPORTED_HOSTED_TOOLS in src/responses/hosted-tool-policy.ts,
a table of (model, baseUrl) predicates, so supporting a narrower destination
meant shipping a proxy release that named its endpoint.

The reported destination (#5002) accepts plain Responses requests and function
tools but rejects hosted web_search with HTTP 400 unsupported_request. Codex
forwards its hosted declaration with every request, so even "Reply exactly with
OK" failed before the model answered, and the only workaround was disabling web
search globally for every provider.

Add unsupportedHostedTools to the provider config. stripUnsupportedHostedTools
now consults it alongside the built-in table and removes denied declarations
from tools, from client-loaded additional_tools, and from tool_choice before
serialization. The declaration is additive: the table still covers destinations
that reject a tool regardless of configuration, so a declaration can only deny
more, never re-enable a known-broken pairing.

Two deliberate properties. Spelling variants of one capability are aliased, so
declaring web_search also denies web_search_preview; the rest of the proxy
already folds that pair into a single tool, and honouring only the spelling the
operator wrote would reproduce the original 400 while the config claimed to have
prevented it. And the value is validated against a closed vocabulary, because
the provider schema ends in .passthrough(): an unvalidated misspelling would be
persisted and then match no tool, leaving the operator with the rejection the
field exists to prevent and nothing explaining why. That is the codexToolMode
lesson from #2106.

A provider can no longer both deny a hosted tool and prefer it in
modelPreferHostedTools; the denial wins at request time, so accepting the pair
would silently ignore the preference.

The custom-tool half of the report needs no change: supportsResponsesCustomTools
already exists as a provider capability and the reporter confirmed it works.
The two are independent and are denied independently.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 18, 2026 05:52
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-18T05:57:35.122459Z 7f73921 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the bug Something isn't working label Sep 18, 2026
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: c7c5a458-4b6e-4759-a8df-a2673e472c21

📥 Commits

Reviewing files that changed from the base of the PR and between 11bc4f7 and 7f73921.

📒 Files selected for processing (12)
  • docs-site/src/content/docs/reference/configuration/providers.md
  • scripts/test-layout/layout.json
  • src/adapters/openai-responses/tool-schema.ts
  • src/config/schema/leaf-validators.ts
  • src/responses/hosted-tool-policy.ts
  • src/responses/schema.ts
  • src/server/auth-cors.ts
  • src/server/management/provider-routes.ts
  • src/types/provider.ts
  • structure/providers/chat-compat.md
  • tests/fixtures/test-layout-expected.json
  • tests/responses/responses-hosted-tool-declaration.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

The PR adds unsupportedHostedTools to provider configuration. It validates declared tool types, supports aliases, exposes PATCH and GET management behavior, rejects conflicts with preferred hosted tools, and filters unsupported declarations from Responses requests.

Changes

Hosted-tool capability declarations

Layer / File(s) Summary
Hosted-tool capability contracts
src/responses/schema.ts, src/responses/hosted-tool-policy.ts, src/types/provider.ts
The PR exports the hosted-tool vocabulary, adds aliased provider declarations, and adds unsupportedHostedTools to OcxProviderConfig.
Provider configuration validation and management
src/config/schema/leaf-validators.ts, src/server/auth-cors.ts, src/server/management/provider-routes.ts, docs-site/src/content/docs/reference/configuration/providers.md
Provider declarations are validated against the closed vocabulary, exposed through provider management APIs, classified as editor-writable, and documented. Conflicts with modelPreferHostedTools are rejected.
Responses request filtering and validation coverage
src/adapters/openai-responses/tool-schema.ts, tests/responses/responses-hosted-tool-declaration.test.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json, structure/providers/chat-compat.md
Responses filtering removes provider-declared hosted tools from tools, additional_tools, and tool_choice. Tests and compatibility documentation cover aliases, validation, built-in policy composition, and preference conflicts.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature · Severity of issue fixed: Medium

Suggested reviewers: luvs01

Sequence Diagram(s)

sequenceDiagram
  participant ProviderConfig
  participant OpenAIResponsesAdapter
  participant HostedToolPolicy
  participant ResponsesGateway
  ProviderConfig->>OpenAIResponsesAdapter: configure unsupportedHostedTools
  OpenAIResponsesAdapter->>HostedToolPolicy: check declared and built-in exclusions
  HostedToolPolicy-->>OpenAIResponsesAdapter: return filtering decisions
  OpenAIResponsesAdapter->>ResponsesGateway: send filtered tools and tool_choice
Loading

Merge Risk: ⚪ Minimal · up to 7f739

No actionable current-head issue remains from this review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 8 files. (4 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: allowing a provider to declare hosted tools that it rejects. It matches the provider-level unsupportedHostedTools configuration and related f…
Linked Issues check ✅ Passed Issue #5002 requires provider-specific filtering without disabling supported Responses or function tools. src/types/provider.ts adds unsupportedHostedTools; src/config/schema/leaf-validators.ts,…
Out of Scope Changes check ✅ Passed The changes stay within Issue #5002. Documentation in docs-site/src/content/docs/reference/configuration/providers.md and structure/providers/chat-compat.md describes the new provider capability. …
Full details: Docstring Coverage

Explanation

Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 8 files. (4 skipped: 4 unsupported.)

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 72 / 80

이 PR은 지금 dev tip(11bc4f708, #4781 native-main 프로필 관리 phase 1, package 2.59.0) 위에서 이슈 #5002를 고친다. 문제의 핵심은 이렇다. Agent.Space 같은 OpenAI 호환 Responses 게이트웨이는 일반 Responses와 function 도구는 받지만, hosted web_search(그리고 별도로 custom 도구)는 HTTP 400 unsupported_request로 거절한다. Codex는 거의 매 요청에 hosted web_search 선언을 붙이므로, "Reply exactly with OK"처럼 검색이 필요 없는 문장만 보내도 모델 답변 전에 실패한다. 지금까지 우회는 web_search="disabled"뿐이었고, 이건 웹검색을 지원하는 다른 제공자까지 한꺼번에 끈다. 하드코딩 테이블 UNSUPPORTED_HOSTED_TOOLS(src/responses/hosted-tool-policy.ts)는 (model, baseUrl) 짝만 알고 있어서, 새 게이트웨이마다 프록시 릴리스가 필요했다.

이 변경이 넣는 답은 제공자 설정 키 unsupportedHostedTools다. stripUnsupportedHostedTools(src/adapters/openai-responses/tool-schema.ts)가 내장 테이블과 함께 이 선언을 보고, 직렬화 전에 tools / client-loaded additional_tools / tool_choice에서 거절된 hosted 선언을 뺀다. 선언은 더하기만 한다. 이미 깨진 짝(예: OpenCode Go의 grok-4.6 + web_search)을 다시 켜 줄 수는 없다. custom 도구 쪽은 이미 있는 supportsResponsesCustomTools: false로 충분하고, 리포터도 그쪽은 확인했다고 본문에 적혀 있다. 두 능력은 서로 독립이다.

설계에서 특히 잘 막은 두 가지는 본문이 직접 적은 그대로다. (1) web_searchweb_search_preview, image_generationimage_gen, computer_use_previewcomputer_use 별칭을 한쪽으로만 적어도 양쪽을 막는다. 파서·Chat ingress·canonical strip이 이미 한 도구로 묶고 있어서, 철자만 맞추면 400이 다시 난다. (2) 스키마가 .passthrough()라서 오타(web_serch)를 그냥 저장하면 strip이 아무 것도 안 하고 upstream 400만 남는다(#2106 codexToolMode 교훈). 그래서 DECLARABLE_HOSTED_TOOL_TYPES 닫힌 목록으로 zod·management·PATCH가 모두 거절한다. modelPreferHostedTools와 같은 도구를 동시에 선호/거절하는 것도 설정 단계에서 막는다(요청 시에는 거절이 이긴다).

관리 왕복도 빠지지 않았다. OcxProviderConfig 타입, leaf-validators 스키마, providerManagementConfigError + PROVIDER_CONFIG_FIELD_POLICY(editor), provider-routes PATCH/null 클리어·읽기 DTO, docs-site providers 표, structure/providers/chat-compat.md 경계 설명이 같이 움직인다. 테스트는 ratchet 캡에 꽉 찬 openai-responses-passthrough.test.ts에 붙이지 않고 #5011/#5018 형제 패턴으로 responses-hosted-tool-declaration.test.ts를 새로 두고 layout.json과 test-layout-expected.json 양쪽에 등록했다. 와이어까지 가는 어댑터 케이스 2개 + strip/별칭/additive/설정 거절 케이스가 본문 주장과 맞다. types.ts/config.ts 대형 분할 캠페인과 충돌하지 않으니 close-don't-rebase 대상이 아니다. 중복 open PR도 이 키워드로는 #5021만 보인다.

라인 hosted-tool-policy.ts / DECLARABLE_HOSTED_TOOL_TYPES - 닫힌 목록이 HOSTED_TOOL_TYPES(inbound schema enum)보다 넓다(computer_use, image_gen, tool_search, local_shell, x_search). 테스트는 "DECLARABLE ⊇ HOSTED"만 검사한다. 클라이언트가 loose/builtin 경로로 보낼 수 있는 이름을 막으려는 의도로는 맞다. 다만 두 목록이 앞으로 따로 자라면 문서·오퍼레이터 혼동이 생길 수 있다.
라인 auth-cors.ts / modelPreferHostedToolsConfigError 호출 순서 - prefer 교차검증은 unsupportedHostedTools 어휘 검사보다 먼저 돈다. typed에 배열이 실려 있으면 동작은 한다. 오타 배열은 나중에야 거절되므로, 같은 PATCH에 오타+prefer가 같이 오면 에러 메시지가 prefer 쪽이 먼저일 수 있다. 치명적이진 않다.
라인 passthrough.ts / injectXaiResponsesXSearch·preferConfiguredHostedTools - 둘 다 strip보다 앞에서 도구를 넣을 수 있다. prefer는 설정 교차검증이 있지만, xaiResponsesXSearch / webSearchBridgeunsupportedHostedTools 교차는 없다. strip이 마지막에 지우므로 upstream 400은 막히지만, 켜 둔 opt-in이 조용히 무시될 수 있다.
provider-routes.ts / unsupportedHostedTools PATCH - null 클리어, 빈 배열 삭제, 어휘 거절이 leaf-validators·auth-cors와 같은 닫힌 목록을 쓴다. 왕복 모양은 supportsResponsesCustomTools 계열과 같다.
tests/.../responses-hosted-tool-declaration.test.ts - 미선언 게이트웨이는 web_search를 그대로 통과시키고, 선언 시에만 뺀다. "고치면서 지원 게이트웨이까지 깨는" 회귀를 직접 잠근 점이 좋다. 로컬 스위트는 레인 금지라 CI가 실행 검증이다(현재 checks 대부분 pending).

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

  • CI(responses 샤드·layout·file-size·gates)가 초록이면 바로 머지해 #5002를 닫을지
  • xaiResponsesXSearch / webSearchBridgeunsupportedHostedTools 교차를 prefer처럼 설정 단계에서 막을지, strip 승리만으로 충분하다고 볼지
  • Dashboard editor UI를 이번 PR에 넣을지(지금은 config/PATCH·docs만; policy는 editor)
  • 제공자 전역 거절만으로 #5002에 충분한지, 나중에 modelUnsupportedHostedTools가 필요한지(본문 scope note와 같음)

너의 추천
CI가 초록이면 머지 후보로 본다. #5002 재현(Agent.Space + Codex hosted web_search)을 설정만으로 푸는 최소·안전한 축이고, 내장 테이블을 덮어쓰지 않으며 형제 테스트·docs·management 왕복이 갖춰져 있다. 머지 후 #5002를 Closes로 닫으면 된다. xAI/bridge 교차검증과 GUI 피커는 필수가 아니면 후속으로 남겨도 된다.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f739213a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


> Decision record: [ADR-0052](../decisions/ADR-0052-reasoning-and-tool-result-compatibility.md)

## Declared hosted-tool denials

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update every structure owner for the changed source areas

This adds the contract only to providers/chat-compat.md, although structure/INDEX.md maps the changed src/adapters/, src/config/, src/responses/, and src/server/ areas to several additional owners, including config.md, transports/responses.md, providers-and-adapters.md, and gui-and-management-api.md. Those canonical documents therefore remain stale about the new provider capability and its management/configuration behavior; update every mapped owner in this change, using links where repeating the full contract would cause drift.

AGENTS.md reference: structure/AGENTS.md:L44-L50

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

Merging with macOS legs outstanding, and recording why rather than leaving it implicit.

At this exact head the full Linux suite (test 1/4 through 4/4), gates, storage policy, enforce-target, the docs build, and the keyring and npm-global smokes are green. The macOS legs are queued behind a saturated hosted-runner pool shared by several concurrent lanes, and the sharded macOS legs are separately known to go silent mid-suite and be cancelled at their job budget — a long-standing defect recorded with six occurrences in #4956, including two from the 2.58.0 round that were previously written off as capacity.

This change is platform-neutral, so waiting on a queue that is both saturated and known-unreliable would delay the work without adding information. The evidence that governs the release is not per-PR macOS legs; it is the full-platform lane=all dispatch at the frozen release candidate, which is held until #4956 has a named cause. Nothing is promoted on the strength of this merge.

Stating the boundary plainly: this is merged on Linux, gates and cross-platform smoke evidence at its exact head, with macOS coverage deferred to the candidate run rather than claimed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant