Skip to content

feat(protocols): implement T3 web_search non-preview tool + WebSearchCall.results - #1304

Merged
slin1237 merged 2 commits into
mainfrom
feat/audit-t3-web-search
Apr 22, 2026
Merged

slin1237 merged 2 commits into
mainfrom
feat/audit-t3-web-search

Conversation

@slin1237

@slin1237 slin1237 commented Apr 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Add the OpenAI Responses API non-preview `web_search` tool variant and wire the `WebSearchCall.results` output field so the P4 `web_search_call.results` include-field (shipped in #1274) has a typed carrier. Spec reference: `openai-responses-api-spec.md` §tools line 439-440.

What changed

  • `crates/protocols/src/responses.rs`
    • New `ResponseTool::WebSearch(WebSearchTool)` variant. Tag `"web_search"` canonical, `"web_search_2025_08_26"` accepted as `#[serde(alias = ...)]`. Canonical serialization always emits `"web_search"`.
    • New `WebSearchTool { filters?, search_context_size?, user_location? }` with `#[serde(deny_unknown_fields)]`.
    • New `WebSearchFilters { allowed_domains? }` with `#[serde(deny_unknown_fields)]`.
    • New `WebSearchContextSize` closed enum with `Low | Medium | High` (snake_case wire).
    • New `WebSearchUserLocation { city?, country?, region?, timezone?, location_type? }` — `location_type` is serde-renamed to `"type"`.
    • `ResponseOutputItem::WebSearchCall` gains `results: Option<Vec>` with `#[serde(default, skip_serializing_if = "Option::is_none")]`. Default wire shape is byte-identical — absent `results` emits zero bytes and no key.
    • New `WebSearchResult { url, title?, snippet?, score? }` modeled on `FileSearchResult` precedent (spec does not enumerate the inner shape; see Notes below).
    • Two serde round-trip tests pin the tool declaration (canonical + alias + all-fields-populated) and the output struct (populated + absent cases, including explicit byte-identity assertion for the default shape).
  • `crates/mcp/src/transform/transformer.rs` — only production `WebSearchCall` construction site. Initialized `results: None` so MCP-transformed outputs keep the pre-T3 wire shape. Test destructure updated.
  • `model_gateway/src/routers/grpc/harmony/builder.rs` — added compile-forced arm for `ResponseTool::WebSearch(_) => "web_search"`.
  • `model_gateway/src/routers/openai/responses/utils.rs` — added compile-forced arm in `response_tool_to_value`; updated doc comment.

Why

Spec §tools line 439-440 distinguishes non-preview `web_search` from `web_search_preview` — non-preview adds `filters.allowed_domains` and pins `search_context_size` to a typed enum. P4 (merged in #1274) already landed the matching `IncludeField` variants for `web_search_call.results` and `web_search_call.action.sources`; T3 now wires the actual output carrier so the wire round-trip is complete and closes the gemini-bot flag raised on P4 that `WebSearchCall.results` had no typed home.

How

  • Tagged enum variant on `ResponseTool` uses `#[serde(rename = "web_search", alias = "web_search_2025_08_26")]` — deserialization accepts both tags, serialization always emits the canonical tag (verified by lead-probe tests during review).
  • `results` field gated by `skip_serializing_if = "Option::is_none"` plus `#[serde(default)]` on deserialize so pre-T3 clients round-trip byte-identical.
  • Construction-site fix in the MCP transformer is compile-forced: the enum is not `#[non_exhaustive]`, and removing the `results: None` initializer yields E0063 at rustc time (verified via revert-test during review).

Scrutiny passes (lead review)

  • Spec anchors (line 439-440) confirmed field-by-field: alias spelling `web_search_2025_08_26` verbatim, `filters { allowed_domains }`, `search_context_size` enum values `"low"|"medium"|"high"`, `user_location { city?, country? ISO2, region?, timezone? IANA, type? "approximate" }`.
  • §7 pass: all 5 new public items (`WebSearchTool`, `WebSearchFilters`, `WebSearchContextSize`, `WebSearchUserLocation`, `WebSearchResult`) are referenced in-file; no dead types.
  • Compile-forced revert-test: removing `results: None` in `transformer.rs` produces E0063 — construction site coverage is complete.
  • Lead-probe round-trips (executed then reverted) covered: minimal `{"type":"web_search"}` with no optional fields; alias canonicalization; every `search_context_size` enum value + unknown-rejection; `deny_unknown_fields` on all three new structs; byte-identity of the pre-T3 `WebSearchCall` wire shape; full + partial `user_location`.
  • `cargo fmt --all --check`, `cargo clippy -p openai-protocol -p smg-mcp -p smg --all-targets -- -D warnings`, and `codespell` (on the T3 diff) all clean.

Notes & scope

  • WebSearchResult inner shape is precedent-based. The spec document used for this audit enumerates `"web_search_call.results"` as an include field (line 73) but does not list the per-entry fields. The chosen `{ url, title?, snippet?, score? }` shape mirrors `FileSearchResult` precedent. No production code path populates the field today — `transformer.rs` explicitly sets `results: None` — so the speculative shape carries zero wire risk until a populating path is added. Revisit if upstream OpenAI schemas later formalize a different shape.
  • Out of scope (follow-up). MCP built-in routing filters in `model_gateway/src/routers/common/mcp_utils.rs` and `.../grpc/common/responses/utils.rs` still match only `WebSearchPreview | CodeInterpreter` against `BuiltinToolType`. Non-preview `WebSearch` is accepted at the protocol layer but is not yet MCP-routed. `BuiltinToolType` gaining a `WebSearch` variant plus the downstream wiring is a separate task; T3 intentionally stays protocol-only.

Test plan

  • `cargo test -p openai-protocol test_web_search` — both new tests pass (tool round-trip + `WebSearchCall.results` populated + absent).
  • `cargo check --workspace --all-targets` — clean.
  • `cargo clippy -p openai-protocol -p smg-mcp -p smg --all-targets -- -D warnings` — clean.
  • Manual revert-test on `transformer.rs` confirms the `results` field is compile-forced at the single construction site.
  • Byte-identity of the pre-T3 `WebSearchCall` wire shape asserted in `test_web_search_call_results_round_trip` (no `"results"` key leaks when the field is `None`).

Refs: T3

Summary by CodeRabbit

  • New Features

    • Web search is now a production built-in tool (no longer preview).
    • Configurable options: domain allowlist, context size levels (low/medium/high), and user location.
    • Search results include URL, title, snippet, and relevance score; results are optional and omitted when not provided.
  • Tests

    • Added JSON round-trip tests covering the web search tool and result handling.

… field

What: add `ResponseTool::WebSearch(WebSearchTool)` variant with typed
`filters { allowed_domains }`, `search_context_size` enum (low|medium|
high), and `user_location { city, country, region, timezone, type }`
sub-shapes. Wire the `results: Option<Vec<WebSearchResult>>` field on
`ResponseOutputItem::WebSearchCall` so callers requesting
`web_search_call.results` via the top-level `include[]` array receive a
typed array. Accept the versioned alias `web_search_2025_08_26` on
deserialization.

Why: OpenAI Responses spec §tools line 439 defines `web_search` as a
distinct tool from the existing `web_search_preview` — non-preview adds
`filters.allowed_domains` and constrains `search_context_size` to a
typed enum. P4 (merged in #1274) already landed the matching `IncludeField`
variants for `web_search_call.results` and `web_search_call.action.sources`;
T3 now wires the actual output struct so the wire round-trip is complete.

How: new tagged variant on `ResponseTool` using `#[serde(rename =
"web_search", alias = "web_search_2025_08_26")]` so canonical
serialization emits `"web_search"` while still accepting the dated tag.
The `WebSearchCall` output struct gains `results` gated by
`#[serde(default, skip_serializing_if = "Option::is_none")]` — absent
results serialize byte-identically to `{id, action, status, type}`
per spec, populated results ride alongside. Mirrors the
`FileSearchResult` shape precedent for the `results` entry fields.
Construction sites in the MCP response transformer and exhaustive matches
in the model_gateway harmony builder and OpenAI responses utils are
updated to compile. Two serde round-trip tests pin both the tool
declaration and the output struct (populated + absent cases).

Refs: T3
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions github-actions Bot added grpc gRPC client and router changes mcp MCP related changes protocols Protocols crate changes model-gateway Model gateway crate changes openai OpenAI router changes labels Apr 22, 2026
@coderabbitai

coderabbitai Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5d28e1a5-56e5-468f-9964-9a76627030ea

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea4370 and b2485f0.

📒 Files selected for processing (1)
  • model_gateway/src/routers/grpc/harmony/builder.rs

📝 Walkthrough

Walkthrough

Adds a non-preview web_search Responses API tool with typed config and results, updates MCP transformer to emit an explicit results: None for web search calls, and extends model-gateway builders and OpenAI utilities to recognize and serialize the new WebSearch tool variant.

Changes

Cohort / File(s) Summary
Web Search Protocol & Types
crates/protocols/src/responses.rs
Added ResponseTool::WebSearch(WebSearchTool) (with serde alias), new types WebSearchTool, WebSearchFilters, WebSearchContextSize, WebSearchUserLocation, WebSearchResult; added optional results: Option<Vec<WebSearchResult>> to ResponseOutputItem::WebSearchCall; included round-trip serialization tests.
MCP Transformer
crates/mcp/src/transform/transformer.rs
Constructs ResponseOutputItem::WebSearchCall with explicit results: None; updated tests to destructure and assert results.is_none().
Gateway Builders & Utilities
model_gateway/src/routers/grpc/harmony/builder.rs, model_gateway/src/routers/openai/responses/utils.rs
Treat ResponseTool::WebSearch(_) as a built-in tool in harmony builder (maps to "web_search") and serialize WebSearch via serde_json::to_value in OpenAI response utils; added "web_search" to BUILTIN_TOOLS mapping.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

tests

Suggested reviewers

  • CatherineSue
  • key4ng
  • claude

Poem

🐇 I hopped through code, a tiny search delight,
Added a tool for searches, tidy and light,
Results optional, left neatly alone,
From protocol roots to gateway bone,
A rabbit's nibble, shipping done tonight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: implementing a non-preview T3 web_search tool variant and the WebSearchCall.results field in the protocols crate.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/audit-t3-web-search

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

.map(|tool| match tool {
ResponseTool::Function(_) => "function",
ResponseTool::WebSearchPreview(_) => "web_search_preview",
ResponseTool::WebSearch(_) => "web_search",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: "web_search" is added to the tool_types collection here, but the BUILTIN_TOOLS constant (line 63) and ToolLike::is_builtin() impl for ResponseTool (line 110) in this same file were not updated to include the new variant.

This means has_custom_tools(&tool_types) at line 441 will return true when web_search is the only tool in the request (since "web_search" is not in BUILTIN_TOOLS), incorrectly triggering the developer-message injection path.

Similarly, collect_builtin_routing and extract_builtin_types in mcp_utils.rs, and ensure_mcp_connection in grpc/common/responses/utils.rs, don't recognize ResponseTool::WebSearch as a builtin — though that full fix also needs a BuiltinToolType::WebSearch enum variant in crates/mcp/src/core/config.rs. If MCP routing for the non-preview variant is intentionally deferred, at minimum BUILTIN_TOOLS and is_builtin() in this file should be updated.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
model_gateway/src/routers/grpc/harmony/builder.rs (1)

423-433: ⚠️ Potential issue | 🟠 Major

web_search is mapped but still classified as custom in Harmony.

Line 432 adds "web_search" to tool_types, but BUILTIN_TOOLS (Line 63) still lacks "web_search". That makes has_custom_tools() return true for a builtin-only request and changes system/developer message shaping.

💡 Proposed fix
 const BUILTIN_TOOLS: &[&str] = &[
+    "web_search",
     "web_search_preview",
     "code_interpreter",
     "container",
     "file_search",
 ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/grpc/harmony/builder.rs` around lines 423 - 433,
The mapping adds "web_search" to tool_types but BUILTIN_TOOLS (used by
has_custom_tools()) does not include it, causing builtin-only requests to be
treated as custom; update the BUILTIN_TOOLS set/array (the symbol named
BUILTIN_TOOLS) to include "web_search" so has_custom_tools() returns false for
requests containing only builtins, and ensure this aligns with the ResponseTool
enum mapping in the tool_types construction in builder.rs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@model_gateway/src/routers/grpc/harmony/builder.rs`:
- Around line 423-433: The mapping adds "web_search" to tool_types but
BUILTIN_TOOLS (used by has_custom_tools()) does not include it, causing
builtin-only requests to be treated as custom; update the BUILTIN_TOOLS
set/array (the symbol named BUILTIN_TOOLS) to include "web_search" so
has_custom_tools() returns false for requests containing only builtins, and
ensure this aligns with the ResponseTool enum mapping in the tool_types
construction in builder.rs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 14b6c241-8269-41ec-bd64-4e34d803b2c9

📥 Commits

Reviewing files that changed from the base of the PR and between 97dc848 and 8ea4370.

📒 Files selected for processing (4)
  • crates/mcp/src/transform/transformer.rs
  • crates/protocols/src/responses.rs
  • model_gateway/src/routers/grpc/harmony/builder.rs
  • model_gateway/src/routers/openai/responses/utils.rs

@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: 8ea4370a03

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/protocols/src/responses.rs
…S + is_builtin)

Claude nit on PR #1304 comment 3121138818: adding `"web_search"` to the
tool_types collection in `extract_tool_types_from_response_tools` without
also adding it to the `BUILTIN_TOOLS` slice and `ToolLike::is_builtin()`
for `ResponseTool` meant `has_custom_tools` would misclassify web-search-
only requests as custom. Follow the T1 file_search pattern: add
`"web_search"` to the BUILTIN_TOOLS slice at L63 and add
`ResponseTool::WebSearch(_)` to the `matches!` arm in `is_builtin()`
at L108-112.

Codex P1 comment 3121145843 (MCP routing for web_search) deliberately
left for a follow-up task — the proper fix spans the `BuiltinToolType`
enum in `crates/mcp/src/core/config.rs` and its ~30 callsites plus
`grpc/common/responses/utils.rs::ensure_mcp_connection`, which is out
of T3's protocol-only charter.

Refs: T3
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

grpc gRPC client and router changes mcp MCP related changes model-gateway Model gateway crate changes openai OpenAI router changes protocols Protocols crate changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant