Serialize Responses routing fields before large inputs (#49675)

## Why

Gateways that inspect request bodies incrementally need routing fields before potentially multi-megabyte prompts so they can route requests without buffering the input first.

## What changed

Serialize `model`, `stream`, and optional `service_tier` before `input` in both HTTP Responses requests and WebSocket `response.create` messages. Preserve field values and omission of unset `service_tier`.

## Testing

Add an HTTP regression test with a 2 MiB input that checks routing field order and unchanged JSON contents. Extend WebSocket serialization coverage to assert the routing fields appear first, and check that HTTP requests omit unset `service_tier`.

GitOrigin-RevId: dbc174e1fe7ca30d0c00f075bc37dd5f97b27b37
This commit is contained in:
Justin Beckwith
2026-09-30 16:57:01 +00:00
committed by copyberry
parent 67727e7cf1
commit ed0cc1a4ab
3 changed files with 71 additions and 6 deletions
+10 -6
View File
@@ -277,7 +277,12 @@ impl Serialize for ResponsesApiTools {
#[derive(Debug, Serialize, Clone, PartialEq)]
pub struct ResponsesApiRequest {
// Keep routing fields first: serde serializes struct fields in declaration order, and
// gateways may inspect request bodies incrementally before potentially multi-megabyte input.
pub model: String,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "String::is_empty")]
pub instructions: String,
pub input: Vec<ResponseItem>,
@@ -287,13 +292,10 @@ pub struct ResponsesApiRequest {
pub parallel_tool_calls: bool,
pub reasoning: Option<Reasoning>,
pub store: bool,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
pub include: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextControls>,
@@ -330,7 +332,12 @@ impl<'a> From<&'a ResponsesApiRequest> for ResponseCreateWsRequest<'a> {
#[derive(Debug, Serialize)]
pub struct ResponseCreateWsRequest<'a> {
// Keep routing fields first: serde serializes struct fields in declaration order, and
// gateways may inspect request bodies incrementally before potentially multi-megabyte input.
pub model: &'a str,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<&'a str>,
#[serde(skip_serializing_if = "str::is_empty")]
pub instructions: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -342,13 +349,10 @@ pub struct ResponseCreateWsRequest<'a> {
pub parallel_tool_calls: bool,
pub reasoning: Option<&'a Reasoning>,
pub store: bool,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<&'a StreamOptions>,
pub include: &'a [String],
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<&'a TextControls>,
@@ -997,6 +997,9 @@ mod tests {
expected_payload["generate"] = json!(false);
let request_text =
serialize_websocket_request(&request).expect("serialize websocket request");
assert!(request_text.starts_with(
r#"{"type":"response.create","model":"gpt-test","stream":true,"service_tier":"priority","instructions":"Use the available tools.","previous_response_id":"resp-1","input":"#
));
let wire_payload =
serde_json::from_str::<Value>(&request_text).expect("parse websocket request");