Skip to content

feat(protocols): implement T9 namespace tool grouping - #1337

Merged
slin1237 merged 2 commits into
mainfrom
feat/audit-t9-namespace
Apr 23, 2026
Merged

slin1237 merged 2 commits into
mainfrom
feat/audit-t9-namespace

Conversation

@slin1237

@slin1237 slin1237 commented Apr 22, 2026 •

Copy link
Copy Markdown
Member

Description

Problem

Task T9 of the Responses API gap audit: the openai-protocol crate has
no representation for the Namespace tool defined in the Responses API
spec. Requests carrying a {"type": "namespace", ...} tool cannot be
deserialized, so namespace-based tool grouping is unreachable end-to-end.

Spec (.claude/_audit/openai-responses-api-spec.md §tools L475):

Namespace { description, name, tools: array of Function | Custom, type: "namespace" }
— inner Function / Custom share the top-level shape.

Audit entry: .claude/_audit/responses-api-gap-audit.md §T9 (L568-578).

Solution

Add the Namespace variant to ResponseTool and a dedicated nested
NamespaceTool enum that restricts elements to Function or Custom
per spec. Wire the two forced-cascade router call sites so the existing
smg lib still compiles. Scope is protocol-only (no guardrails, no
router behavior beyond the exhaustive-match arms that the type checker
forces).

Changes

Protocol (crates/protocols/src/responses.rs):

  • ResponseTool::Namespace { description: String, name: String, tools: Vec<NamespaceTool> } tagged #[serde(rename = "namespace")].
  • New NamespaceTool = Function(FunctionTool) | Custom(CustomTool) enum. Reusing ResponseTool for the inner element type would structurally allow recursive Namespace nesting and hosted/built-in tools — both forbidden by spec.

Integration tests (crates/protocols/tests/responses.rs, 4 new):

  • test_namespace_tool_with_function_round_trip — namespace with a single Function element.
  • test_namespace_tool_with_custom_text_format_round_trip — namespace with a Custom tool using Text format.
  • test_namespace_tool_with_custom_grammar_format_round_trip — namespace with a Custom tool using Grammar format (both lark and regex).
  • test_namespace_tool_mixed_elements_round_trip — single namespace mixing Function and Custom elements.

Forced-cascade router arms (compile-driven, no behavior beyond the pre-existing pattern):

  • response_tool_to_value in model_gateway/src/routers/openai/responses/utils.rs — route Namespace through serde_json::to_value, mirroring how Custom / hosted tools already round-trip back to the client.
  • Harmony tool_types debug mapping in model_gateway/src/routers/grpc/harmony/builder.rs — emit "namespace" to match the spec discriminator (same pattern as all sibling arms).

No guardrails, no validation, no cross-file refactors. Blocks: unblocks T10 (ToolSearch hosted/client tool which depends on the full AnyTool union including Namespace).

Test plan

  • cargo check -p openai-protocol --tests — clean.
  • cargo test -p openai-protocol --test responses — 63 passed (4 new).
  • cargo check -p smg --lib — clean after forced-cascade arms added.
  • cargo fmt --all — clean.
  • cargo clippy -p openai-protocol -p smg --lib --tests -- -D warnings — clean.

Refs: .claude/_audit/responses-api-gap-audit.md §T9, .claude/_audit/openai-responses-api-spec.md L475

Summary by CodeRabbit

  • New Features

    • Tools can be organized into named namespaces with descriptions, grouping function and custom tools for clearer structure and management.
  • Bug Fixes

    • Namespace tools are now preserved and correctly emitted in client-facing tool lists.
  • Tests

    • Added validation to ensure namespace contents accept only function or custom tools and reject nested or invalid entries.

Responses API spec §tools L475 defines a `Namespace` tool that groups
related `Function` / `Custom` tools under a shared name + description.
Inner elements share the top-level shape but are restricted to those
two variants — nested namespaces and hosted/built-in tools are not
permitted as elements.

Protocol changes (`crates/protocols/src/responses.rs`):
- Add `ResponseTool::Namespace { description, name, tools:
  Vec<NamespaceTool> }` tagged `#[serde(rename = "namespace")]`.
- Add a dedicated `NamespaceTool = Function(FunctionTool) |
  Custom(CustomTool)` enum rather than reusing `ResponseTool`. Using
  `ResponseTool` would structurally allow recursive `Namespace` nesting
  and hosted tools as namespace elements — both explicitly forbidden
  by spec.

Forced-cascade router arms (no scope bleed):
- `response_tool_to_value` (openai/responses/utils.rs) — route
  `Namespace` through `serde_json::to_value` so the full payload
  round-trips back to the client, mirroring how `Custom` / hosted
  tools are handled.
- Harmony `tool_types` mapping (grpc/harmony/builder.rs) — emit
  `"namespace"` to match the spec discriminator.

Integration tests (`crates/protocols/tests/responses.rs`, 4 new):
- `test_namespace_tool_with_function_round_trip` — namespace carrying
  a single `Function` element.
- `test_namespace_tool_with_custom_text_format_round_trip` — namespace
  carrying a `Custom` tool with `Text` format.
- `test_namespace_tool_with_custom_grammar_format_round_trip` —
  namespace carrying a `Custom` tool with `Grammar` format, exercised
  for both `lark` and `regex` syntaxes.
- `test_namespace_tool_mixed_elements_round_trip` — single namespace
  mixing `Function` and `Custom` elements.

Refs: T9 (.claude/_audit/responses-api-gap-audit.md §T9, L568-578)
Spec: .claude/_audit/openai-responses-api-spec.md L475
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!

@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: a2e3b967-fd52-4954-9018-a0f6ab2b8a48

📥 Commits

Reviewing files that changed from the base of the PR and between 8de2bab and 3743cce.

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

📝 Walkthrough

Walkthrough

Adds a new ResponseTool::Namespace variant modeled by NamespaceToolDef (name, description, tools) and a NamespaceTool enum that permits only Function and Custom tools. Gateway serialization/mapping and protocol tests updated to handle namespace tools and reject nested/invalid entries.

Changes

Cohort / File(s) Summary
Protocol Definition
crates/protocols/src/responses.rs
Added Namespace(NamespaceToolDef) variant to ResponseTool. Introduced NamespaceToolDef { name, description, tools } and NamespaceTool enum permitting only Function and Custom variants; #[serde(deny_unknown_fields)] applied to namespace payload.
Protocol Tests
crates/protocols/tests/responses.rs
Added tests for round-trip serialize→deserialize equality, validation of Namespace.tools containing Function and Custom entries, preservation of fields (e.g., function.strict, function.name, custom grammar syntax), and negative tests rejecting nested namespaces and disallowed tool types.
Gateway Mapping / Serialization
model_gateway/src/routers/grpc/harmony/builder.rs, model_gateway/src/routers/openai/responses/utils.rs
Updated tool-type mapping to recognize "namespace" and updated response_tool_to_value to serialize ResponseTool::Namespace into the outgoing tools array so namespace payloads are emitted to clients.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • CatherineSue
  • key4ng
  • claude

Poem

🐰 A namespace hops in, tidy and bright,
Holding functions and customs in sight,
No nesting allowed — neat as can be,
Tests clap their paws for each round-trip spree,
A rabbit rejoices: organized delight! ✨

🚥 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 'feat(protocols): implement T9 namespace tool grouping' clearly and specifically describes the main change: adding namespace tool grouping support to the protocols crate as part of Task T9.
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-t9-namespace

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

@github-actions github-actions Bot added grpc gRPC client and router changes tests Test changes protocols Protocols crate changes model-gateway Model gateway crate changes openai OpenAI router changes labels Apr 22, 2026
Comment thread crates/protocols/src/responses.rs Outdated
Comment on lines +414 to +426
#[serde(rename = "namespace")]
Namespace {
/// Human-readable description surfaced to the model alongside the group.
description: String,
/// Stable identifier the model uses to address the namespace in
/// `function_call` / `custom_tool_call` items (via the `namespace` field).
name: String,
/// Tools in this namespace. Spec restricts elements to `Function` or
/// `Custom`; the dedicated [`NamespaceTool`] enum prevents nested
/// namespaces and hosted-tool leakage that the parent `ResponseTool`
/// enum would otherwise allow.
tools: Vec<NamespaceTool>,
},

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: The Namespace variant uses inline struct fields, so it doesn't get #[serde(deny_unknown_fields)] — unlike peer variants like Custom(CustomTool) and FunctionTool where the wrapper struct rejects unexpected keys. A payload like {"type": "namespace", "name": "x", "description": "y", "tools": [], "bogus": 1} will silently succeed here but would fail for Custom.

Consider extracting a NamespaceToolDef struct (with deny_unknown_fields) and using Namespace(NamespaceToolDef) to stay consistent with the other arms. Not blocking — this can land as-is and be tightened later.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/protocols/tests/responses.rs`:
- Around line 1457-1647: Add fail-fast tests that assert deserialization fails
when a Namespace's tools array contains disallowed child types: create two tests
(e.g., test_namespace_tool_rejects_nested_namespace and
test_namespace_tool_rejects_hosted_tool) that build payloads where
ResponseTool::Namespace has a tools entry with "type":"namespace" and another
with a hosted tool type like "file_search", then call
serde_json::from_value(payload) and assert it returns an Err (i.e.,
deserialization fails). Locate the namespace parsing logic via ResponseTool and
NamespaceTool types to ensure these tests cover the invariant that
namespace.tools only allow Function or Custom.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 375894c8-91bb-4c10-9995-3275f027d86e

📥 Commits

Reviewing files that changed from the base of the PR and between baf8b91 and 8de2bab.

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

Comment thread crates/protocols/tests/responses.rs

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

Clean, well-scoped protocol addition. The NamespaceTool enum correctly prevents recursive nesting and hosted-tool leakage per spec. Tests cover all element-type combinations with round-trip verification. One minor nit posted about deny_unknown_fields consistency on the inline struct fields.

0 🔴 Important · 1 🟡 Nit · 0 🟣 Pre-existing

Two follow-ups from PR #1337 review (Claude nit + CodeRabbit nitpick).

1. Extract `NamespaceToolDef` struct (`crates/protocols/src/responses.rs`)
   Claude flagged that the inline struct-variant form of
   `ResponseTool::Namespace { description, name, tools }` cannot carry
   `#[serde(deny_unknown_fields)]`, unlike sibling tuple variants such
   as `Custom(CustomTool)` / `Function(FunctionTool)` whose wrapper
   structs reject unknown keys. A payload with a bogus top-level key
   (e.g. `{"type": "namespace", "name": "x", "bogus": 1, ...}`) was
   silently accepted — inconsistent with how the rest of the enum
   behaves.

   Switched to `ResponseTool::Namespace(NamespaceToolDef)` with
   `#[serde(deny_unknown_fields)]` on the dedicated struct. Updates
   propagate to the two forced-cascade router arms plus the matching
   test destructurings.

2. Add fail-fast tests for disallowed namespace child types
   (`crates/protocols/tests/responses.rs`)
   CodeRabbit requested negative tests locking in the spec invariant
   that namespace.tools may contain only Function or Custom. Added two
   deserialization-failure tests:
   - `test_namespace_tool_rejects_nested_namespace_element` — nested
     `{"type": "namespace", ...}` must fail.
   - `test_namespace_tool_rejects_hosted_tool_element` — hosted tools
     like `file_search` must fail.

Tests: `cargo test -p openai-protocol --test responses` → 65 passed
(2 new negative, 4 round-trip from the prior commit).
Gates: clippy clean, fmt clean.

Refs: review comments on #1337 (claude[bot], coderabbitai[bot]).
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@slin1237

Copy link
Copy Markdown
Member Author

Thanks for the review! Addressed both items in 3743cce:

  1. Claude (deny_unknown_fields consistency) — extracted NamespaceToolDef struct and switched to ResponseTool::Namespace(NamespaceToolDef) so the variant now enforces #[serde(deny_unknown_fields)] alongside siblings like Custom(CustomTool) / FunctionTool. Propagated the tuple-variant form through the two forced-cascade router arms and the existing test destructurings.

  2. CodeRabbit (negative tests) — added test_namespace_tool_rejects_nested_namespace_element and test_namespace_tool_rejects_hosted_tool_element. Both assert serde_json::from_value::<ResponseTool>(payload).is_err() for disallowed child types, locking the spec invariant in against future serde changes.

Namespace suite now runs 6 tests (4 round-trip + 2 negative), 65 passing in total. Clippy + fmt clean.

@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: 3743ccedda

ℹ️ 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".

pub enum NamespaceTool {
/// Function tool — same shape as [`ResponseTool::Function`].
#[serde(rename = "function")]
Function(FunctionTool),

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 Preserve namespace function defer-loading and optional schema

Do not reuse FunctionTool for namespace members: this path flattens into common::Function (which requires parameters and has no defer_loading field), so namespace tools like {"type":"namespace",...,"tools":[{"type":"function","name":"lookup","defer_loading":true}]} cannot be represented faithfully. As a result, valid namespace function definitions either fail deserialization (missing required schema fields) or silently lose namespace-specific function metadata, which breaks deferred namespace/tool-search workflows.

Useful? React with 👍 / 👎.

@slin1237
slin1237 merged commit d0ea9d6 into main Apr 23, 2026
49 checks passed
@slin1237
slin1237 deleted the feat/audit-t9-namespace branch April 23, 2026 00:51
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 model-gateway Model gateway crate changes openai OpenAI router changes protocols Protocols crate changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant