Skip to content

fix(protocols): accept flat Responses tool_choice shape - #1441

Closed
ankrovv wants to merge 2 commits into
smg-project:mainfrom
ankrovv:fix/responses-tool-choice-flat-shape
Closed

ankrovv wants to merge 2 commits into
smg-project:mainfrom
ankrovv:fix/responses-tool-choice-flat-shape

Conversation

@ankrovv

@ankrovv ankrovv commented May 4, 2026 •

Copy link
Copy Markdown

Description

Problem

POST /v1/responses rejects requests that use the OpenAI-spec-correct flat function tool_choice shape with HTTP 400 tool_choice: data did not match any variant of untagged enum ToolChoice.

The OpenAI Responses API uses a flat shape:

{ "tool_choice": { "type": "function", "name": "lookup_city" } }

while Chat Completions uses a nested shape:

{ "tool_choice": { "type": "function", "function": { "name": "lookup_city" } } }

ResponsesRequest.tool_choice reuses the shared ToolChoice enum which models only the nested Chat shape, so serde rejects the spec-correct flat input at the SMG entrypoint, before any router dispatch — affecting both gRPC- and HTTP-backend routing modes equally.

Solution

Add a Responses-specific input variant that accepts the flat shape and normalize it into the existing internal ToolChoice::Function after deserialization. Downstream Harmony preparation, validation, and dispatch continue to operate on the existing internal shape unchanged. The Chat Completions nested shape remains accepted via the same input variant, preserving backcompat.

Changes

  • crates/protocols/src/responses.rs — Responses-specific tool_choice input variant accepting flat shape; normalization to internal ToolChoice.
  • crates/protocols/src/builders/responses/response.rs — flat-shape serialization for Responses output.
  • model_gateway/src/routers/grpc/common/responses/streaming.rs — flat-shape serialization for streaming response.completed.
  • model_gateway/tests/spec/responses.rs — spec coverage for flat-input deserialize, nested-input backcompat, and roundtrip.
  • model_gateway/tests/api/api_endpoints_test.rs — endpoint guard exercising the flat-input path end-to-end.

Test Plan

cargo +nightly fmt -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

All three pass locally. The new spec and endpoint tests cover:

  • flat function tool_choice deserializes and normalizes to internal Function variant
  • nested Chat-style tool_choice continues to deserialize correctly (backcompat)
  • Responses request echo, response-builder output, and streaming response.completed all serialize the flat shape
  • spec-invalid shapes still produce the expected validation errors
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Summary by CodeRabbit

  • New Features

    • Enhanced tool choice handling with support for flat function-style format and improved backward compatibility for alternative input shapes.
  • Bug Fixes

    • Improved API endpoint validation for forced function tool choices.
  • Tests

    • Added comprehensive test coverage for tool choice serialization and validation scenarios, including legacy string format support.

ankrovv added 2 commits May 4, 2026 10:11
The OpenAI Responses API uses a flat function tool_choice shape
`{"type":"function","name":"..."}`, while Chat Completions uses a
nested shape `{"type":"function","function":{"name":"..."}}`. SMG's
shared `ToolChoice` enum modeled only the nested shape, so the Responses
endpoint deserializer failed validation on spec-correct flat input
with `data did not match any variant of untagged enum ToolChoice`,
returning HTTP 400 before any router dispatch.

Add a Responses-specific input variant that accepts the flat shape and
normalizes it into the existing internal `ToolChoice::Function` so
downstream Harmony preparation, validation, and dispatch are unchanged.
Mirror the flat shape on the way out: serialize Responses request,
response builder output, and streaming `response.completed` events with
the Responses wire format. Chat Completions backcompat is preserved.

Test plan
- `cargo +nightly fmt -- --check`
- `cargo clippy --all-targets --all-features -- -D warnings`
- `cargo test`

New regression coverage in `model_gateway/tests/spec/responses.rs` and
`model_gateway/tests/api/api_endpoints_test.rs` covers the flat-input
deserialize path, the nested-input backcompat path, response-builder
serialization, and streaming completed-event shape.

Signed-off-by: Aniruddh Krovvidi <aniruddh.krovvidi@oracle.com>
Cleared pre-existing `cargo clippy --all-targets --all-features -- -D warnings`
failures encountered while validating an unrelated fix. Mechanical
rewrites only; no behavior change.

- collapsible_match / collapsible_if: convert nested `if`/`if let` to
  match arms with guards (chat_template, bucket, accumulator,
  parser_endpoints_test, common/mod).
