[Rust Frontend] Use enum-backed domain types for engine outputs and structured outputs (#47283)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Bugen Zhao committed
Jul 2, 2026 at 17:41 UTC
25fcb65d51deef0026aa34e6067703da4a91f956
16 files changed
+760
-603
rust/src/chat/src/output/default/structural_tag.rs
+12
-18
@@ -47,9 +47,8 @@ pub(super) fn apply_structural_tag_constraint(
47
48
// Overwrite any existing structured output settings with the structural tag constraint.
49
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
50
- structural_tag: Some(structural_tag),
50
backend: StructuredOutputBackend::Xgrammar,
52
- ..Default::default()
51
+ ..StructuredOutputsParams::structural_tag(structural_tag)
52
});
53
54
Ok(())
@@ -119,10 +118,11 @@ mod tests {
118
.as_ref()
119
.expect("structured outputs should be set");
120
assert_eq!(params.backend, StructuredOutputBackend::Xgrammar);
122
- serde_json::from_str(
123
- params.structural_tag.as_deref().expect("structural_tag should be set"),
124
- )
125
- .expect("structural_tag should be valid JSON")
121
+ let structural_tag = params
122
+ .constraint
123
+ .as_structural_tag()
124
+ .expect("structured output constraint should be structural_tag");
125
+ serde_json::from_str(structural_tag).expect("structural_tag should be valid JSON")
126
}
127
128
fn structured_outputs(request: &ChatRequest) -> &StructuredOutputsParams {
@@ -161,9 +161,8 @@ mod tests {
161
fn auto_strict_tool_choice_overwrites_existing_json_guidance() {
162
let mut request = request(ChatToolChoice::Auto, vec![chat_tool("search", Some(true))]);
163
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
164
- json: Some(json!({"type": "object"})),
164
backend: StructuredOutputBackend::Xgrammar,
166
- ..Default::default()
165
+ ..StructuredOutputsParams::json(json!({"type": "object"}))
166
});
167
let parser = qwen3_coder_parser(&request.tools);
168
@@ -171,8 +170,7 @@ mod tests {
170
.expect("structural tag should build");
171
172
let params = structured_outputs(&request);
174
- assert!(params.json.is_none());
175
- assert!(params.structural_tag.is_some());
173
+ assert!(params.constraint.is_structural_tag());
174
let tag = structural_tag_value(&request);
175
assert_eq!(tag["type"], "structural_tag");
176
assert!(tag.to_string().contains("search"));
@@ -195,9 +193,8 @@ mod tests {
193
fn required_tool_choice_overwrites_existing_json_object_guidance() {
194
let mut request = request(ChatToolChoice::Required, vec![chat_tool("search", None)]);
195
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
198
- json_object: Some(true),
196
backend: StructuredOutputBackend::Xgrammar,
200
- ..Default::default()
197
+ ..StructuredOutputsParams::json_object()
198
});
199
let parser = qwen3_coder_parser(&request.tools);
200
@@ -205,8 +202,7 @@ mod tests {
202
.expect("structural tag should build");
203
204
let params = structured_outputs(&request);
208
- assert!(params.json_object.is_none());
209
- assert!(params.structural_tag.is_some());
205
+ assert!(params.constraint.is_structural_tag());
206
let tag = structural_tag_value(&request);
207
assert_eq!(tag["type"], "structural_tag");
208
assert!(tag.to_string().contains("search"));
@@ -245,9 +241,8 @@ mod tests {
241
fn none_tool_choice_preserves_existing_json_object_guidance() {
242
let mut request = request(ChatToolChoice::None, vec![chat_tool("search", Some(true))]);
243
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
248
- json_object: Some(true),
244
backend: StructuredOutputBackend::Xgrammar,
250
- ..Default::default()
245
+ ..StructuredOutputsParams::json_object()
246
});
247
let parser = qwen3_coder_parser(&request.tools);
248
@@ -255,7 +250,6 @@ mod tests {
250
.expect("structural tag decision should succeed");
251
252
let params = structured_outputs(&request);
258
- assert_eq!(params.json_object, Some(true));
259
- assert!(params.structural_tag.is_none());
253
+ assert!(params.constraint.is_json_object());
254
}
255
}
rust/src/chat/tests/chat.rs
+34
-23
@@ -16,7 +16,7 @@ use vllm_engine_core_client::protocol::logprobs::{
16
Logprobs, MaybeWireLogprobs, PositionLogprobs, TokenLogprob,
17
};
18
use vllm_engine_core_client::protocol::output::{
19
- EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, StopReason,
19
+ EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs, StopReason,
20
};
21
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
22
use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
@@ -342,7 +342,7 @@ async fn chat_streams_text_events() {
342
);
343
send_outputs(
344
push,
345
- EngineCoreOutputs {
345
+ RequestBatchOutputs {
346
outputs: vec![
347
request_output("chat-1", vec![b'H' as u32], None, None),
348
request_output(
@@ -354,7 +354,8 @@ async fn chat_streams_text_events() {
354
],
355
finished_requests: Some(BTreeSet::from(["chat-1".to_string()])),
356
..Default::default()
357
- },
357
+ }
358
+ .into(),
359
)
360
.await;
361
})
@@ -454,7 +455,7 @@ async fn chat_stream_waits_for_complete_utf8_before_emitting() {
455
let _ = recv_engine_message(dealer).await;
456
send_outputs(
457
push,
457
- EngineCoreOutputs {
458
+ RequestBatchOutputs {
459
outputs: vec![
460
request_output("chat-utf8", bytes_to_token_ids(&[0xe4]), None, None),
461
request_output(
@@ -466,7 +467,8 @@ async fn chat_stream_waits_for_complete_utf8_before_emitting() {
467
],
468
finished_requests: Some(BTreeSet::from(["chat-utf8".to_string()])),
469
..Default::default()
469
- },
470
+ }
471
+ .into(),
472
)
473
.await;
474
})
@@ -542,7 +544,7 @@ async fn chat_stream_flushes_held_text_on_finish() {
544
let _ = recv_engine_message(dealer).await;
545
send_outputs(
546
push,
545
- EngineCoreOutputs {
547
+ RequestBatchOutputs {
548
outputs: vec![request_output(
549
"chat-final-flush",
550
bytes_to_token_ids(b"ok st"),
@@ -551,7 +553,8 @@ async fn chat_stream_flushes_held_text_on_finish() {
553
)],
554
finished_requests: Some(BTreeSet::from(["chat-final-flush".to_string()])),
555
..Default::default()
554
- },
556
+ }
557
+ .into(),
558
)
559
.await;
560
})
@@ -662,7 +665,7 @@ async fn chat_stream_reports_decode_failure_as_error_event() {
665
let _ = recv_engine_message(dealer).await;
666
send_outputs(
667
push,
665
- EngineCoreOutputs {
668
+ RequestBatchOutputs {
669
outputs: vec![request_output(
670
"chat-4",
671
vec![UNKNOWN_DECODE_TOKEN_ID],
@@ -670,7 +673,8 @@ async fn chat_stream_reports_decode_failure_as_error_event() {
673
None,
674
)],
675
..Default::default()
673
- },
676
+ }
677
+ .into(),
678
)
679
.await;
680
})
@@ -725,7 +729,7 @@ async fn chat_stream_preserves_terminal_stop_token_when_requested() {
729
let _ = recv_engine_message(dealer).await;
730
send_outputs(
731
push,
728
- EngineCoreOutputs {
732
+ RequestBatchOutputs {
733
outputs: vec![request_output(
734
"chat-include-stop",
735
vec![b'H' as u32, b'i' as u32, b'!' as u32],
@@ -734,7 +738,8 @@ async fn chat_stream_preserves_terminal_stop_token_when_requested() {
738
)],
739
finished_requests: Some(BTreeSet::from(["chat-include-stop".to_string()])),
740
..Default::default()
737
- },
741
+ }
742
+ .into(),
743
)
744
.await;
745
})
@@ -812,7 +817,7 @@ async fn chat_stream_separates_reasoning_blocks_automatically() {
817
let _ = recv_engine_message(dealer).await;
818
send_outputs(
819
push,
815
- EngineCoreOutputs {
820
+ RequestBatchOutputs {
821
outputs: vec![
822
request_output(
823
"chat-reasoning",
@@ -841,7 +846,8 @@ async fn chat_stream_separates_reasoning_blocks_automatically() {
846
],
847
finished_requests: Some(BTreeSet::from(["chat-reasoning".to_string()])),
848
..Default::default()
844
- },
849
+ }
850
+ .into(),
851
)
852
.await;
853
})
@@ -955,7 +961,7 @@ async fn chat_collectors_return_structured_message_and_visible_text() {
961
let _ = recv_engine_message(dealer).await;
962
send_outputs(
963
push,
958
- EngineCoreOutputs {
964
+ RequestBatchOutputs {
965
outputs: vec![request_output(
966
"chat-collect",
967
bytes_to_token_ids(b"<think>inner</think>outer"),
@@ -964,7 +970,8 @@ async fn chat_collectors_return_structured_message_and_visible_text() {
970
)],
971
finished_requests: Some(BTreeSet::from(["chat-collect".to_string()])),
972
..Default::default()
967
- },
973
+ }
974
+ .into(),
975
)
976
.await;
977
})
@@ -1018,7 +1025,7 @@ async fn chat_explicitly_disables_reasoning_parser() {
1025
let _ = recv_engine_message(dealer).await;
1026
send_outputs(
1027
push,
1021
- EngineCoreOutputs {
1028
+ RequestBatchOutputs {
1029
outputs: vec![
1030
request_output(
1031
"chat-reasoning-disabled",
@@ -1049,7 +1056,8 @@ async fn chat_explicitly_disables_reasoning_parser() {
1056
"chat-reasoning-disabled".to_string()
1057
])),
1058
..Default::default()
1052
- },
1059
+ }
1060
+ .into(),
1061
)
1062
.await;
1063
})
@@ -1096,7 +1104,7 @@ async fn chat_stream_parses_tool_calls_automatically() {
1104
let _ = recv_engine_message(dealer).await;
1105
send_outputs(
1106
push,
1099
- EngineCoreOutputs {
1107
+ RequestBatchOutputs {
1108
outputs: vec![
1109
request_output(
1110
"chat-tool",
@@ -1121,7 +1129,8 @@ async fn chat_stream_parses_tool_calls_automatically() {
1129
],
1130
finished_requests: Some(BTreeSet::from(["chat-tool".to_string()])),
1131
..Default::default()
1124
- },
1132
+ }
1133
+ .into(),
1134
)
1135
.await;
1136
})
@@ -1204,7 +1213,7 @@ async fn chat_collect_message_preserves_tool_call_arguments_in_final_only_mode()
1213
let _ = recv_engine_message(dealer).await;
1214
send_outputs(
1215
push,
1207
- EngineCoreOutputs {
1216
+ RequestBatchOutputs {
1217
outputs: vec![
1218
request_output(
1219
"chat-final-only-tool",
@@ -1231,7 +1240,8 @@ async fn chat_collect_message_preserves_tool_call_arguments_in_final_only_mode()
1240
"chat-final-only-tool".to_string()
1241
])),
1242
..Default::default()
1234
- },
1243
+ }
1244
+ .into(),
1245
)
1246
.await;
1247
})
@@ -1282,7 +1292,7 @@ async fn chat_stream_and_collect_preserve_prompt_and_sample_logprobs() {
1292
let request: EngineCoreRequest = rmp_serde::from_slice(&add[1]).unwrap();
1293
send_outputs(
1294
push,
1285
- EngineCoreOutputs {
1295
+ RequestBatchOutputs {
1296
outputs: vec![
1297
request_output_with_logprobs(
1298
&request.request_id,
@@ -1303,7 +1313,8 @@ async fn chat_stream_and_collect_preserve_prompt_and_sample_logprobs() {
1313
],
1314
finished_requests: Some(BTreeSet::from([request.request_id])),
1315
..Default::default()
1306
- },
1316
+ }
1317
+ .into(),
1318
)
1319
.await;
1320
}
rust/src/engine-core-client/src/client/imp.rs
+5
-6
@@ -17,7 +17,7 @@ use crate::client::{AbortCause, AbortRequest};
17
use crate::error::{client_closed, dispatcher_closed, unexpected_dispatcher_output};
18
use crate::metrics::{LoraInfoExporter, record_scheduler_stats};
19
use crate::protocol::encode_msgpack;
20
-use crate::protocol::output::{ClassifiedEngineCoreOutputs, EngineCoreOutput, EngineCoreOutputs};
20
+use crate::protocol::output::{EngineCoreOutput, EngineCoreOutputs};
21
use crate::protocol::request::EngineCoreRequestType;
22
use crate::protocol::stats::SchedulerStats;
23
use crate::protocol::utility::UtilityOutput;
@@ -351,8 +351,8 @@ pub(crate) async fn run_output_dispatcher_loop(
351
)),
352
}?;
353
354
- match outputs.classify() {
355
- ClassifiedEngineCoreOutputs::RequestBatch(batch) => {
354
+ match outputs {
355
+ EngineCoreOutputs::RequestBatch(batch) => {
356
let senders = inner.take_senders_for_outputs(&batch.outputs);
357
for (output, sender) in batch.outputs.into_iter().zip(senders) {
358
let request_id = output.request_id.clone();
@@ -403,7 +403,7 @@ pub(crate) async fn run_output_dispatcher_loop(
403
let (running, waiting) = inner.lora_adapter_states();
404
lora_info.update(&METRICS.scheduler, running, waiting);
405
}
406
- ClassifiedEngineCoreOutputs::Utility(utility) => {
406
+ EngineCoreOutputs::Utility(utility) => {
407
let call_id = utility.output.call_id;
408
if inner.resolve_utility_output(utility.output) {
409
trace!(
@@ -419,8 +419,7 @@ pub(crate) async fn run_output_dispatcher_loop(
419
);
420
}
421
}
422
- other @ (ClassifiedEngineCoreOutputs::DpControl { .. }
423
- | ClassifiedEngineCoreOutputs::Other(_)) => {
422
+ other => {
423
Err::<(), _>(unexpected_dispatcher_output!(
424
"received unexpected output on main dispatcher path: {other:?}"
425
))?;
rust/src/engine-core-client/src/coordinator/inproc.rs
+5
-5
@@ -11,7 +11,7 @@ use crate::client::imp::ClientInner;
11
use crate::coordinator::handle::{CoordinatorCommand, CoordinatorState};
12
use crate::error::{Error, Result, bail_unexpected_coordinator_output};
13
use crate::protocol::encode_msgpack;
14
-use crate::protocol::output::{ClassifiedEngineCoreOutputs, DpControlMessage, EngineCoreOutputs};
14
+use crate::protocol::output::{DpControlMessage, DpControlOutput, EngineCoreOutputs};
15
use crate::protocol::request::EngineCoreRequestType;
16
17
/// Coordinator-to-engine `START_DP_WAVE` control payload encoded on the
@@ -109,19 +109,19 @@ impl InProcCoordinatorRunner {
109
/// Apply one engine-originated control output to the coordinator state
110
/// machine.
111
async fn handle_outputs(&mut self, outputs: EngineCoreOutputs) -> Result<()> {
112
- match outputs.classify() {
113
- ClassifiedEngineCoreOutputs::RequestBatch(batch)
112
+ match outputs {
113
+ EngineCoreOutputs::RequestBatch(batch)
114
if batch.outputs.is_empty() && batch.finished_requests.is_none() =>
115
{
116
// Stats-only output for coordinator.
117
// Ignore since the Rust coordinator doesn't track stats for
118
// routing decisions.
119
}
120
- ClassifiedEngineCoreOutputs::DpControl {
120
+ EngineCoreOutputs::DpControl(DpControlOutput {
121
engine_index,
122
control,
123
..
124
- } => match control {
124
+ }) => match control {
125
// The engines signals they completed the current wave and are now paused.
126
// Advance the current wave and mark the state as paused.
127
DpControlMessage::WaveComplete(wave) => {
rust/src/engine-core-client/src/error.rs
+2
@@ -25,6 +25,8 @@ pub enum Error {
25
ValueDecode(#[from] rmpv::decode::Error),
26
#[error("messagepack ext value decode failed: {message}")]
27
ExtValueDecode { message: String },
28
+ #[error("invalid structured outputs params: {message}")]
29
+ InvalidStructuredOutputsParams { message: String },
30
#[error("io error")]
31
Io(#[from] std::io::Error),
32
#[error("transport error")]
rust/src/engine-core-client/src/protocol/logprobs/tests.rs
+4
-4
@@ -183,7 +183,7 @@ fn decodes_inline_new_logprobs() {
183
Some(inline_logprobs_value()),
184
None,
185
)))];
186
- let decoded = decode_engine_core_outputs(&frames).unwrap();
186
+ let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
187
188
let logprobs = decoded.outputs[0].new_logprobs.clone().unwrap().into_direct().unwrap();
189
assert_eq!(logprobs, expected_sample_logprobs());
@@ -214,7 +214,7 @@ fn decodes_multipart_new_logprobs() {
214
]),
215
Bytes::from_static(&[1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]),
216
];
217
- let decoded = decode_engine_core_outputs(&frames).unwrap();
217
+ let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
218
219
let logprobs = decoded.outputs[0].new_logprobs.clone().unwrap().into_direct().unwrap();
220
assert_eq!(logprobs, expected_sample_logprobs());
@@ -226,7 +226,7 @@ fn decodes_inline_prompt_logprobs() {
226
None,
227
Some(inline_prompt_logprobs_value()),
228
)))];
229
- let decoded = decode_engine_core_outputs(&frames).unwrap();
229
+ let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
230
231
let logprobs = decoded.outputs[0]
232
.new_prompt_logprobs_tensors
@@ -252,7 +252,7 @@ fn decodes_big_endian_payloads() {
252
])),
253
None,
254
)))];
255
- let decoded = decode_engine_core_outputs(&frames).unwrap();
255
+ let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
256
let logprobs = decoded.outputs[0].new_logprobs.clone().unwrap().into_direct().unwrap();
257
assert_eq!(
258
logprobs,
rust/src/engine-core-client/src/protocol/output.rs
+165
-115
@@ -1,7 +1,7 @@
1
use std::collections::BTreeSet;
2
3
use enum_as_inner::EnumAsInner;
4
-use serde::{Deserialize, Serialize};
4
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
use serde_default::DefaultFromSerde;
6
use serde_repr::{Deserialize_repr, Serialize_repr};
7
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
@@ -131,33 +131,33 @@ impl EngineCoreOutput {
131
}
132
}
133
134
-/// Batch of engine-core outputs returned to a frontend client.
134
+/// Raw Python/msgpack engine-core output envelope.
135
///
136
/// Original Python definition:
137
/// <https://github.com/vllm-project/vllm/blob/f22d6e026798a74e6542a52ef776c054f2de572a/vllm/v1/engine/__init__.py#L186-L214>
138
#[derive(Debug, Clone, PartialEq, Serialize_tuple, Deserialize_tuple, DefaultFromSerde)]
139
-pub struct EngineCoreOutputs {
139
+struct WireEngineCoreOutputs {
140
#[serde(default)]
141
- pub engine_index: u32,
141
+ engine_index: u32,
142
/// Outputs grouped for this client in the current engine tick.
143
#[serde(default)]
144
- pub outputs: Vec<EngineCoreOutput>,
144
+ outputs: Vec<EngineCoreOutput>,
145
#[serde(default)]
146
- pub scheduler_stats: Option<Box<SchedulerStats>>,
146
+ scheduler_stats: Option<Box<SchedulerStats>>,
147
#[serde(default)]
148
- pub timestamp: f64,
148
+ timestamp: f64,
149
#[serde(default)]
150
- pub utility_output: Option<UtilityOutput>,
150
+ utility_output: Option<UtilityOutput>,
151
#[serde(default)]
152
- pub finished_requests: Option<BTreeSet<String>>,
152
+ finished_requests: Option<BTreeSet<String>>,
153
/// In DP mode, signals that the current wave finished and engines are
154
/// paused.
155
#[serde(default)]
156
- pub wave_complete: Option<u32>,
156
+ wave_complete: Option<u32>,
157
/// In DP mode, signals that a request arrived for an old wave and the next
158
/// wave needs to start in other engines.
159
#[serde(default)]
160
- pub start_wave: Option<u32>,
160
+ start_wave: Option<u32>,
161
}
162
163
/// Data-parallel control notifications multiplexed through `EngineCoreOutputs`.
@@ -167,7 +167,7 @@ pub enum DpControlMessage {
167
StartWave(u32),
168
}
169
170
-#[derive(Debug, Clone, PartialEq)]
170
+#[derive(Debug, Clone, PartialEq, Default)]
171
pub struct RequestBatchOutputs {
172
pub engine_index: u32,
173
pub outputs: Vec<EngineCoreOutput>,
@@ -176,30 +176,48 @@ pub struct RequestBatchOutputs {
176
pub finished_requests: Option<BTreeSet<String>>,
177
}
178
179
-#[derive(Debug, Clone, PartialEq)]
179
+#[derive(Debug, Clone, PartialEq, Default)]
180
pub struct UtilityCallOutput {
181
pub engine_index: u32,
182
pub timestamp: f64,
183
pub output: UtilityOutput,
184
}
185
186
-/// Semantic classification of a raw `EngineCoreOutputs` message.
186
+#[derive(Debug, Clone, PartialEq)]
187
+pub struct DpControlOutput {
188
+ pub engine_index: u32,
189
+ pub timestamp: f64,
190
+ pub control: DpControlMessage,
191
+}
192
+
193
+/// Semantic engine-core output families.
194
///
188
-/// Python currently uses one product-shaped wire struct for several distinct
189
-/// output families. This enum exposes those families more explicitly without
190
-/// changing the wire format.
195
+/// Python currently uses one product-shaped wire struct. The Rust protocol
196
+/// exposes the finite semantic families while preserving the same msgpack shape
197
+/// for serialization.
198
#[derive(Debug, Clone, PartialEq, EnumAsInner)]
192
-pub enum ClassifiedEngineCoreOutputs {
199
+pub enum EngineCoreOutputs {
200
RequestBatch(RequestBatchOutputs),
201
Utility(UtilityCallOutput),
195
- DpControl {
196
- engine_index: u32,
197
- timestamp: f64,
198
- control: DpControlMessage,
199
- },
200
- /// Fallback for wire-shape combinations that do not map cleanly onto the
201
- /// current semantic families.
202
- Other(EngineCoreOutputs),
202
+ DpControl(DpControlOutput),
203
+}
204
+
205
+impl From<RequestBatchOutputs> for EngineCoreOutputs {
206
+ fn from(outputs: RequestBatchOutputs) -> Self {
207
+ Self::RequestBatch(outputs)
208
+ }
209
+}
210
+
211
+impl From<UtilityCallOutput> for EngineCoreOutputs {
212
+ fn from(output: UtilityCallOutput) -> Self {
213
+ Self::Utility(output)
214
+ }
215
+}
216
+
217
+impl From<DpControlOutput> for EngineCoreOutputs {
218
+ fn from(output: DpControlOutput) -> Self {
219
+ Self::DpControl(output)
220
+ }
221
}
222
223
impl EngineCoreOutputs {
@@ -209,55 +227,119 @@ impl EngineCoreOutputs {
227
where
228
Frame: AsRef<[u8]>,
229
{
212
- for output in &mut self.outputs {
213
- output.resolve_in_place(frames)?;
230
+ if let Self::RequestBatch(batch) = self {
231
+ for output in &mut batch.outputs {
232
+ output.resolve_in_place(frames)?;
233
+ }
234
}
235
Ok(())
236
}
237
+}
238
218
- /// Classify the raw wire message into a more semantic Rust enum.
219
- pub fn classify(self) -> ClassifiedEngineCoreOutputs {
220
- let has_request_payload = !self.outputs.is_empty()
221
- || self.scheduler_stats.is_some()
222
- || self.finished_requests.is_some();
239
+/// Classify the raw wire message into a more semantic Rust enum.
240
+impl TryFrom<WireEngineCoreOutputs> for EngineCoreOutputs {
241
+ type Error = Error;
242
+
243
+ fn try_from(value: WireEngineCoreOutputs) -> Result<Self> {
244
+ let has_request_payload = !value.outputs.is_empty()
245
+ || value.scheduler_stats.is_some()
246
+ || value.finished_requests.is_some();
247
248
match (
249
has_request_payload,
226
- &self.utility_output,
227
- &self.wave_complete,
228
- &self.start_wave,
250
+ &value.utility_output,
251
+ &value.wave_complete,
252
+ &value.start_wave,
253
) {
230
- (true, None, None, None) => {
231
- ClassifiedEngineCoreOutputs::RequestBatch(RequestBatchOutputs {
232
- engine_index: self.engine_index,
233
- outputs: self.outputs,
234
- scheduler_stats: self.scheduler_stats,
235
- timestamp: self.timestamp,
236
- finished_requests: self.finished_requests,
237
- })
254
+ (true, None, None, None) => Ok(RequestBatchOutputs {
255
+ engine_index: value.engine_index,
256
+ outputs: value.outputs,
257
+ scheduler_stats: value.scheduler_stats,
258
+ timestamp: value.timestamp,
259
+ finished_requests: value.finished_requests,
260
+ }
261
+ .into()),
262
+ (false, Some(_), None, None) => Ok(UtilityCallOutput {
263
+ engine_index: value.engine_index,
264
+ timestamp: value.timestamp,
265
+ output: value.utility_output.unwrap(),
266
}
239
- (false, Some(_), None, None) => {
240
- ClassifiedEngineCoreOutputs::Utility(UtilityCallOutput {
241
- engine_index: self.engine_index,
242
- timestamp: self.timestamp,
243
- output: self.utility_output.unwrap(),
244
- })
267
+ .into()),
268
+ (false, None, Some(_), None) => Ok(DpControlOutput {
269
+ engine_index: value.engine_index,
270
+ timestamp: value.timestamp,
271
+ control: DpControlMessage::WaveComplete(value.wave_complete.unwrap()),
272
}
246
- (false, None, Some(_), None) => ClassifiedEngineCoreOutputs::DpControl {
247
- engine_index: self.engine_index,
248
- timestamp: self.timestamp,
249
- control: DpControlMessage::WaveComplete(self.wave_complete.unwrap()),
273
+ .into()),
274
+ (false, None, None, Some(_)) => Ok(DpControlOutput {
275
+ engine_index: value.engine_index,
276
+ timestamp: value.timestamp,
277
+ control: DpControlMessage::StartWave(value.start_wave.unwrap()),
278
+ }
279
+ .into()),
280
+
281
+ _ => Err(Error::Decode {
282
+ target_type: "EngineCoreOutputs",
283
+ message: "invalid wire shape".to_string(),
284
+ }),
285
+ }
286
+ }
287
+}
288
+
289
+impl From<EngineCoreOutputs> for WireEngineCoreOutputs {
290
+ fn from(value: EngineCoreOutputs) -> Self {
291
+ match value {
292
+ EngineCoreOutputs::RequestBatch(batch) => Self {
293
+ engine_index: batch.engine_index,
294
+ outputs: batch.outputs,
295
+ scheduler_stats: batch.scheduler_stats,
296
+ timestamp: batch.timestamp,
297
+ finished_requests: batch.finished_requests,
298
+ ..Default::default()
299
},
251
- (false, None, None, Some(_)) => ClassifiedEngineCoreOutputs::DpControl {
252
- engine_index: self.engine_index,
253
- timestamp: self.timestamp,
254
- control: DpControlMessage::StartWave(self.start_wave.unwrap()),
300
+ EngineCoreOutputs::Utility(utility) => Self {
301
+ engine_index: utility.engine_index,
302
+ timestamp: utility.timestamp,
303
+ utility_output: Some(utility.output),
304
+ ..Default::default()
305
},
256
- _ => ClassifiedEngineCoreOutputs::Other(self),
306
+ EngineCoreOutputs::DpControl(control) => {
307
+ let (wave_complete, start_wave) = match control.control {
308
+ DpControlMessage::WaveComplete(wave) => (Some(wave), None),
309
+ DpControlMessage::StartWave(wave) => (None, Some(wave)),
310
+ };
311
+ Self {
312
+ engine_index: control.engine_index,
313
+ timestamp: control.timestamp,
314
+ wave_complete,
315
+ start_wave,
316
+ ..Default::default()
317
+ }
318
+ }
319
}
320
}
321
}
322
323
+impl Serialize for EngineCoreOutputs {
324
+ fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
325
+ where
326
+ S: Serializer,
327
+ {
328
+ WireEngineCoreOutputs::from(self.clone()).serialize(serializer)
329
+ }
330
+}
331
+
332
+impl<'de> Deserialize<'de> for EngineCoreOutputs {
333
+ fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
334
+ where
335
+ D: Deserializer<'de>,
336
+ {
337
+ WireEngineCoreOutputs::deserialize(deserializer)?
338
+ .try_into()
339
+ .map_err(serde::de::Error::custom)
340
+ }
341
+}
342
+
343
/// Decode one ordinary or multipart engine-core output message into the strong
344
/// typed public protocol shape.
345
pub fn decode_engine_core_outputs<Frame>(frames: &[Frame]) -> Result<EngineCoreOutputs>
@@ -281,7 +363,7 @@ mod tests {
363
364
#[test]
365
fn engine_core_outputs_roundtrip_finished_fields() {
284
- let outputs = EngineCoreOutputs {
366
+ let outputs = WireEngineCoreOutputs {
367
outputs: vec![EngineCoreOutput {
368
request_id: "req-1".to_string(),
369
new_token_ids: vec![42],
@@ -302,7 +384,7 @@ mod tests {
384
};
385
386
let encoded = encode_msgpack(&outputs).unwrap();
305
- let decoded: EngineCoreOutputs = decode_msgpack(&encoded).unwrap();
387
+ let decoded: WireEngineCoreOutputs = decode_msgpack(&encoded).unwrap();
388
389
assert_eq!(decoded.outputs.len(), 1);
390
assert_eq!(
@@ -317,7 +399,7 @@ mod tests {
399
400
#[test]
401
fn engine_core_outputs_classify_request_batch() {
320
- let outputs = EngineCoreOutputs {
402
+ let outputs = WireEngineCoreOutputs {
403
outputs: vec![EngineCoreOutput {
404
request_id: "req-1".to_string(),
405
new_token_ids: vec![7],
@@ -360,12 +442,12 @@ mod tests {
442
},
443
)
444
"#]]
363
- .assert_debug_eq(&outputs.classify());
445
+ .assert_debug_eq(&EngineCoreOutputs::try_from(outputs).unwrap());
446
}
447
448
#[test]
449
fn engine_core_outputs_classify_utility() {
368
- let outputs = EngineCoreOutputs {
450
+ let outputs = WireEngineCoreOutputs {
451
utility_output: Some(UtilityOutput {
452
call_id: 42_u64.into(),
453
failure_message: None,
@@ -387,31 +469,33 @@ mod tests {
469
},
470
)
471
"#]]
390
- .assert_debug_eq(&outputs.classify());
472
+ .assert_debug_eq(&EngineCoreOutputs::try_from(outputs).unwrap());
473
}
474
475
#[test]
476
fn engine_core_outputs_classify_control() {
395
- let outputs = EngineCoreOutputs {
477
+ let outputs = WireEngineCoreOutputs {
478
start_wave: Some(3),
479
..Default::default()
480
};
481
482
expect_test::expect![[r#"
401
- DpControl {
402
- engine_index: 0,
403
- timestamp: 0.0,
404
- control: StartWave(
405
- 3,
406
- ),
407
- }
483
+ DpControl(
484
+ DpControlOutput {
485
+ engine_index: 0,
486
+ timestamp: 0.0,
487
+ control: StartWave(
488
+ 3,
489
+ ),
490
+ },
491
+ )
492
"#]]
409
- .assert_debug_eq(&outputs.classify());
493
+ .assert_debug_eq(&EngineCoreOutputs::try_from(outputs).unwrap());
494
}
495
496
#[test]
413
- fn engine_core_outputs_classify_mixed_shape_as_raw() {
414
- let outputs = EngineCoreOutputs {
497
+ fn engine_core_outputs_rejects_mixed_shape() {
498
+ let outputs = WireEngineCoreOutputs {
499
outputs: vec![EngineCoreOutput {
500
request_id: "req-1".to_string(),
501
new_token_ids: vec![7],
@@ -425,44 +509,10 @@ mod tests {
509
..Default::default()
510
};
511
428
- expect_test::expect![[r#"
429
- Other(
430
- EngineCoreOutputs {
431
- engine_index: 0,
432
- outputs: [
433
- EngineCoreOutput {
434
- request_id: "req-1",
435
- new_token_ids: [
436
- 7,
437
- ],
438
- new_logprobs: None,
439
- new_prompt_logprobs_tensors: None,
440
- pooling_output: None,
441
- finish_reason: None,
442
- stop_reason: None,
443
- events: None,
444
- kv_transfer_params: None,
445
- trace_headers: None,
446
- prefill_stats: None,
447
- routed_experts: None,
448
- num_nans_in_logits: 0,
449
- },
450
- ],
451
- scheduler_stats: None,
452
- timestamp: 0.0,
453
- utility_output: Some(
454
- UtilityOutput {
455
- call_id: 1,
456
- failure_message: None,
457
- result: None,
458
- },
459
- ),
460
- finished_requests: None,
461
- wave_complete: None,
462
- start_wave: None,
463
- },
464
- )
465
- "#]]
466
- .assert_debug_eq(&outputs.classify());
512
+ let error = EngineCoreOutputs::try_from(outputs).unwrap_err();
513
+ expect_test::expect![[
514
+ r#"messagepack decode failed for EngineCoreOutputs: invalid wire shape"#
515
+ ]]
516
+ .assert_eq(&error.to_string());
517
}
518
}
rust/src/engine-core-client/src/protocol/structured_outputs.rs
+249
-25
@@ -1,4 +1,8 @@
1
-use serde::{Deserialize, Serialize};
1
+use enum_as_inner::EnumAsInner;
2
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
3
+use serde_json::Value;
4
+
5
+use crate::error::{Error, Result};
6
7
/// Structured-output backend selected for EngineCore grammar compilation.
8
///
@@ -15,50 +19,219 @@ pub enum StructuredOutputBackend {
19
LmFormatEnforcer,
20
}
21
18
-/// Parameters for configuring structured outputs (guided decoding).
19
-///
20
-/// Exactly one constraint field (`json`, `regex`, `choice`, `grammar`,
21
-/// `json_object`, or `structural_tag`) should be set. The engine-core
22
-/// backend selects the appropriate grammar compiler based on which field
23
-/// is present.
24
-///
25
-/// Original Python definition:
26
-/// <https://github.com/vllm-project/vllm/blob/f22d6e026798a74e6542a52ef776c054f2de572a/vllm/sampling_params.py#L36-L107>
27
-#[serde_with::skip_serializing_none]
28
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
29
-#[serde(default)]
30
-pub struct StructuredOutputsParams {
22
+/// The single structured-output constraint selected for a request.
23
+#[derive(Debug, Clone, PartialEq, EnumAsInner)]
24
+pub enum StructuredOutputConstraint {
25
/// JSON schema (as a dict/object or JSON string) constraining the output.
32
- pub json: Option<serde_json::Value>,
26
+ Json(Value),
27
/// Regular expression the output must match.
34
- pub regex: Option<String>,
28
+ Regex(String),
29
/// List of allowed output strings (the model must produce one of these).
36
- pub choice: Option<Vec<String>>,
30
+ Choice(Vec<String>),
31
/// Context-free grammar (in EBNF-like notation) the output must conform to.
38
- pub grammar: Option<String>,
39
- /// When `true`, output must be valid JSON (free-form, no schema).
40
- pub json_object: Option<bool>,
32
+ Grammar(String),
33
+ /// Output must be valid JSON (free-form, no schema).
34
+ JsonObject,
35
+ /// Structural tag configuration (JSON-encoded string).
36
+ StructuralTag(String),
37
+}
38
+
39
+/// Additional structured-output options that do not select the constraint mode.
40
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
41
+pub struct StructuredOutputOptions {
42
/// Disable any additional whitespace in guided JSON output.
42
- #[serde(skip_serializing_if = "crate::protocol::is_false")]
43
pub disable_any_whitespace: bool,
44
/// Disable `additionalProperties` in JSON schema output.
45
- #[serde(skip_serializing_if = "crate::protocol::is_false")]
45
pub disable_additional_properties: bool,
46
/// Custom whitespace pattern for guided JSON output.
47
pub whitespace_pattern: Option<String>,
49
- /// Structural tag configuration (JSON-encoded string).
50
- pub structural_tag: Option<String>,
48
+}
49
+
50
+/// Parameters for configuring structured outputs (guided decoding).
51
+///
52
+/// This is the semantic Rust representation: exactly one constraint mode is
53
+/// always selected. The Python/msgpack product-shaped representation is kept in
54
+/// the private wire type below and used only at serde boundaries.
55
+///
56
+/// Original Python definition:
57
+/// <https://github.com/vllm-project/vllm/blob/f22d6e026798a74e6542a52ef776c054f2de572a/vllm/sampling_params.py#L36-L107>
58
+#[derive(Debug, Clone, PartialEq)]
59
+pub struct StructuredOutputsParams {
60
+ pub constraint: StructuredOutputConstraint,
61
+ pub options: StructuredOutputOptions,
62
/// Structured-output backend, mirroring Python's internal `_backend`.
63
///
64
/// User-supplied values are ignored during deserialization. This matches
65
/// Python's request boundary, where `_backend` is set by validation rather
66
/// than accepted as a request-level backend selector.
67
+ pub backend: StructuredOutputBackend,
68
+}
69
+
70
+impl StructuredOutputsParams {
71
+ pub fn json(json: Value) -> Self {
72
+ Self::from_constraint(StructuredOutputConstraint::Json(json))
73
+ }
74
+
75
+ pub fn regex(regex: impl Into<String>) -> Self {
76
+ Self::from_constraint(StructuredOutputConstraint::Regex(regex.into()))
77
+ }
78
+
79
+ pub fn choice(choice: Vec<String>) -> Self {
80
+ Self::from_constraint(StructuredOutputConstraint::Choice(choice))
81
+ }
82
+
83
+ pub fn grammar(grammar: impl Into<String>) -> Self {
84
+ Self::from_constraint(StructuredOutputConstraint::Grammar(grammar.into()))
85
+ }
86
+
87
+ pub fn json_object() -> Self {
88
+ Self::from_constraint(StructuredOutputConstraint::JsonObject)
89
+ }
90
+
91
+ pub fn structural_tag(structural_tag: impl Into<String>) -> Self {
92
+ Self::from_constraint(StructuredOutputConstraint::StructuralTag(
93
+ structural_tag.into(),
94
+ ))
95
+ }
96
+
97
+ fn from_constraint(constraint: StructuredOutputConstraint) -> Self {
98
+ Self {
99
+ constraint,
100
+ options: StructuredOutputOptions::default(),
101
+ backend: StructuredOutputBackend::default(),
102
+ }
103
+ }
104
+}
105
+
106
+/// Wire-compatible structured-output payload used by Python engine-core.
107
+///
108
+/// Python models `StructuredOutputsParams` as a product-shaped dataclass with
109
+/// several optional constraint fields, then validates that exactly one of those
110
+/// fields is present. Rust exposes [`StructuredOutputsParams`] as an enum-backed
111
+/// domain type instead, while using this private wire type for ser/de.
112
+#[serde_with::skip_serializing_none]
113
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
114
+#[serde(default)]
115
+struct WireStructuredOutputsParams {
116
+ json: Option<Value>,
117
+ regex: Option<String>,
118
+ choice: Option<Vec<String>>,
119
+ grammar: Option<String>,
120
+ json_object: Option<bool>,
121
+ #[serde(skip_serializing_if = "crate::protocol::is_false")]
122
+ disable_any_whitespace: bool,
123
+ #[serde(skip_serializing_if = "crate::protocol::is_false")]
124
+ disable_additional_properties: bool,
125
+ whitespace_pattern: Option<String>,
126
+ structural_tag: Option<String>,
127
#[serde(
128
default,
129
rename = "_backend",
130
deserialize_with = "serde_with::rust::deserialize_ignore_any"
131
)]
61
- pub backend: StructuredOutputBackend,
132
+ backend: StructuredOutputBackend,
133
+}
134
+
135
+impl TryFrom<WireStructuredOutputsParams> for StructuredOutputsParams {
136
+ type Error = Error;
137
+
138
+ fn try_from(raw: WireStructuredOutputsParams) -> Result<Self> {
139
+ use StructuredOutputConstraint::*;
140
+
141
+ let mut constraint = None;
142
+
143
+ macro_rules! insert_constraint {
144
+ ($name:literal, $value:expr) => {
145
+ if let Some(value) = $value {
146
+ if let Some((existing, _)) = constraint {
147
+ return Err(Error::InvalidStructuredOutputsParams {
148
+ message: format!(
149
+ "multiple structured output constraints specified: {existing}, {}",
150
+ $name
151
+ ),
152
+ });
153
+ }
154
+ constraint = Some(($name, value));
155
+ }
156
+ };
157
+ }
158
+
159
+ insert_constraint!("json", raw.json.map(Json));
160
+ insert_constraint!("regex", raw.regex.map(Regex));
161
+ insert_constraint!("choice", raw.choice.map(Choice));
162
+ insert_constraint!("grammar", raw.grammar.map(Grammar));
163
+ match raw.json_object {
164
+ Some(true) => {
165
+ insert_constraint!("json_object", Some(JsonObject))
166
+ }
167
+ Some(false) => {
168
+ return Err(Error::InvalidStructuredOutputsParams {
169
+ message: "structured_outputs.json_object must be true if set; omit structured_outputs to disable structured outputs".to_string(),
170
+ });
171
+ }
172
+ None => {}
173
+ }
174
+ insert_constraint!("structural_tag", raw.structural_tag.map(StructuralTag));
175
+
176
+ Ok(Self {
177
+ constraint: constraint.map(|(_, c)| c).ok_or_else(|| {
178
+ Error::InvalidStructuredOutputsParams {
179
+ message: "missing structured output constraint".to_string(),
180
+ }
181
+ })?,
182
+ options: StructuredOutputOptions {
183
+ disable_any_whitespace: raw.disable_any_whitespace,
184
+ disable_additional_properties: raw.disable_additional_properties,
185
+ whitespace_pattern: raw.whitespace_pattern,
186
+ },
187
+ backend: raw.backend,
188
+ })
189
+ }
190
+}
191
+
192
+impl From<StructuredOutputsParams> for WireStructuredOutputsParams {
193
+ fn from(params: StructuredOutputsParams) -> Self {
194
+ let mut raw = Self {
195
+ disable_any_whitespace: params.options.disable_any_whitespace,
196
+ disable_additional_properties: params.options.disable_additional_properties,
197
+ whitespace_pattern: params.options.whitespace_pattern,
198
+ backend: params.backend,
199
+ ..Self::default()
200
+ };
201
+
202
+ match params.constraint {
203
+ StructuredOutputConstraint::Json(json) => raw.json = Some(json),
204
+ StructuredOutputConstraint::Regex(regex) => raw.regex = Some(regex),
205
+ StructuredOutputConstraint::Choice(choice) => raw.choice = Some(choice),
206
+ StructuredOutputConstraint::Grammar(grammar) => raw.grammar = Some(grammar),
207
+ StructuredOutputConstraint::JsonObject => raw.json_object = Some(true),
208
+ StructuredOutputConstraint::StructuralTag(structural_tag) => {
209
+ raw.structural_tag = Some(structural_tag);
210
+ }
211
+ }
212
+
213
+ raw
214
+ }
215
+}
216
+
217
+impl Serialize for StructuredOutputsParams {
218
+ fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
219
+ where
220
+ S: Serializer,
221
+ {
222
+ WireStructuredOutputsParams::from(self.clone()).serialize(serializer)
223
+ }
224
+}
225
+
226
+impl<'de> Deserialize<'de> for StructuredOutputsParams {
227
+ fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
228
+ where
229
+ D: Deserializer<'de>,
230
+ {
231
+ WireStructuredOutputsParams::deserialize(deserializer)?
232
+ .try_into()
233
+ .map_err(serde::de::Error::custom)
234
+ }
235
}
236
237
#[cfg(test)]
@@ -74,8 +247,59 @@ mod tests {
247
.unwrap();
248
249
assert_eq!(params.backend, StructuredOutputBackend::Guidance);
250
+ assert_eq!(params.constraint, StructuredOutputConstraint::JsonObject);
251
252
let value = serde_json::to_value(params).unwrap();
253
assert_eq!(value["_backend"], "guidance");
254
}
255
+
256
+ #[test]
257
+ fn structured_outputs_rejects_missing_constraint() {
258
+ let error =
259
+ serde_json::from_value::<StructuredOutputsParams>(serde_json::json!({})).unwrap_err();
260
+
261
+ assert!(error.to_string().contains("missing structured output constraint"));
262
+ }
263
+
264
+ #[test]
265
+ fn structured_outputs_rejects_multiple_constraints() {
266
+ let error = serde_json::from_value::<StructuredOutputsParams>(serde_json::json!({
267
+ "json": {"type": "object"},
268
+ "regex": ".*",
269
+ }))
270
+ .unwrap_err();
271
+
272
+ assert!(
273
+ error
274
+ .to_string()
275
+ .contains("multiple structured output constraints specified: json, regex")
276
+ );
277
+ }
278
+
279
+ #[test]
280
+ fn structured_outputs_rejects_json_object_false() {
281
+ let error = serde_json::from_value::<StructuredOutputsParams>(serde_json::json!({
282
+ "json_object": false,
283
+ }))
284
+ .unwrap_err();
285
+
286
+ assert!(error.to_string().contains("json_object must be true"));
287
+ }
288
+
289
+ #[test]
290
+ fn structured_outputs_serializes_through_raw_shape() {
291
+ let params = StructuredOutputsParams {
292
+ constraint: StructuredOutputConstraint::StructuralTag(
293
+ r#"{"type":"structural_tag"}"#.to_string(),
294
+ ),
295
+ options: StructuredOutputOptions::default(),
296
+ backend: StructuredOutputBackend::Xgrammar,
297
+ };
298
+
299
+ let value = serde_json::to_value(params).unwrap();
300
+
301
+ assert_eq!(value["structural_tag"], r#"{"type":"structural_tag"}"#);
302
+ assert_eq!(value["_backend"], "xgrammar");
303
+ assert!(value.get("json").is_none());
304
+ }
305
}
rust/src/engine-core-client/src/tests/client.rs
+163
-219
@@ -23,7 +23,8 @@ use crate::protocol::multimodal::{
23
SliceSpec,
24
};
25
use crate::protocol::output::{
26
- EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, decode_engine_core_outputs,
26
+ DpControlMessage, DpControlOutput, EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
27
+ RequestBatchOutputs, UtilityCallOutput, decode_engine_core_outputs,
28
};
29
use crate::protocol::request::{EngineCoreRequest, EngineCoreRequestType};
30
use crate::protocol::sampling::EngineCoreSamplingParams;
@@ -571,8 +572,7 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
572
573
send_outputs(
574
&mut data_socket.push,
574
- EngineCoreOutputs {
575
- engine_index: 0,
575
+ RequestBatchOutputs {
576
outputs: vec![request_output(
577
"req-1",
578
vec![],
@@ -580,17 +580,19 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
580
)],
581
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
582
..Default::default()
583
- },
583
+ }
584
+ .into(),
585
)
586
.await;
587
588
send_outputs(
589
&mut coordinator.output_push,
589
- EngineCoreOutputs {
590
+ DpControlOutput {
591
engine_index: 0,
591
- wave_complete: Some(0),
592
- ..Default::default()
593
- },
592
+ timestamp: 0.0,
593
+ control: DpControlMessage::WaveComplete(0),
594
+ }
595
+ .into(),
596
)
597
.await;
598
@@ -605,8 +607,7 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
607
608
send_outputs(
609
&mut data_socket.push,
608
- EngineCoreOutputs {
609
- engine_index: 0,
610
+ RequestBatchOutputs {
611
outputs: vec![request_output(
612
"req-3",
613
vec![],
@@ -614,7 +615,8 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
615
)],
616
finished_requests: Some(BTreeSet::from(["req-3".to_string()])),
617
..Default::default()
617
- },
618
+ }
619
+ .into(),
620
)
621
.await;
622
@@ -651,7 +653,7 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
653
654
send_outputs(
655
&mut data_socket.push,
654
- EngineCoreOutputs {
656
+ RequestBatchOutputs {
657
engine_index: 1,
658
outputs: vec![request_output(
659
"req-2",
@@ -660,7 +662,8 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
662
)],
663
finished_requests: Some(BTreeSet::from(["req-2".to_string()])),
664
..Default::default()
663
- },
665
+ }
666
+ .into(),
667
)
668
.await;
669
@@ -766,11 +769,12 @@ async fn coordinator_rebroadcasts_engine_start_wave_control() {
769
770
send_outputs(
771
&mut coordinator.output_push,
769
- EngineCoreOutputs {
772
+ DpControlOutput {
773
engine_index: 1,
771
- start_wave: Some(4),
772
- ..Default::default()
773
- },
774
+ timestamp: 0.0,
775
+ control: DpControlMessage::StartWave(4),
776
+ }
777
+ .into(),
778
)
779
.await;
780
@@ -821,15 +825,16 @@ async fn coordinator_accepts_stats_only_outputs() {
825
826
send_outputs(
827
&mut coordinator.output_push,
824
- EngineCoreOutputs {
825
- engine_index: 0,
828
+ RequestBatchOutputs {
829
+ outputs: Vec::new(),
830
scheduler_stats: Some(Box::new(SchedulerStats {
831
num_running_reqs: 1,
832
current_wave: 0,
833
..Default::default()
834
})),
835
..Default::default()
832
- },
836
+ }
837
+ .into(),
838
)
839
.await;
840
@@ -840,8 +845,7 @@ async fn coordinator_accepts_stats_only_outputs() {
845
846
send_outputs(
847
&mut data_socket.push,
843
- EngineCoreOutputs {
844
- engine_index: 0,
848
+ RequestBatchOutputs {
849
outputs: vec![request_output(
850
"req-stats",
851
vec![],
@@ -849,7 +853,8 @@ async fn coordinator_accepts_stats_only_outputs() {
853
)],
854
finished_requests: Some(BTreeSet::from(["req-stats".to_string()])),
855
..Default::default()
852
- },
856
+ }
857
+ .into(),
858
)
859
.await;
860
@@ -910,30 +915,34 @@ async fn client_fail_closes_when_main_output_path_receives_dp_control() {
915
916
send_outputs(
917
push,
913
- EngineCoreOutputs {
914
- utility_output: Some(UtilityOutput {
918
+ UtilityCallOutput {
919
+ output: UtilityOutput {
920
call_id: 1_u64.into(),
921
failure_message: None,
922
result: None,
918
- }),
923
+ },
924
..Default::default()
920
- },
925
+ }
926
+ .into(),
927
)
928
.await;
929
send_outputs(
930
push,
925
- EngineCoreOutputs {
926
- start_wave: Some(3),
927
- ..Default::default()
928
- },
931
+ DpControlOutput {
932
+ engine_index: 0,
933
+ timestamp: 0.0,
934
+ control: DpControlMessage::StartWave(3),
935
+ }
936
+ .into(),
937
)
938
.await;
939
send_outputs(
940
push,
933
- EngineCoreOutputs {
941
+ RequestBatchOutputs {
942
outputs: vec![request_output("req-1", vec![999], None)],
943
..Default::default()
936
- },
944
+ }
945
+ .into(),
946
)
947
.await;
948
@@ -985,91 +994,6 @@ async fn client_fail_closes_when_main_output_path_receives_dp_control() {
994
client.shutdown().await.unwrap();
995
}
996
988
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
989
-async fn client_fail_closes_when_main_output_path_receives_mixed_shape_output() {
990
- init_tracing();
991
- let ipc = IpcNamespace::new().unwrap();
992
- let handshake_address = ipc.handshake_endpoint();
993
- let engine_id = b"engine-0".to_vec();
994
-
995
- let (shutdown_tx, engine_task) = spawn_mock_engine_task(
996
- handshake_address.clone(),
997
- engine_id.clone(),
998
- |dealer, push| {
999
- Box::pin(async move {
1000
- let add_1 = recv_engine_message(dealer).await;
1001
- assert_eq!(add_1[0].as_ref(), &[0x00]);
1002
- let request_1: EngineCoreRequest = rmp_serde::from_slice(&add_1[1]).unwrap();
1003
- assert_eq!(request_1.client_index, 7);
1004
- assert_eq!(request_1.request_id, "req-1");
1005
-
1006
- let add_2 = recv_engine_message(dealer).await;
1007
- assert_eq!(add_2[0].as_ref(), &[0x00]);
1008
- let request_2: EngineCoreRequest = rmp_serde::from_slice(&add_2[1]).unwrap();
1009
- assert_eq!(request_2.client_index, 7);
1010
- assert_eq!(request_2.request_id, "req-2");
1011
-
1012
- send_outputs(
1013
- push,
1014
- EngineCoreOutputs {
1015
- utility_output: Some(UtilityOutput {
1016
- call_id: 1_u64.into(),
1017
- failure_message: None,
1018
- result: None,
1019
- }),
1020
- outputs: vec![request_output("req-1", vec![999], None)],
1021
- ..Default::default()
1022
- },
1023
- )
1024
- .await;
1025
-
1026
- tokio::time::sleep(Duration::from_millis(50)).await;
1027
- })
1028
- },
1029
- );
1030
-
1031
- let client = connect_client_with_ipc(
1032
- handshake_test_config(
1033
- handshake_address,
1034
- 1,
1035
- "test-model",
1036
- Duration::from_secs(2),
1037
- 7,
1038
- None,
1039
- ),
1040
- &ipc,
1041
- )
1042
- .await;
1043
- assert_eq!(client.engine_identities()[0], b"engine-0");
1044
- assert!(client.ready_responses()[0].max_model_len > 0);
1045
-
1046
- let mut stream_1 = client.call(sample_request_with_id("req-1")).await.unwrap();
1047
- let mut stream_2 = client.call(sample_request_with_id("req-2")).await.unwrap();
1048
-
1049
- let error_2 = timeout(Duration::from_secs(1), stream_2.next())
1050
- .await
1051
- .unwrap()
1052
- .unwrap()
1053
- .unwrap_err();
1054
- assert!(is_unexpected_dispatcher_output(&error_2));
1055
-
1056
- let error_1 = timeout(Duration::from_secs(1), stream_1.next())
1057
- .await
1058
- .unwrap()
1059
- .unwrap()
1060
- .unwrap_err();
1061
- assert!(is_unexpected_dispatcher_output(&error_1));
1062
-
1063
- assert!(matches!(
1064
- client.health_error().as_deref(),
1065
- Some(error) if is_unexpected_dispatcher_output(error)
1066
- ));
1067
-
1068
- let _ = shutdown_tx.send(());
1069
- engine_task.await.unwrap();
1070
- client.shutdown().await.unwrap();
1071
-}
1072
-
997
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
998
async fn duplicate_request_ids_are_rejected_without_sending_a_second_add() {
999
init_tracing();
@@ -1091,7 +1015,7 @@ async fn duplicate_request_ids_are_rejected_without_sending_a_second_add() {
1015
1016
send_outputs(
1017
push,
1094
- EngineCoreOutputs {
1018
+ RequestBatchOutputs {
1019
outputs: vec![request_output(
1020
"req-1",
1021
vec![],
@@ -1099,7 +1023,8 @@ async fn duplicate_request_ids_are_rejected_without_sending_a_second_add() {
1023
)],
1024
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
1025
..Default::default()
1102
- },
1026
+ }
1027
+ .into(),
1028
)
1029
.await;
1030
})
@@ -1158,10 +1083,12 @@ async fn finished_requests_without_final_output_is_treated_as_unexpected_close()
1083
1084
send_outputs(
1085
push,
1161
- EngineCoreOutputs {
1086
+ RequestBatchOutputs {
1087
+ outputs: Vec::new(),
1088
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
1089
..Default::default()
1164
- },
1090
+ }
1091
+ .into(),
1092
)
1093
.await;
1094
@@ -1217,10 +1144,11 @@ async fn dropping_a_live_stream_triggers_abort() {
1144
assert_eq!(add[0].as_ref(), &[0x00]);
1145
send_outputs(
1146
push,
1220
- EngineCoreOutputs {
1147
+ RequestBatchOutputs {
1148
outputs: vec![request_output("req-1", vec![99], None)],
1149
..Default::default()
1223
- },
1150
+ }
1151
+ .into(),
1152
)
1153
.await;
1154
@@ -1275,14 +1203,15 @@ async fn dropping_multiple_live_streams_aborts_all_in_a_burst() {
1203
}
1204
send_outputs(
1205
push,
1278
- EngineCoreOutputs {
1206
+ RequestBatchOutputs {
1207
outputs: vec![
1208
request_output("req-1", vec![99], None),
1209
request_output("req-2", vec![99], None),
1210
request_output("req-3", vec![99], None),
1211
],
1212
..Default::default()
1285
- },
1213
+ }
1214
+ .into(),
1215
)
1216
.await;
1217
@@ -1436,14 +1365,16 @@ async fn is_sleeping_wrapper_sends_typed_request_and_returns_typed_response() {
1365
1366
send_outputs(
1367
push,
1439
- EngineCoreOutputs {
1440
- utility_output: Some(UtilityOutput {
1368
+ UtilityCallOutput {
1369
+ engine_index: 0,
1370
+ timestamp: 0.0,
1371
+ output: UtilityOutput {
1372
call_id: call_id.into(),
1373
failure_message: None,
1374
result: Some(utility_result_value(true)),
1444
- }),
1445
- ..Default::default()
1446
- },
1375
+ },
1376
+ }
1377
+ .into(),
1378
)
1379
.await;
1380
})
@@ -1491,14 +1422,16 @@ async fn call_utility_failure_message_surfaces_as_error() {
1422
1423
send_outputs(
1424
push,
1494
- EngineCoreOutputs {
1495
- utility_output: Some(UtilityOutput {
1425
+ UtilityCallOutput {
1426
+ engine_index: 0,
1427
+ timestamp: 0.0,
1428
+ output: UtilityOutput {
1429
call_id: call_id.into(),
1430
failure_message: Some("boom".to_string()),
1431
result: None,
1499
- }),
1500
- ..Default::default()
1501
- },
1432
+ },
1433
+ }
1434
+ .into(),
1435
)
1436
.await;
1437
})
@@ -1815,8 +1748,7 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
1748
finish_req_1_rx.await.unwrap();
1749
send_outputs(
1750
push,
1818
- EngineCoreOutputs {
1819
- engine_index: 0,
1751
+ RequestBatchOutputs {
1752
outputs: vec![request_output(
1753
&request_1.request_id,
1754
vec![10],
@@ -1824,7 +1756,8 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
1756
)],
1757
finished_requests: Some(BTreeSet::from([request_1.request_id.clone()])),
1758
..Default::default()
1827
- },
1759
+ }
1760
+ .into(),
1761
)
1762
.await;
1763
@@ -1836,8 +1769,7 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
1769
finish_req_3_rx.await.unwrap();
1770
send_outputs(
1771
push,
1839
- EngineCoreOutputs {
1840
- engine_index: 0,
1772
+ RequestBatchOutputs {
1773
outputs: vec![request_output(
1774
&request_3.request_id,
1775
vec![30],
@@ -1845,7 +1777,8 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
1777
)],
1778
finished_requests: Some(BTreeSet::from([request_3.request_id.clone()])),
1779
..Default::default()
1848
- },
1780
+ }
1781
+ .into(),
1782
)
1783
.await;
1784
})
@@ -1864,7 +1797,7 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
1797
finish_req_2_rx.await.unwrap();
1798
send_outputs(
1799
push,
1867
- EngineCoreOutputs {
1800
+ RequestBatchOutputs {
1801
engine_index: 1,
1802
outputs: vec![request_output(
1803
&request_2.request_id,
@@ -1873,7 +1806,8 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
1806
)],
1807
finished_requests: Some(BTreeSet::from([request_2.request_id.clone()])),
1808
..Default::default()
1876
- },
1809
+ }
1810
+ .into(),
1811
)
1812
.await;
1813
})
@@ -1990,14 +1924,16 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
1924
assert_eq!(array[2], Value::from("is_sleeping"));
1925
send_outputs(
1926
push,
1993
- EngineCoreOutputs {
1994
- utility_output: Some(UtilityOutput {
1927
+ UtilityCallOutput {
1928
+ engine_index: 0,
1929
+ timestamp: 0.0,
1930
+ output: UtilityOutput {
1931
call_id: call_id.into(),
1932
failure_message: None,
1933
result: Some(utility_result_value(true)),
1998
- }),
1999
- ..Default::default()
2000
- },
1934
+ },
1935
+ }
1936
+ .into(),
1937
)
1938
.await;
1939
@@ -2012,8 +1948,7 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
1948
assert_eq!(aborted_ids, vec!["req-1".to_string()]);
1949
send_outputs(
1950
push,
2015
- EngineCoreOutputs {
2016
- engine_index: 0,
1951
+ RequestBatchOutputs {
1952
outputs: vec![request_output(
1953
"req-1",
1954
vec![],
@@ -2021,7 +1956,8 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
1956
)],
1957
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
1958
..Default::default()
2024
- },
1959
+ }
1960
+ .into(),
1961
)
1962
.await;
1963
})
@@ -2044,14 +1980,16 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
1980
assert_eq!(array[2], Value::from("is_sleeping"));
1981
send_outputs(
1982
push,
2047
- EngineCoreOutputs {
2048
- utility_output: Some(UtilityOutput {
1983
+ UtilityCallOutput {
1984
+ engine_index: 0,
1985
+ timestamp: 0.0,
1986
+ output: UtilityOutput {
1987
call_id: call_id.into(),
1988
failure_message: None,
1989
result: Some(utility_result_value(true)),
2052
- }),
2053
- ..Default::default()
2054
- },
1990
+ },
1991
+ }
1992
+ .into(),
1993
)
1994
.await;
1995
@@ -2066,7 +2004,7 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
2004
assert_eq!(aborted_ids, vec!["req-2".to_string()]);
2005
send_outputs(
2006
push,
2069
- EngineCoreOutputs {
2007
+ RequestBatchOutputs {
2008
engine_index: 1,
2009
outputs: vec![request_output(
2010
"req-2",
@@ -2075,7 +2013,8 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
2013
)],
2014
finished_requests: Some(BTreeSet::from(["req-2".to_string()])),
2015
..Default::default()
2078
- },
2016
+ }
2017
+ .into(),
2018
)
2019
.await;
2020
})
@@ -2155,14 +2094,16 @@ async fn collective_rpc_flattens_results_from_all_engines() {
2094
2095
send_outputs(
2096
push,
2158
- EngineCoreOutputs {
2159
- utility_output: Some(UtilityOutput {
2097
+ UtilityCallOutput {
2098
+ engine_index: 0,
2099
+ timestamp: 0.0,
2100
+ output: UtilityOutput {
2101
call_id: call_id.into(),
2102
failure_message: None,
2103
result: Some(utility_result_value(vec!["engine-0-worker"])),
2163
- }),
2164
- ..Default::default()
2165
- },
2104
+ },
2105
+ }
2106
+ .into(),
2107
)
2108
.await;
2109
})
@@ -2186,14 +2127,16 @@ async fn collective_rpc_flattens_results_from_all_engines() {
2127
2128
send_outputs(
2129
push,
2189
- EngineCoreOutputs {
2190
- utility_output: Some(UtilityOutput {
2130
+ UtilityCallOutput {
2131
+ engine_index: 0,
2132
+ timestamp: 0.0,
2133
+ output: UtilityOutput {
2134
call_id: call_id.into(),
2135
failure_message: None,
2136
result: Some(utility_result_value(vec!["engine-1-worker"])),
2194
- }),
2195
- ..Default::default()
2196
- },
2137
+ },
2138
+ }
2139
+ .into(),
2140
)
2141
.await;
2142
})
@@ -2264,14 +2207,16 @@ fn spawn_mock_utility_engine(
2207
assert_eq!(array[3], expected_args, "unexpected utility args");
2208
send_outputs(
2209
push,
2267
- EngineCoreOutputs {
2268
- utility_output: Some(UtilityOutput {
2210
+ UtilityCallOutput {
2211
+ engine_index: 0,
2212
+ timestamp: 0.0,
2213
+ output: UtilityOutput {
2214
call_id: call_id.into(),
2215
failure_message: None,
2216
result: Some(utility_result_value(result)),
2272
- }),
2273
- ..Default::default()
2274
- },
2217
+ },
2218
+ }
2219
+ .into(),
2220
)
2221
.await;
2222
})
@@ -2552,41 +2497,40 @@ fn python_msgpack_fixtures_match_rust_encoding() {
2497
2498
let decoded_outputs: EngineCoreOutputs = rmp_serde::from_slice(&outputs_bytes).unwrap();
2499
expect_test::expect![[r#"
2555
- EngineCoreOutputs {
2556
- engine_index: 0,
2557
- outputs: [
2558
- EngineCoreOutput {
2559
- request_id: "req-1",
2560
- new_token_ids: [
2561
- 7,
2562
- 8,
2563
- ],
2564
- new_logprobs: None,
2565
- new_prompt_logprobs_tensors: None,
2566
- pooling_output: None,
2567
- finish_reason: Some(
2568
- Length,
2569
- ),
2570
- stop_reason: None,
2571
- events: None,
2572
- kv_transfer_params: None,
2573
- trace_headers: None,
2574
- prefill_stats: None,
2575
- routed_experts: None,
2576
- num_nans_in_logits: 0,
2577
- },
2578
- ],
2579
- scheduler_stats: None,
2580
- timestamp: 0.0,
2581
- utility_output: None,
2582
- finished_requests: Some(
2583
- {
2584
- "req-1",
2585
- },
2586
- ),
2587
- wave_complete: None,
2588
- start_wave: None,
2589
- }
2500
+ RequestBatch(
2501
+ RequestBatchOutputs {
2502
+ engine_index: 0,
2503
+ outputs: [
2504
+ EngineCoreOutput {
2505
+ request_id: "req-1",
2506
+ new_token_ids: [
2507
+ 7,
2508
+ 8,
2509
+ ],
2510
+ new_logprobs: None,
2511
+ new_prompt_logprobs_tensors: None,
2512
+ pooling_output: None,
2513
+ finish_reason: Some(
2514
+ Length,
2515
+ ),
2516
+ stop_reason: None,
2517
+ events: None,
2518
+ kv_transfer_params: None,
2519
+ trace_headers: None,
2520
+ prefill_stats: None,
2521
+ routed_experts: None,
2522
+ num_nans_in_logits: 0,
2523
+ },
2524
+ ],
2525
+ scheduler_stats: None,
2526
+ timestamp: 0.0,
2527
+ finished_requests: Some(
2528
+ {
2529
+ "req-1",
2530
+ },
2531
+ ),
2532
+ },
2533
+ )
2534
"#]]
2535
.assert_debug_eq(&decoded_outputs);
2536
@@ -2599,7 +2543,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
2543
let inline_logprobs =
2544
decode_engine_core_outputs(&decode_frames(inline_logprobs_frames)).unwrap();
2545
expect_sample_logprobs(
2602
- inline_logprobs.outputs[0]
2546
+ inline_logprobs.as_request_batch().unwrap().outputs[0]
2547
.new_logprobs
2548
.as_ref()
2549
.expect("inline logprobs decoded"),
@@ -2608,7 +2552,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
2552
let multipart_logprobs =
2553
decode_engine_core_outputs(&decode_frames(multipart_logprobs_frames)).unwrap();
2554
expect_sample_logprobs(
2611
- multipart_logprobs.outputs[0]
2555
+ multipart_logprobs.as_request_batch().unwrap().outputs[0]
2556
.new_logprobs
2557
.as_ref()
2558
.expect("multipart logprobs decoded"),
@@ -2616,7 +2560,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
2560
2561
let inline_prompt = decode_engine_core_outputs(&decode_frames(inline_prompt_frames)).unwrap();
2562
expect_prompt_logprobs(
2619
- inline_prompt.outputs[0]
2563
+ inline_prompt.as_request_batch().unwrap().outputs[0]
2564
.new_prompt_logprobs_tensors
2565
.as_ref()
2566
.expect("inline prompt logprobs decoded"),
@@ -2625,7 +2569,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
2569
let multipart_prompt =
2570
decode_engine_core_outputs(&decode_frames(multipart_prompt_frames)).unwrap();
2571
expect_prompt_logprobs(
2628
- multipart_prompt.outputs[0]
2572
+ multipart_prompt.as_request_batch().unwrap().outputs[0]
2573
.new_prompt_logprobs_tensors
2574
.as_ref()
2575
.expect("multipart prompt logprobs decoded"),
@@ -2945,8 +2889,7 @@ async fn bootstrapped_external_coordinator_updates_wave_ignores_counts_and_sends
2889
2890
send_outputs(
2891
&mut push,
2948
- EngineCoreOutputs {
2949
- engine_index: 0,
2892
+ RequestBatchOutputs {
2893
outputs: vec![request_output(
2894
"req-1",
2895
vec![],
@@ -2954,7 +2897,8 @@ async fn bootstrapped_external_coordinator_updates_wave_ignores_counts_and_sends
2897
)],
2898
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
2899
..Default::default()
2957
- },
2900
+ }
2901
+ .into(),
2902
)
2903
.await;
2904
@@ -3020,8 +2964,7 @@ async fn bootstrapped_external_coordinator_running_state_suppresses_wakeup() {
2964
2965
send_outputs(
2966
&mut push,
3023
- EngineCoreOutputs {
3024
- engine_index: 0,
2967
+ RequestBatchOutputs {
2968
outputs: vec![request_output(
2969
"req-1",
2970
vec![],
@@ -3029,7 +2972,8 @@ async fn bootstrapped_external_coordinator_running_state_suppresses_wakeup() {
2972
)],
2973
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
2974
..Default::default()
3032
- },
2975
+ }
2976
+ .into(),
2977
)
2978
.await;
2979
rust/src/llm/tests/generate.rs
+36
-30
@@ -11,7 +11,7 @@ use vllm_engine_core_client::protocol::logprobs::{
11
};
12
use vllm_engine_core_client::protocol::output::{
13
EngineCoreEvent, EngineCoreEventType, EngineCoreFinishReason, EngineCoreOutput,
14
- EngineCoreOutputs,
14
+ EngineCoreOutputs, RequestBatchOutputs,
15
};
16
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
17
use vllm_engine_core_client::protocol::sampling::EngineCoreSamplingParams;
@@ -251,7 +251,7 @@ async fn generate_streams_outputs() {
251
252
send_outputs(
253
push,
254
- EngineCoreOutputs {
254
+ RequestBatchOutputs {
255
outputs: vec![
256
request_output_with_logprobs(
257
&request.request_id,
@@ -270,7 +270,8 @@ async fn generate_streams_outputs() {
270
],
271
finished_requests: Some(BTreeSet::from([request.request_id.clone()])),
272
..Default::default()
273
- },
273
+ }
274
+ .into(),
275
)
276
.await;
277
})
@@ -331,8 +332,7 @@ async fn collect_output_aggregates_raw_tokens_logprobs_and_terminal_metadata() {
332
333
send_outputs(
334
push,
334
- EngineCoreOutputs {
335
- engine_index: 0,
335
+ RequestBatchOutputs {
336
outputs: vec![
337
EngineCoreOutput {
338
prefill_stats: Some(PrefillStats {
@@ -358,13 +358,9 @@ async fn collect_output_aggregates_raw_tokens_logprobs_and_terminal_metadata() {
358
Some(serde_json::json!({"connector": "x"})),
359
),
360
],
361
- scheduler_stats: None,
362
- timestamp: 0.0,
363
- utility_output: None,
364
- finished_requests: None,
365
- wave_complete: None,
366
- start_wave: None,
367
- },
361
+ ..Default::default()
362
+ }
363
+ .into(),
364
)
365
.await;
366
})
@@ -412,10 +408,12 @@ async fn generate_propagates_unexpected_close_errors() {
408
409
send_outputs(
410
push,
415
- EngineCoreOutputs {
411
+ RequestBatchOutputs {
412
+ outputs: Vec::new(),
413
finished_requests: Some(BTreeSet::from([request.request_id])),
414
..Default::default()
418
- },
415
+ }
416
+ .into(),
417
)
418
.await;
419
})
@@ -459,10 +457,11 @@ async fn dropping_a_live_generate_stream_triggers_abort() {
457
458
send_outputs(
459
push,
462
- EngineCoreOutputs {
460
+ RequestBatchOutputs {
461
outputs: vec![request_output(&request.request_id, vec![99], None)],
462
..Default::default()
465
- },
463
+ }
464
+ .into(),
465
)
466
.await;
467
@@ -513,7 +512,7 @@ async fn duplicate_external_request_ids_are_randomized_before_reaching_engine_co
512
513
send_outputs(
514
push,
516
- EngineCoreOutputs {
515
+ RequestBatchOutputs {
516
outputs: vec![request_output(
517
&request_1.request_id,
518
vec![],
@@ -521,13 +520,14 @@ async fn duplicate_external_request_ids_are_randomized_before_reaching_engine_co
520
)],
521
finished_requests: Some(BTreeSet::from([request_1.request_id.clone()])),
522
..Default::default()
524
- },
523
+ }
524
+ .into(),
525
)
526
.await;
527
528
send_outputs(
529
push,
530
- EngineCoreOutputs {
530
+ RequestBatchOutputs {
531
outputs: vec![request_output(
532
&request_2.request_id,
533
vec![],
@@ -535,7 +535,8 @@ async fn duplicate_external_request_ids_are_randomized_before_reaching_engine_co
535
)],
536
finished_requests: Some(BTreeSet::from([request_2.request_id])),
537
..Default::default()
538
- },
538
+ }
539
+ .into(),
540
)
541
.await;
542
})
@@ -576,10 +577,11 @@ async fn abort_resolves_external_request_id_to_internal_before_reaching_engine()
577
578
send_outputs(
579
push,
579
- EngineCoreOutputs {
580
+ RequestBatchOutputs {
581
outputs: vec![request_output(&request.request_id, vec![7], None)],
582
..Default::default()
582
- },
583
+ }
584
+ .into(),
585
)
586
.await;
587
@@ -643,13 +645,14 @@ async fn abort_by_external_id_aborts_all_internal_requests() {
645
646
send_outputs(
647
push,
646
- EngineCoreOutputs {
648
+ RequestBatchOutputs {
649
outputs: vec![
650
request_output(&request_1.request_id, vec![7], None),
651
request_output(&request_2.request_id, vec![8], None),
652
],
653
..Default::default()
652
- },
654
+ }
655
+ .into(),
656
)
657
.await;
658
@@ -710,7 +713,7 @@ async fn generate_records_request_metrics_in_prometheus_output() {
713
714
send_outputs(
715
push,
713
- EngineCoreOutputs {
716
+ RequestBatchOutputs {
717
engine_index: 4,
718
timestamp: 10.0,
719
outputs: vec![EngineCoreOutput {
@@ -736,13 +739,14 @@ async fn generate_records_request_metrics_in_prometheus_output() {
739
)
740
}],
741
..Default::default()
739
- },
742
+ }
743
+ .into(),
744
)
745
.await;
746
747
send_outputs(
748
push,
745
- EngineCoreOutputs {
749
+ RequestBatchOutputs {
750
engine_index: 4,
751
timestamp: 11.5,
752
outputs: vec![request_output_with_events(
@@ -756,7 +760,8 @@ async fn generate_records_request_metrics_in_prometheus_output() {
760
)],
761
finished_requests: Some(BTreeSet::from([request.request_id])),
762
..Default::default()
759
- },
763
+ }
764
+ .into(),
765
)
766
.await;
767
})
@@ -843,7 +848,7 @@ async fn dropping_stream_records_abort_terminal_request_metrics() {
848
849
send_outputs(
850
push,
846
- EngineCoreOutputs {
851
+ RequestBatchOutputs {
852
engine_index: 5,
853
timestamp: 10.0,
854
outputs: vec![request_output_with_events(
@@ -862,7 +867,8 @@ async fn dropping_stream_records_abort_terminal_request_metrics() {
867
]),
868
)],
869
..Default::default()
865
- },
870
+ }
871
+ .into(),
872
)
873
.await;
874
rust/src/mock-engine/src/engine.rs
+16
-12
@@ -12,7 +12,8 @@ use tokio::task::yield_now;
12
use tokio_util::sync::CancellationToken;
13
use tracing::{debug, info, warn};
14
use vllm_engine_core_client::protocol::output::{
15
- EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
15
+ EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs,
16
+ UtilityCallOutput,
17
};
18
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
19
use vllm_engine_core_client::protocol::utility::{
@@ -61,13 +62,14 @@ fn empty_finish_outputs(
62
let output = request_output(request_id, Vec::new(), Some(finish_reason));
63
let finished_requests = BTreeSet::from([output.request_id.clone()]);
64
64
- EngineCoreOutputs {
65
+ RequestBatchOutputs {
66
engine_index,
67
outputs: vec![output],
68
timestamp: now_secs(),
69
finished_requests: Some(finished_requests),
70
..Default::default()
71
}
72
+ .into()
73
}
74
75
/// Encode a utility result into the protocol's msgpack value envelope.
@@ -98,16 +100,16 @@ fn utility_response(
100
_ => utility_envelope(Value::Nil),
101
}?;
102
101
- Ok(EngineCoreOutputs {
103
+ Ok(UtilityCallOutput {
104
engine_index,
103
- utility_output: Some(UtilityOutput {
105
+ timestamp: now_secs(),
106
+ output: UtilityOutput {
107
call_id: request.call_id,
108
failure_message: None,
109
result: Some(result),
107
- }),
108
- timestamp: now_secs(),
109
- ..Default::default()
110
- })
110
+ },
111
+ }
112
+ .into())
113
}
114
115
/// Message sent from the frontend to the mock engine task to drive the engine loop.
@@ -270,13 +272,14 @@ impl Engine {
272
}
273
for (client_index, (client_outputs, finished_requests)) in outputs_by_client {
274
outputs.push({
273
- let outputs = EngineCoreOutputs {
275
+ let outputs = RequestBatchOutputs {
276
engine_index: self.engine_index,
277
outputs: client_outputs,
278
timestamp: now_secs(),
279
finished_requests: Some(finished_requests),
280
..Default::default()
279
- };
281
+ }
282
+ .into();
283
EngineOutput {
284
client_index,
285
outputs,
@@ -358,14 +361,15 @@ impl Engine {
361
.filter_map(|(client_index, (outputs, finished_requests))| {
362
(!outputs.is_empty()).then(|| EngineOutput {
363
client_index,
361
- outputs: EngineCoreOutputs {
364
+ outputs: RequestBatchOutputs {
365
engine_index: self.engine_index,
366
outputs,
367
timestamp: now_secs(),
368
finished_requests: (!finished_requests.is_empty())
369
.then_some(finished_requests),
370
..Default::default()
368
- },
371
+ }
372
+ .into(),
373
})
374
})
375
.collect()
rust/src/server/src/grpc/convert.rs
+10
-24
@@ -231,32 +231,18 @@ fn convert_structured_output(
231
StructuredOutput::Json(schema) => {
232
let json: serde_json::Value = serde_json::from_str(schema)
233
.map_err(|e| Status::invalid_argument(format!("invalid json schema: {e}")))?;
234
- StructuredOutputsParams {
235
- json: Some(json),
236
- ..Default::default()
237
- }
234
+ StructuredOutputsParams::json(json)
235
}
239
- StructuredOutput::Regex(regex) => StructuredOutputsParams {
240
- regex: Some(regex.clone()),
241
- ..Default::default()
242
- },
243
- StructuredOutput::Choice(choices) => StructuredOutputsParams {
244
- choice: Some(choices.choices.clone()),
245
- ..Default::default()
246
- },
247
- StructuredOutput::Grammar(grammar) => StructuredOutputsParams {
248
- grammar: Some(grammar.clone()),
249
- ..Default::default()
250
- },
251
- StructuredOutput::JsonObject(true) => StructuredOutputsParams {
252
- json_object: Some(true),
253
- ..Default::default()
254
- },
236
+ StructuredOutput::Regex(regex) => StructuredOutputsParams::regex(regex.clone()),
237
+ StructuredOutput::Choice(choices) => {
238
+ StructuredOutputsParams::choice(choices.choices.clone())
239
+ }
240
+ StructuredOutput::Grammar(grammar) => StructuredOutputsParams::grammar(grammar.clone()),
241
+ StructuredOutput::JsonObject(true) => StructuredOutputsParams::json_object(),
242
StructuredOutput::JsonObject(false) => return Ok(None),
256
- StructuredOutput::StructuralTag(tag) => StructuredOutputsParams {
257
- structural_tag: Some(tag.clone()),
258
- ..Default::default()
259
- },
243
+ StructuredOutput::StructuralTag(tag) => {
244
+ StructuredOutputsParams::structural_tag(tag.clone())
245
+ }
246
};
247
Ok(Some(params))
248
}
rust/src/server/src/grpc/tests.rs
+4
-9
@@ -19,7 +19,7 @@ use vllm_chat::{
19
DynChatOutputProcessor, DynChatRenderer, NewChatOutputProcessorOptions, RenderedPrompt,
20
};
21
use vllm_engine_core_client::protocol::output::{
22
- EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
22
+ EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs,
23
};
24
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
25
use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
@@ -115,19 +115,14 @@ fn engine_outputs_for_request(
115
request_id: &str,
116
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
117
) -> EngineCoreOutputs {
118
- EngineCoreOutputs {
119
- engine_index: 0,
118
+ RequestBatchOutputs {
119
outputs: output_specs
120
.into_iter()
121
.map(|(token_ids, finish_reason)| request_output(request_id, token_ids, finish_reason))
122
.collect(),
124
- scheduler_stats: None,
125
- timestamp: 0.0,
126
- utility_output: None,
127
- finished_requests: None,
128
- wave_complete: None,
129
- start_wave: None,
123
+ ..Default::default()
124
}
125
+ .into()
126
}
127
128
fn default_stream_output_specs() -> Vec<(Vec<u32>, Option<EngineCoreFinishReason>)> {
rust/src/server/src/routes/http_client_tests.rs
+4
-9
@@ -19,7 +19,7 @@ use vllm_chat::{
19
DynChatOutputProcessor, DynChatRenderer, NewChatOutputProcessorOptions, RenderedPrompt,
20
};
21
use vllm_engine_core_client::protocol::output::{
22
- EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
22
+ EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs,
23
};
24
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
25
use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
@@ -111,19 +111,14 @@ fn engine_outputs_for_request(
111
request_id: &str,
112
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
113
) -> EngineCoreOutputs {
114
- EngineCoreOutputs {
115
- engine_index: 0,
114
+ RequestBatchOutputs {
115
outputs: output_specs
116
.into_iter()
117
.map(|(token_ids, finish_reason)| request_output(request_id, token_ids, finish_reason))
118
.collect(),
120
- scheduler_stats: None,
121
- timestamp: 0.0,
122
- utility_output: None,
123
- finished_requests: None,
124
- wave_complete: None,
125
- start_wave: None,
119
+ ..Default::default()
120
}
121
+ .into()
122
}
123
124
fn default_stream_output_specs() -> Vec<(Vec<u32>, Option<EngineCoreFinishReason>)> {
rust/src/server/src/routes/openai/utils/structured_outputs.rs
+6
-13
@@ -37,7 +37,7 @@ pub enum ResponseFormat {
37
},
38
/// vLLM-specific structural tag format. The entire object (including the
39
/// `type` field) is JSON-serialized and passed as
40
- /// `StructuredOutputsParams.structural_tag`.
40
+ /// `StructuredOutputConstraint::StructuralTag`.
41
///
42
/// We capture the payload as a catch-all map so both the legacy
43
/// (`structures`/`triggers`) and current (`format`) shapes are
@@ -79,14 +79,10 @@ pub fn convert_from_response_format(
79
};
80
match fmt {
81
ResponseFormat::Text => Ok(None),
82
- ResponseFormat::JsonObject => Ok(Some(StructuredOutputsParams {
83
- json_object: Some(true),
84
- ..Default::default()
85
- })),
86
- ResponseFormat::JsonSchema { json_schema } => Ok(Some(StructuredOutputsParams {
87
- json: Some(json_schema.schema.clone()),
88
- ..Default::default()
89
- })),
82
+ ResponseFormat::JsonObject => Ok(Some(StructuredOutputsParams::json_object())),
83
+ ResponseFormat::JsonSchema { json_schema } => Ok(Some(StructuredOutputsParams::json(
84
+ json_schema.schema.clone(),
85
+ ))),
86
ResponseFormat::StructuralTag { .. } => {
87
// The Python frontend dumps the entire response_format object (including the
88
// `type` field) as a JSON string for the engine-core backend.
@@ -96,10 +92,7 @@ pub fn convert_from_response_format(
92
Some("response_format"),
93
)
94
})?;
99
- Ok(Some(StructuredOutputsParams {
100
- structural_tag: Some(tag_json),
101
- ..Default::default()
102
- }))
95
+ Ok(Some(StructuredOutputsParams::structural_tag(tag_json)))
96
}
97
}
98
}
rust/src/server/src/routes/tests.rs
+45
-91
@@ -29,7 +29,8 @@ use vllm_engine_core_client::protocol::logprobs::{
29
Logprobs, MaybeWireLogprobs, PositionLogprobs, TokenLogprob,
30
};
31
use vllm_engine_core_client::protocol::output::{
32
- EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, StopReason,
32
+ EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs, StopReason,
33
+ UtilityCallOutput,
34
};
35
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
36
use vllm_engine_core_client::protocol::utility::{UtilityOutput, UtilityResultEnvelope};
@@ -232,19 +233,14 @@ fn engine_outputs_for_request(
233
request_id: &str,
234
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
235
) -> EngineCoreOutputs {
235
- EngineCoreOutputs {
236
- engine_index: 0,
236
+ RequestBatchOutputs {
237
outputs: output_specs
238
.into_iter()
239
.map(|(token_ids, finish_reason)| request_output(request_id, token_ids, finish_reason))
240
.collect(),
241
- scheduler_stats: None,
242
- timestamp: 0.0,
243
- utility_output: None,
244
- finished_requests: None,
245
- wave_complete: None,
246
- start_wave: None,
241
+ ..Default::default()
242
}
243
+ .into()
244
}
245
246
fn test_llm(client: EngineCoreClient) -> Llm {
@@ -390,14 +386,15 @@ fn utility_none_result() -> UtilityResultEnvelope {
386
}
387
388
fn utility_outputs(call_id: u64, result: UtilityResultEnvelope) -> EngineCoreOutputs {
393
- EngineCoreOutputs {
394
- utility_output: Some(UtilityOutput {
389
+ UtilityCallOutput {
390
+ output: UtilityOutput {
391
call_id: call_id.into(),
392
failure_message: None,
393
result: Some(result),
398
- }),
394
+ },
395
..Default::default()
396
}
397
+ .into()
398
}
399
400
async fn send_outputs(push: &mut PushSocket, outputs: EngineCoreOutputs) {
@@ -2281,8 +2278,7 @@ async fn non_stream_chat_includes_logprobs_and_prompt_logprobs() {
2278
2279
send_outputs(
2280
push,
2284
- EngineCoreOutputs {
2285
- engine_index: 0,
2281
+ RequestBatchOutputs {
2282
outputs: vec![request_output_with_logprobs(
2283
&request.request_id,
2284
bytes_to_token_ids(b"hi"),
@@ -2291,13 +2287,9 @@ async fn non_stream_chat_includes_logprobs_and_prompt_logprobs() {
2287
Some(sample_logprobs_for_tokens(&bytes_to_token_ids(b"hi"))),
2288
Some(prompt_logprobs_for_tokens(&prompt_token_ids)),
2289
)],
2294
- scheduler_stats: None,
2295
- timestamp: 0.0,
2296
- utility_output: None,
2297
- finished_requests: None,
2298
- wave_complete: None,
2299
- start_wave: None,
2300
- },
2290
+ ..Default::default()
2291
+ }
2292
+ .into(),
2293
)
2294
.await;
2295
})
@@ -3121,8 +3113,7 @@ async fn non_stream_completions_include_logprobs() {
3113
rmp_serde::from_slice(&add[1]).expect("decode request");
3114
send_outputs(
3115
push,
3124
- EngineCoreOutputs {
3125
- engine_index: 0,
3116
+ RequestBatchOutputs {
3117
outputs: vec![
3118
request_output_with_logprobs(
3119
&request.request_id,
@@ -3141,13 +3132,10 @@ async fn non_stream_completions_include_logprobs() {
3132
None,
3133
),
3134
],
3144
- scheduler_stats: None,
3145
- timestamp: 0.0,
3146
- utility_output: None,
3135
finished_requests: Some(BTreeSet::from([request.request_id.clone()])),
3148
- wave_complete: None,
3149
- start_wave: None,
3150
- },
3136
+ ..Default::default()
3137
+ }
3138
+ .into(),
3139
)
3140
.await;
3141
})
@@ -3224,8 +3212,7 @@ async fn non_stream_completions_include_prompt_logprobs() {
3212
rmp_serde::from_slice(&add[1]).expect("decode request");
3213
send_outputs(
3214
push,
3227
- EngineCoreOutputs {
3228
- engine_index: 0,
3215
+ RequestBatchOutputs {
3216
outputs: vec![request_output_with_logprobs(
3217
&request.request_id,
3218
vec![b'h' as u32, b'i' as u32, b'!' as u32],
@@ -3246,13 +3233,9 @@ async fn non_stream_completions_include_prompt_logprobs() {
3233
}),
3234
Some(prompt_logprobs_for_hello()),
3235
)],
3249
- scheduler_stats: None,
3250
- timestamp: 0.0,
3251
- utility_output: None,
3252
- finished_requests: None,
3253
- wave_complete: None,
3254
- start_wave: None,
3255
- },
3236
+ ..Default::default()
3237
+ }
3238
+ .into(),
3239
)
3240
.await;
3241
})
@@ -3588,8 +3571,7 @@ async fn non_stream_raw_generate_returns_token_output_envelope() {
3571
3572
send_outputs(
3573
push,
3591
- EngineCoreOutputs {
3592
- engine_index: 0,
3574
+ RequestBatchOutputs {
3575
outputs: vec![
3576
request_output_with_logprobs(
3577
&request.request_id,
@@ -3609,13 +3591,9 @@ async fn non_stream_raw_generate_returns_token_output_envelope() {
3591
Some(json!({"connector": "x"})),
3592
),
3593
],
3612
- scheduler_stats: None,
3613
- timestamp: 0.0,
3614
- utility_output: None,
3615
- finished_requests: None,
3616
- wave_complete: None,
3617
- start_wave: None,
3618
- },
3594
+ ..Default::default()
3595
+ }
3596
+ .into(),
3597
)
3598
.await;
3599
})
@@ -3708,8 +3686,7 @@ async fn stream_raw_generate_returns_sse_chunks_and_usage() {
3686
3687
send_outputs(
3688
push,
3711
- EngineCoreOutputs {
3712
- engine_index: 0,
3689
+ RequestBatchOutputs {
3690
outputs: vec![
3691
request_output_with_logprobs(
3692
&request.request_id,
@@ -3728,13 +3705,9 @@ async fn stream_raw_generate_returns_sse_chunks_and_usage() {
3705
None,
3706
),
3707
],
3731
- scheduler_stats: None,
3732
- timestamp: 0.0,
3733
- utility_output: None,
3734
- finished_requests: None,
3735
- wave_complete: None,
3736
- start_wave: None,
3737
- },
3708
+ ..Default::default()
3709
+ }
3710
+ .into(),
3711
)
3712
.await;
3713
})
@@ -4426,8 +4399,7 @@ async fn include_reasoning_false_suppresses_non_stream_output_metadata() {
4399
4400
send_outputs(
4401
push,
4429
- EngineCoreOutputs {
4430
- engine_index: 0,
4402
+ RequestBatchOutputs {
4403
outputs: vec![
4404
request_output_with_logprobs(
4405
&request.request_id,
@@ -4446,13 +4418,9 @@ async fn include_reasoning_false_suppresses_non_stream_output_metadata() {
4418
None,
4419
),
4420
],
4449
- scheduler_stats: None,
4450
- timestamp: 0.0,
4451
- utility_output: None,
4452
- finished_requests: None,
4453
- wave_complete: None,
4454
- start_wave: None,
4455
- },
4421
+ ..Default::default()
4422
+ }
4423
+ .into(),
4424
)
4425
.await;
4426
})
@@ -4601,8 +4569,7 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
4569
4570
send_outputs(
4571
push,
4604
- EngineCoreOutputs {
4605
- engine_index: 0,
4572
+ RequestBatchOutputs {
4573
outputs: vec![request_output_with_logprobs(
4574
&request.request_id,
4575
bytes_to_token_ids(b"<think>Need tool.</think>"),
@@ -4613,19 +4580,14 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
4580
))),
4581
None,
4582
)],
4616
- scheduler_stats: None,
4617
- timestamp: 0.0,
4618
- utility_output: None,
4619
- finished_requests: None,
4620
- wave_complete: None,
4621
- start_wave: None,
4622
- },
4583
+ ..Default::default()
4584
+ }
4585
+ .into(),
4586
)
4587
.await;
4588
send_outputs(
4589
push,
4627
- EngineCoreOutputs {
4628
- engine_index: 0,
4590
+ RequestBatchOutputs {
4591
outputs: vec![request_output_with_logprobs(
4592
&request.request_id,
4593
bytes_to_token_ids(b"<tool_call>\n{\"name\":\"get_weather\", "),
@@ -4636,19 +4598,14 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
4598
))),
4599
None,
4600
)],
4639
- scheduler_stats: None,
4640
- timestamp: 0.0,
4641
- utility_output: None,
4642
- finished_requests: None,
4643
- wave_complete: None,
4644
- start_wave: None,
4645
- },
4601
+ ..Default::default()
4602
+ }
4603
+ .into(),
4604
)
4605
.await;
4606
send_outputs(
4607
push,
4650
- EngineCoreOutputs {
4651
- engine_index: 0,
4608
+ RequestBatchOutputs {
4609
outputs: vec![request_output_with_logprobs(
4610
&request.request_id,
4611
bytes_to_token_ids(
@@ -4661,13 +4618,10 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
4618
))),
4619
None,
4620
)],
4664
- scheduler_stats: None,
4665
- timestamp: 0.0,
4666
- utility_output: None,
4621
finished_requests: Some(BTreeSet::from([request.request_id.clone()])),
4668
- wave_complete: None,
4669
- start_wave: None,
4670
- },
4622
+ ..Default::default()
4623
+ }
4624
+ .into(),
4625
)
4626
.await;
4627
})