- manual_div_ceil: replace `if denom > 0 { a / denom } else { fallback }`
  with `a.checked_div(denom).unwrap_or(fallback)` (mcp metrics, tokenizer
  fingerprint).
- explicit_into_iter_loop: drop redundant `.into_iter()` (metrics_aggregator).
- unnecessary_sort_by: replace `sort_by(|a, b| b.cmp(&a))` with
  `sort_by_key(... Reverse(...))` (data_connector memory).

Also fix `model_gateway/tests/otel_tracing_test.rs`: install the OTEL
tracing layer as the default subscriber inside the test so traces are
captured locally and in CI; previously the layer was created but never
set, causing the test to depend on harness ordering.

Test plan
- `cargo +nightly fmt -- --check`
- `cargo clippy --all-targets --all-features -- -D warnings`
- `cargo test`

Signed-off-by: Aniruddh Krovvidi <aniruddh.krovvidi@oracle.com>
@github-actions github-actions Bot added tokenizer Tokenizer related changes grpc gRPC client and router changes mcp MCP related changes tests Test changes data-connector Data connector crate changes protocols Protocols crate changes model-gateway Model gateway crate changes openai OpenAI router changes labels May 4, 2026
@coderabbitai

coderabbitai Bot commented May 4, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR modernizes tool_choice handling in the Responses protocol by storing it as structured JSON values instead of strings, adds comprehensive test coverage for flat function-style tool choice serialization, and applies consistent arithmetic safety refactors (checked_div patterns) across multiple modules alongside minor code cleanups.

Changes

Responses tool_choice JSON Value Refactoring

Layer / File(s) Summary
Data Shape & Serialization Logic
crates/protocols/src/responses.rs
ResponsesRequest.tool_choice and ResponsesResponse.tool_choice change from String to serde_json::Value. New helper functions responses_tool_choice_value(), serialize_responses_tool_choice(), and deserialize_responses_tool_choice() map ToolChoice::Function to/from flat { "type": "function", "name": ... } JSON shape with fallback support for legacy nested and string formats.
Builder Updates
crates/protocols/src/builders/responses/response.rs
ResponsesResponseBuilder.tool_choice field changes from String to Value, defaulting to json!("auto"). Setter signature changes to accept impl Into<Value>, and copy_from_request() now derives tool_choice using the new responses_tool_choice_value() helper.
Streaming Emission
model_gateway/src/routers/grpc/common/responses/streaming.rs
ResponseStreamEventEmitter::emit_completed() now serializes tool_choice via responses_tool_choice_value() instead of direct json!(), producing the flat function-style JSON shape in emitted events.
Tests & Validation
model_gateway/tests/spec/responses.rs, model_gateway/tests/api/api_endpoints_test.rs
New test coverage validates flat function-style tool_choice deserialization, validation against provided tools, serialization round-trip, builder copy behavior, error cases for missing functions, and backward compatibility with nested chat-style and legacy string formats. API endpoint test confirms /v1/responses accepts forced function tool choice without rejection.

Arithmetic Safety & Code Cleanups

Layer / File(s) Summary
Arithmetic Safety Refactors
crates/mcp/src/core/metrics.rs, crates/tokenizer/src/cache/fingerprint.rs, model_gateway/src/policies/bucket.rs, model_gateway/src/core/metrics_aggregator.rs
Replace manual conditional division with checked_div patterns: LatencyStats avg computation, vocabulary sampling step calculation, bucket boundary division, and metrics collection iteration all now use checked_div(...).unwrap_or(...) for defensive arithmetic.
Pattern & Logic Refactors
crates/data_connector/src/memory.rs, crates/tokenizer/src/chat_template.rs, model_gateway/src/routers/openai/responses/accumulator.rs
Sorting logic uses sort_by_key() with Reverse(), conditional assignment detection is reformatted across lines, and ResponseEvent matching moves emptiness check into guard clause; all preserve existing behavior.
Test Infrastructure Updates
model_gateway/tests/api/parser_endpoints_test.rs, model_gateway/tests/common/mod.rs, model_gateway/tests/otel_tracing_test.rs
Consolidate match guard patterns in test setup for worker URL initialization across multiple test contexts; add explicit OTEL tracing layer retrieval and subscriber installation in tracing test.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ResponsesParser
    participant ToolChoiceDeser
    participant ResponseBuilder
    participant ToolChoiceEmitter

    Client->>ResponsesParser: POST /v1/responses with<br/>flat function tool_choice
    ResponsesParser->>ToolChoiceDeser: Deserialize request.tool_choice<br/>{ "type": "function", "name": ... }
    ToolChoiceDeser->>ToolChoiceDeser: Map to ToolChoice::Function
    ResponsesParser->>ResponsesParser: Validate function exists in tools
    ResponsesParser->>ResponseBuilder: Build response via<br/>copy_from_request()
    ResponseBuilder->>ToolChoiceEmitter: Pass tool_choice Value
    ToolChoiceEmitter->>ToolChoiceEmitter: Call responses_tool_choice_value()<br/>to serialize back to flat JSON
    ToolChoiceEmitter->>Client: Return event with flat<br/>tool_choice JSON shape
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #1314: Documents and guards the Responses/Chat ToolChoice protocol separation that this PR implements for flat function JSON handling.
  • #1244: Modifies ResponsesResponse and ResponsesResponseBuilder with additional fields (background, completed_at, conversation) in the same builder/protocol module.
  • #1276: Directly modifies Responses tool_choice representation and serialization helpers in crates/protocols/src/responses.rs.

Suggested labels

protocols, model-gateway, tests, openai, grpc

Suggested reviewers

  • CatherineSue
  • key4ng
  • slin1237

Poem

🐰 A tool choice, once a string so plain,
Now dances as a JSON chain—
Flat functions shine in structured form,
While safety checks keep math from harm,
The rabbit hops through tests so bright! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically summarizes the main change: accepting a flat Responses tool_choice shape instead of rejecting it.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@mergify

mergify Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Hi @ankrovv, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label May 4, 2026

@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: 2

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/policies/bucket.rs (1)

366-377: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard zero-width gap before boundary range math.

Line 366 only avoids worker_cnt == 0; it does not prevent gap == 0 (e.g., when worker_cnt > self.l_max). Then Line 376 underflows on - 1, which can panic in debug builds.

Suggested fix
-        let boundary = if let Some(gap) = self.l_max.checked_div(worker_cnt) {
+        let boundary = if let Some(gap) = self.l_max.checked_div(worker_cnt).filter(|&g| g > 0) {
             self.l_max = usize::MAX;
             prefill_worker_urls
                 .iter()
                 .enumerate()
                 .map(|(i, url)| {
                     let min = i * gap;
                     let max = if i == worker_cnt - 1 {
                         self.l_max
                     } else {
-                        (i + 1) * gap - 1
+                        (i + 1).saturating_mul(gap).saturating_sub(1)
                     };
                     Boundary::new(url.clone(), [min, max])
                 })
                 .collect()
         } else {
             Vec::new()
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/policies/bucket.rs` around lines 366 - 377, The code
assumes gap > 0 after calling self.l_max.checked_div(worker_cnt) but doesn't
guard gap == 0, which causes (i+1)*gap - 1 to underflow; modify the boundary
construction in the block using checked_div so you first check gap != 0 (e.g.,
match on if gap == 0 { /* handle: assign max = self.l_max for all workers or
return a safe range */ } else { ... }) or replace the subtraction with a safe
operation like .saturating_sub(1); update the closure that iterates
prefill_worker_urls (the map with |(i, url)| { let min = i * gap; let max = if i
== worker_cnt - 1 { self.l_max } else { (i + 1) * gap - 1 } }) to use one of
these guards so no underflow can occur when gap == 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/tests/otel_tracing_test.rs`:
- Around line 171-173: The test installs a subscriber built from
tracing_subscriber::registry().with(otel_trace::get_otel_layer()) and sets it as
the thread-local default via tracing::subscriber::set_default, which drops all
tracing fmt output; modify the subscriber construction in otel_tracing_test.rs
(the block that calls otel_trace::get_otel_layer(),
tracing_subscriber::registry(), and tracing::subscriber::set_default) to also
attach a formatting/test-writer layer (e.g., tracing_subscriber::fmt::layer()
configured to write to the test harness) so the subscriber contains both the
OTEL layer and a fmt layer; apply the same change to the
test_grpc_trace_context_injection setup to ensure tracing::info!/warn!/error!
output is not silently discarded.

In `@model_gateway/tests/spec/responses.rs`:
- Around line 927-958: Add a round-trip serialization assertion to
test_deserialize_responses_chat_style_function_tool_choice_backcompat: after
deserializing into ResponsesRequest and validating (in the test function),
serialize the request back to JSON (e.g., via serde_json::to_value or to_string)
and assert that the output uses the flat Responses shape (matching what
test_deserialize_responses_flat_function_tool_choice verifies) — specifically
that the serialized tool_choice is the flat Function variant with a top-level
"name": "lookup_city" (not nested under "function"). This ensures the chat-style
input normalizes back to the flat output shape.

---

Outside diff comments:
In `@model_gateway/src/policies/bucket.rs`:
- Around line 366-377: The code assumes gap > 0 after calling
self.l_max.checked_div(worker_cnt) but doesn't guard gap == 0, which causes
(i+1)*gap - 1 to underflow; modify the boundary construction in the block using
checked_div so you first check gap != 0 (e.g., match on if gap == 0 { /* handle:
assign max = self.l_max for all workers or return a safe range */ } else { ...
}) or replace the subtraction with a safe operation like .saturating_sub(1);
update the closure that iterates prefill_worker_urls (the map with |(i, url)| {
let min = i * gap; let max = if i == worker_cnt - 1 { self.l_max } else { (i +
1) * gap - 1 } }) to use one of these guards so no underflow can occur when gap
== 0.
🪄 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: ed3ebb7b-84b3-496c-b4ed-537e7136daf0

📥 Commits

Reviewing files that changed from the base of the PR and between fe4faa9 and 4d300b6.

📒 Files selected for processing (15)
  • crates/data_connector/src/memory.rs
  • crates/mcp/src/core/metrics.rs
  • crates/protocols/src/builders/responses/response.rs
  • crates/protocols/src/responses.rs
  • crates/tokenizer/src/cache/fingerprint.rs
  • crates/tokenizer/src/chat_template.rs
  • model_gateway/src/core/metrics_aggregator.rs
  • model_gateway/src/policies/bucket.rs
  • model_gateway/src/routers/grpc/common/responses/streaming.rs
  • model_gateway/src/routers/openai/responses/accumulator.rs
  • model_gateway/tests/api/api_endpoints_test.rs
  • model_gateway/tests/api/parser_endpoints_test.rs
  • model_gateway/tests/common/mod.rs
  • model_gateway/tests/otel_tracing_test.rs
  • model_gateway/tests/spec/responses.rs

Comment on lines +171 to +173
let otel_layer = otel_trace::get_otel_layer().expect("Failed to get OTEL layer");
let subscriber = tracing_subscriber::registry().with(otel_layer);
let _subscriber_guard = tracing::subscriber::set_default(subscriber);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Subscriber constructed with only the OTEL layer; all tracing log output on the test thread is silently dropped

tracing_subscriber::registry().with(otel_layer) (line 172) contains no fmt/logging layer. Once set_default installs it as the thread-local default (line 173), it shadows the global subscriber created by init_logging() (which has the fmt layer) for the entire remaining test. On a #[tokio::test] current-thread runtime, every tracing::info!, tracing::warn!, and tracing::error! emitted by the router, middleware, or worker — for roughly 100 lines of async execution — is silently discarded. The test assertions are unaffected (the OTEL layer is present), but diagnosing a future test failure becomes much harder because framework instrumentation won't appear in test output.

Add a fmt test-writer layer to the subscriber so both concerns are served:

🔧 Proposed fix
 let otel_layer = otel_trace::get_otel_layer().expect("Failed to get OTEL layer");
-let subscriber = tracing_subscriber::registry().with(otel_layer);
+let fmt_layer = tracing_subscriber::fmt::layer().with_test_writer();
+let subscriber = tracing_subscriber::registry().with(fmt_layer).with(otel_layer);
 let _subscriber_guard = tracing::subscriber::set_default(subscriber);

Note: test_grpc_trace_context_injection (lines 316–317, unchanged) has the same pattern and would benefit from the same fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/tests/otel_tracing_test.rs` around lines 171 - 173, The test
installs a subscriber built from
tracing_subscriber::registry().with(otel_trace::get_otel_layer()) and sets it as
the thread-local default via tracing::subscriber::set_default, which drops all
tracing fmt output; modify the subscriber construction in otel_tracing_test.rs
(the block that calls otel_trace::get_otel_layer(),
tracing_subscriber::registry(), and tracing::subscriber::set_default) to also
attach a formatting/test-writer layer (e.g., tracing_subscriber::fmt::layer()
configured to write to the test harness) so the subscriber contains both the
OTEL layer and a fmt layer; apply the same change to the
test_grpc_trace_context_injection setup to ensure tracing::info!/warn!/error!
output is not silently discarded.

Comment on lines +927 to +958
#[test]
fn test_deserialize_responses_chat_style_function_tool_choice_backcompat() {
let request: ResponsesRequest = serde_json::from_value(json!({
"input": "test",
"tools": [
{
"type": "function",
"name": "lookup_city",
"parameters": {}
}
],
"tool_choice": {
"type": "function",
"function": {
"name": "lookup_city"
}
}
}))
.expect("Chat-style function tool_choice should remain accepted");

assert!(
matches!(
request.tool_choice,
Some(ToolChoice::Function { ref function, .. }) if function.name == "lookup_city"
),
"Chat-style tool_choice should keep using the internal function choice"
);
assert!(
request.validate().is_ok(),
"Chat-style function tool_choice should still validate"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

test_deserialize_responses_chat_style_function_tool_choice_backcompat is missing the serialization assertion.

The PR's stated contract is that both accepted input shapes (flat Responses and nested Chat-style) mirror the flat Responses shape on output. Test 1 (test_deserialize_responses_flat_function_tool_choice, lines 853-861) verifies round-trip serialization for flat input. This backcompat test verifies deserialization and validation for Chat-style input, but it never checks that the serialized output is also in flat shape — leaving a gap in the critical "normalize → serialize" path.

✅ Proposed serialization assertion for the backcompat test
     assert!(
         request.validate().is_ok(),
         "Chat-style function tool_choice should still validate"
     );
+
+    let serialized = serde_json::to_value(&request).expect("ResponsesRequest should serialize");
+    assert_eq!(
+        serialized["tool_choice"],
+        json!({
+            "type": "function",
+            "name": "lookup_city"
+        }),
+        "Chat-style input should also serialize as flat Responses shape"
+    );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn test_deserialize_responses_chat_style_function_tool_choice_backcompat() {
let request: ResponsesRequest = serde_json::from_value(json!({
"input": "test",
"tools": [
{
"type": "function",
"name": "lookup_city",
"parameters": {}
}
],
"tool_choice": {
"type": "function",
"function": {
"name": "lookup_city"
}
}
}))
.expect("Chat-style function tool_choice should remain accepted");
assert!(
matches!(
request.tool_choice,
Some(ToolChoice::Function { ref function, .. }) if function.name == "lookup_city"
),
"Chat-style tool_choice should keep using the internal function choice"
);
assert!(
request.validate().is_ok(),
"Chat-style function tool_choice should still validate"
);
}
#[test]
fn test_deserialize_responses_chat_style_function_tool_choice_backcompat() {
let request: ResponsesRequest = serde_json::from_value(json!({
"input": "test",
"tools": [
{
"type": "function",
"name": "lookup_city",
"parameters": {}
}
],
"tool_choice": {
"type": "function",
"function": {
"name": "lookup_city"
}
}
}))
.expect("Chat-style function tool_choice should remain accepted");
assert!(
matches!(
request.tool_choice,
Some(ToolChoice::Function { ref function, .. }) if function.name == "lookup_city"
),
"Chat-style tool_choice should keep using the internal function choice"
);
assert!(
request.validate().is_ok(),
"Chat-style function tool_choice should still validate"
);
let serialized = serde_json::to_value(&request).expect("ResponsesRequest should serialize");
assert_eq!(
serialized["tool_choice"],
json!({
"type": "function",
"name": "lookup_city"
}),
"Chat-style input should also serialize as flat Responses shape"
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/tests/spec/responses.rs` around lines 927 - 958, Add a
round-trip serialization assertion to
test_deserialize_responses_chat_style_function_tool_choice_backcompat: after
deserializing into ResponsesRequest and validating (in the test function),
serialize the request back to JSON (e.g., via serde_json::to_value or to_string)
and assert that the output uses the flat Responses shape (matching what
test_deserialize_responses_flat_function_tool_choice verifies) — specifically
that the serialized tool_choice is the flat Function variant with a top-level
"name": "lookup_city" (not nested under "function"). This ensures the chat-style
input normalizes back to the flat output shape.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements support for a "flat" tool choice format in the responses protocol, transitioning the tool_choice field from a String to a serde_json::Value to accommodate more complex structures. It includes custom serialization and deserialization logic to maintain backward compatibility with existing string and chat-style formats. Beyond these protocol changes, the PR performs several code quality improvements, such as replacing manual division checks with checked_div, utilizing sort_by_key for more idiomatic sorting, and refactoring match statements to use if guards. I have no feedback to provide.

@ankrovv

ankrovv commented May 4, 2026

Copy link
Copy Markdown
Author

Fixed by #1276

@ankrovv ankrovv closed this May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data-connector Data connector crate changes grpc gRPC client and router changes mcp MCP related changes model-gateway Model gateway crate changes needs-rebase PR has merge conflicts that need to be resolved openai OpenAI router changes protocols Protocols crate changes tests Test changes tokenizer Tokenizer related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant