[Rust Frontend] Use enum-backed domain types for engine outputs and structured outputs (#47283)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
This commit is contained in:
@@ -47,9 +47,8 @@ pub(super) fn apply_structural_tag_constraint(
|
||||
|
||||
// Overwrite any existing structured output settings with the structural tag constraint.
|
||||
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
|
||||
structural_tag: Some(structural_tag),
|
||||
backend: StructuredOutputBackend::Xgrammar,
|
||||
..Default::default()
|
||||
..StructuredOutputsParams::structural_tag(structural_tag)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -119,10 +118,11 @@ mod tests {
|
||||
.as_ref()
|
||||
.expect("structured outputs should be set");
|
||||
assert_eq!(params.backend, StructuredOutputBackend::Xgrammar);
|
||||
serde_json::from_str(
|
||||
params.structural_tag.as_deref().expect("structural_tag should be set"),
|
||||
)
|
||||
.expect("structural_tag should be valid JSON")
|
||||
let structural_tag = params
|
||||
.constraint
|
||||
.as_structural_tag()
|
||||
.expect("structured output constraint should be structural_tag");
|
||||
serde_json::from_str(structural_tag).expect("structural_tag should be valid JSON")
|
||||
}
|
||||
|
||||
fn structured_outputs(request: &ChatRequest) -> &StructuredOutputsParams {
|
||||
@@ -161,9 +161,8 @@ mod tests {
|
||||
fn auto_strict_tool_choice_overwrites_existing_json_guidance() {
|
||||
let mut request = request(ChatToolChoice::Auto, vec![chat_tool("search", Some(true))]);
|
||||
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
|
||||
json: Some(json!({"type": "object"})),
|
||||
backend: StructuredOutputBackend::Xgrammar,
|
||||
..Default::default()
|
||||
..StructuredOutputsParams::json(json!({"type": "object"}))
|
||||
});
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
@@ -171,8 +170,7 @@ mod tests {
|
||||
.expect("structural tag should build");
|
||||
|
||||
let params = structured_outputs(&request);
|
||||
assert!(params.json.is_none());
|
||||
assert!(params.structural_tag.is_some());
|
||||
assert!(params.constraint.is_structural_tag());
|
||||
let tag = structural_tag_value(&request);
|
||||
assert_eq!(tag["type"], "structural_tag");
|
||||
assert!(tag.to_string().contains("search"));
|
||||
@@ -195,9 +193,8 @@ mod tests {
|
||||
fn required_tool_choice_overwrites_existing_json_object_guidance() {
|
||||
let mut request = request(ChatToolChoice::Required, vec![chat_tool("search", None)]);
|
||||
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
|
||||
json_object: Some(true),
|
||||
backend: StructuredOutputBackend::Xgrammar,
|
||||
..Default::default()
|
||||
..StructuredOutputsParams::json_object()
|
||||
});
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
@@ -205,8 +202,7 @@ mod tests {
|
||||
.expect("structural tag should build");
|
||||
|
||||
let params = structured_outputs(&request);
|
||||
assert!(params.json_object.is_none());
|
||||
assert!(params.structural_tag.is_some());
|
||||
assert!(params.constraint.is_structural_tag());
|
||||
let tag = structural_tag_value(&request);
|
||||
assert_eq!(tag["type"], "structural_tag");
|
||||
assert!(tag.to_string().contains("search"));
|
||||
@@ -245,9 +241,8 @@ mod tests {
|
||||
fn none_tool_choice_preserves_existing_json_object_guidance() {
|
||||
let mut request = request(ChatToolChoice::None, vec![chat_tool("search", Some(true))]);
|
||||
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
|
||||
json_object: Some(true),
|
||||
backend: StructuredOutputBackend::Xgrammar,
|
||||
..Default::default()
|
||||
..StructuredOutputsParams::json_object()
|
||||
});
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
@@ -255,7 +250,6 @@ mod tests {
|
||||
.expect("structural tag decision should succeed");
|
||||
|
||||
let params = structured_outputs(&request);
|
||||
assert_eq!(params.json_object, Some(true));
|
||||
assert!(params.structural_tag.is_none());
|
||||
assert!(params.constraint.is_json_object());
|
||||
}
|
||||
}
|
||||
|
||||
+34
-23
@@ -16,7 +16,7 @@ use vllm_engine_core_client::protocol::logprobs::{
|
||||
Logprobs, MaybeWireLogprobs, PositionLogprobs, TokenLogprob,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::output::{
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, StopReason,
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs, StopReason,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
|
||||
use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
|
||||
@@ -342,7 +342,7 @@ async fn chat_streams_text_events() {
|
||||
);
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output("chat-1", vec![b'H' as u32], None, None),
|
||||
request_output(
|
||||
@@ -354,7 +354,8 @@ async fn chat_streams_text_events() {
|
||||
],
|
||||
finished_requests: Some(BTreeSet::from(["chat-1".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -454,7 +455,7 @@ async fn chat_stream_waits_for_complete_utf8_before_emitting() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output("chat-utf8", bytes_to_token_ids(&[0xe4]), None, None),
|
||||
request_output(
|
||||
@@ -466,7 +467,8 @@ async fn chat_stream_waits_for_complete_utf8_before_emitting() {
|
||||
],
|
||||
finished_requests: Some(BTreeSet::from(["chat-utf8".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -542,7 +544,7 @@ async fn chat_stream_flushes_held_text_on_finish() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"chat-final-flush",
|
||||
bytes_to_token_ids(b"ok st"),
|
||||
@@ -551,7 +553,8 @@ async fn chat_stream_flushes_held_text_on_finish() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["chat-final-flush".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -662,7 +665,7 @@ async fn chat_stream_reports_decode_failure_as_error_event() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"chat-4",
|
||||
vec![UNKNOWN_DECODE_TOKEN_ID],
|
||||
@@ -670,7 +673,8 @@ async fn chat_stream_reports_decode_failure_as_error_event() {
|
||||
None,
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -725,7 +729,7 @@ async fn chat_stream_preserves_terminal_stop_token_when_requested() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"chat-include-stop",
|
||||
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() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["chat-include-stop".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -812,7 +817,7 @@ async fn chat_stream_separates_reasoning_blocks_automatically() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output(
|
||||
"chat-reasoning",
|
||||
@@ -841,7 +846,8 @@ async fn chat_stream_separates_reasoning_blocks_automatically() {
|
||||
],
|
||||
finished_requests: Some(BTreeSet::from(["chat-reasoning".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -955,7 +961,7 @@ async fn chat_collectors_return_structured_message_and_visible_text() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"chat-collect",
|
||||
bytes_to_token_ids(b"<think>inner</think>outer"),
|
||||
@@ -964,7 +970,8 @@ async fn chat_collectors_return_structured_message_and_visible_text() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["chat-collect".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1018,7 +1025,7 @@ async fn chat_explicitly_disables_reasoning_parser() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output(
|
||||
"chat-reasoning-disabled",
|
||||
@@ -1049,7 +1056,8 @@ async fn chat_explicitly_disables_reasoning_parser() {
|
||||
"chat-reasoning-disabled".to_string()
|
||||
])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1096,7 +1104,7 @@ async fn chat_stream_parses_tool_calls_automatically() {
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output(
|
||||
"chat-tool",
|
||||
@@ -1121,7 +1129,8 @@ async fn chat_stream_parses_tool_calls_automatically() {
|
||||
],
|
||||
finished_requests: Some(BTreeSet::from(["chat-tool".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1204,7 +1213,7 @@ async fn chat_collect_message_preserves_tool_call_arguments_in_final_only_mode()
|
||||
let _ = recv_engine_message(dealer).await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output(
|
||||
"chat-final-only-tool",
|
||||
@@ -1231,7 +1240,8 @@ async fn chat_collect_message_preserves_tool_call_arguments_in_final_only_mode()
|
||||
"chat-final-only-tool".to_string()
|
||||
])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1282,7 +1292,7 @@ async fn chat_stream_and_collect_preserve_prompt_and_sample_logprobs() {
|
||||
let request: EngineCoreRequest = rmp_serde::from_slice(&add[1]).unwrap();
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
@@ -1303,7 +1313,8 @@ async fn chat_stream_and_collect_preserve_prompt_and_sample_logprobs() {
|
||||
],
|
||||
finished_requests: Some(BTreeSet::from([request.request_id])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::client::{AbortCause, AbortRequest};
|
||||
use crate::error::{client_closed, dispatcher_closed, unexpected_dispatcher_output};
|
||||
use crate::metrics::{LoraInfoExporter, record_scheduler_stats};
|
||||
use crate::protocol::encode_msgpack;
|
||||
use crate::protocol::output::{ClassifiedEngineCoreOutputs, EngineCoreOutput, EngineCoreOutputs};
|
||||
use crate::protocol::output::{EngineCoreOutput, EngineCoreOutputs};
|
||||
use crate::protocol::request::EngineCoreRequestType;
|
||||
use crate::protocol::stats::SchedulerStats;
|
||||
use crate::protocol::utility::UtilityOutput;
|
||||
@@ -351,8 +351,8 @@ pub(crate) async fn run_output_dispatcher_loop(
|
||||
)),
|
||||
}?;
|
||||
|
||||
match outputs.classify() {
|
||||
ClassifiedEngineCoreOutputs::RequestBatch(batch) => {
|
||||
match outputs {
|
||||
EngineCoreOutputs::RequestBatch(batch) => {
|
||||
let senders = inner.take_senders_for_outputs(&batch.outputs);
|
||||
for (output, sender) in batch.outputs.into_iter().zip(senders) {
|
||||
let request_id = output.request_id.clone();
|
||||
@@ -403,7 +403,7 @@ pub(crate) async fn run_output_dispatcher_loop(
|
||||
let (running, waiting) = inner.lora_adapter_states();
|
||||
lora_info.update(&METRICS.scheduler, running, waiting);
|
||||
}
|
||||
ClassifiedEngineCoreOutputs::Utility(utility) => {
|
||||
EngineCoreOutputs::Utility(utility) => {
|
||||
let call_id = utility.output.call_id;
|
||||
if inner.resolve_utility_output(utility.output) {
|
||||
trace!(
|
||||
@@ -419,8 +419,7 @@ pub(crate) async fn run_output_dispatcher_loop(
|
||||
);
|
||||
}
|
||||
}
|
||||
other @ (ClassifiedEngineCoreOutputs::DpControl { .. }
|
||||
| ClassifiedEngineCoreOutputs::Other(_)) => {
|
||||
other => {
|
||||
Err::<(), _>(unexpected_dispatcher_output!(
|
||||
"received unexpected output on main dispatcher path: {other:?}"
|
||||
))?;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::client::imp::ClientInner;
|
||||
use crate::coordinator::handle::{CoordinatorCommand, CoordinatorState};
|
||||
use crate::error::{Error, Result, bail_unexpected_coordinator_output};
|
||||
use crate::protocol::encode_msgpack;
|
||||
use crate::protocol::output::{ClassifiedEngineCoreOutputs, DpControlMessage, EngineCoreOutputs};
|
||||
use crate::protocol::output::{DpControlMessage, DpControlOutput, EngineCoreOutputs};
|
||||
use crate::protocol::request::EngineCoreRequestType;
|
||||
|
||||
/// Coordinator-to-engine `START_DP_WAVE` control payload encoded on the
|
||||
@@ -109,19 +109,19 @@ impl InProcCoordinatorRunner {
|
||||
/// Apply one engine-originated control output to the coordinator state
|
||||
/// machine.
|
||||
async fn handle_outputs(&mut self, outputs: EngineCoreOutputs) -> Result<()> {
|
||||
match outputs.classify() {
|
||||
ClassifiedEngineCoreOutputs::RequestBatch(batch)
|
||||
match outputs {
|
||||
EngineCoreOutputs::RequestBatch(batch)
|
||||
if batch.outputs.is_empty() && batch.finished_requests.is_none() =>
|
||||
{
|
||||
// Stats-only output for coordinator.
|
||||
// Ignore since the Rust coordinator doesn't track stats for
|
||||
// routing decisions.
|
||||
}
|
||||
ClassifiedEngineCoreOutputs::DpControl {
|
||||
EngineCoreOutputs::DpControl(DpControlOutput {
|
||||
engine_index,
|
||||
control,
|
||||
..
|
||||
} => match control {
|
||||
}) => match control {
|
||||
// The engines signals they completed the current wave and are now paused.
|
||||
// Advance the current wave and mark the state as paused.
|
||||
DpControlMessage::WaveComplete(wave) => {
|
||||
|
||||
@@ -25,6 +25,8 @@ pub enum Error {
|
||||
ValueDecode(#[from] rmpv::decode::Error),
|
||||
#[error("messagepack ext value decode failed: {message}")]
|
||||
ExtValueDecode { message: String },
|
||||
#[error("invalid structured outputs params: {message}")]
|
||||
InvalidStructuredOutputsParams { message: String },
|
||||
#[error("io error")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("transport error")]
|
||||
|
||||
@@ -183,7 +183,7 @@ fn decodes_inline_new_logprobs() {
|
||||
Some(inline_logprobs_value()),
|
||||
None,
|
||||
)))];
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap();
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
|
||||
|
||||
let logprobs = decoded.outputs[0].new_logprobs.clone().unwrap().into_direct().unwrap();
|
||||
assert_eq!(logprobs, expected_sample_logprobs());
|
||||
@@ -214,7 +214,7 @@ fn decodes_multipart_new_logprobs() {
|
||||
]),
|
||||
Bytes::from_static(&[1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]),
|
||||
];
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap();
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
|
||||
|
||||
let logprobs = decoded.outputs[0].new_logprobs.clone().unwrap().into_direct().unwrap();
|
||||
assert_eq!(logprobs, expected_sample_logprobs());
|
||||
@@ -226,7 +226,7 @@ fn decodes_inline_prompt_logprobs() {
|
||||
None,
|
||||
Some(inline_prompt_logprobs_value()),
|
||||
)))];
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap();
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
|
||||
|
||||
let logprobs = decoded.outputs[0]
|
||||
.new_prompt_logprobs_tensors
|
||||
@@ -252,7 +252,7 @@ fn decodes_big_endian_payloads() {
|
||||
])),
|
||||
None,
|
||||
)))];
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap();
|
||||
let decoded = decode_engine_core_outputs(&frames).unwrap().into_request_batch().unwrap();
|
||||
let logprobs = decoded.outputs[0].new_logprobs.clone().unwrap().into_direct().unwrap();
|
||||
assert_eq!(
|
||||
logprobs,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use enum_as_inner::EnumAsInner;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_default::DefaultFromSerde;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
|
||||
@@ -131,33 +131,33 @@ impl EngineCoreOutput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch of engine-core outputs returned to a frontend client.
|
||||
/// Raw Python/msgpack engine-core output envelope.
|
||||
///
|
||||
/// Original Python definition:
|
||||
/// <https://github.com/vllm-project/vllm/blob/f22d6e026798a74e6542a52ef776c054f2de572a/vllm/v1/engine/__init__.py#L186-L214>
|
||||
#[derive(Debug, Clone, PartialEq, Serialize_tuple, Deserialize_tuple, DefaultFromSerde)]
|
||||
pub struct EngineCoreOutputs {
|
||||
struct WireEngineCoreOutputs {
|
||||
#[serde(default)]
|
||||
pub engine_index: u32,
|
||||
engine_index: u32,
|
||||
/// Outputs grouped for this client in the current engine tick.
|
||||
#[serde(default)]
|
||||
pub outputs: Vec<EngineCoreOutput>,
|
||||
outputs: Vec<EngineCoreOutput>,
|
||||
#[serde(default)]
|
||||
pub scheduler_stats: Option<Box<SchedulerStats>>,
|
||||
scheduler_stats: Option<Box<SchedulerStats>>,
|
||||
#[serde(default)]
|
||||
pub timestamp: f64,
|
||||
timestamp: f64,
|
||||
#[serde(default)]
|
||||
pub utility_output: Option<UtilityOutput>,
|
||||
utility_output: Option<UtilityOutput>,
|
||||
#[serde(default)]
|
||||
pub finished_requests: Option<BTreeSet<String>>,
|
||||
finished_requests: Option<BTreeSet<String>>,
|
||||
/// In DP mode, signals that the current wave finished and engines are
|
||||
/// paused.
|
||||
#[serde(default)]
|
||||
pub wave_complete: Option<u32>,
|
||||
wave_complete: Option<u32>,
|
||||
/// In DP mode, signals that a request arrived for an old wave and the next
|
||||
/// wave needs to start in other engines.
|
||||
#[serde(default)]
|
||||
pub start_wave: Option<u32>,
|
||||
start_wave: Option<u32>,
|
||||
}
|
||||
|
||||
/// Data-parallel control notifications multiplexed through `EngineCoreOutputs`.
|
||||
@@ -167,7 +167,7 @@ pub enum DpControlMessage {
|
||||
StartWave(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct RequestBatchOutputs {
|
||||
pub engine_index: u32,
|
||||
pub outputs: Vec<EngineCoreOutput>,
|
||||
@@ -176,30 +176,48 @@ pub struct RequestBatchOutputs {
|
||||
pub finished_requests: Option<BTreeSet<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct UtilityCallOutput {
|
||||
pub engine_index: u32,
|
||||
pub timestamp: f64,
|
||||
pub output: UtilityOutput,
|
||||
}
|
||||
|
||||
/// Semantic classification of a raw `EngineCoreOutputs` message.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DpControlOutput {
|
||||
pub engine_index: u32,
|
||||
pub timestamp: f64,
|
||||
pub control: DpControlMessage,
|
||||
}
|
||||
|
||||
/// Semantic engine-core output families.
|
||||
///
|
||||
/// Python currently uses one product-shaped wire struct for several distinct
|
||||
/// output families. This enum exposes those families more explicitly without
|
||||
/// changing the wire format.
|
||||
/// Python currently uses one product-shaped wire struct. The Rust protocol
|
||||
/// exposes the finite semantic families while preserving the same msgpack shape
|
||||
/// for serialization.
|
||||
#[derive(Debug, Clone, PartialEq, EnumAsInner)]
|
||||
pub enum ClassifiedEngineCoreOutputs {
|
||||
pub enum EngineCoreOutputs {
|
||||
RequestBatch(RequestBatchOutputs),
|
||||
Utility(UtilityCallOutput),
|
||||
DpControl {
|
||||
engine_index: u32,
|
||||
timestamp: f64,
|
||||
control: DpControlMessage,
|
||||
},
|
||||
/// Fallback for wire-shape combinations that do not map cleanly onto the
|
||||
/// current semantic families.
|
||||
Other(EngineCoreOutputs),
|
||||
DpControl(DpControlOutput),
|
||||
}
|
||||
|
||||
impl From<RequestBatchOutputs> for EngineCoreOutputs {
|
||||
fn from(outputs: RequestBatchOutputs) -> Self {
|
||||
Self::RequestBatch(outputs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UtilityCallOutput> for EngineCoreOutputs {
|
||||
fn from(output: UtilityCallOutput) -> Self {
|
||||
Self::Utility(output)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DpControlOutput> for EngineCoreOutputs {
|
||||
fn from(output: DpControlOutput) -> Self {
|
||||
Self::DpControl(output)
|
||||
}
|
||||
}
|
||||
|
||||
impl EngineCoreOutputs {
|
||||
@@ -209,55 +227,119 @@ impl EngineCoreOutputs {
|
||||
where
|
||||
Frame: AsRef<[u8]>,
|
||||
{
|
||||
for output in &mut self.outputs {
|
||||
output.resolve_in_place(frames)?;
|
||||
if let Self::RequestBatch(batch) = self {
|
||||
for output in &mut batch.outputs {
|
||||
output.resolve_in_place(frames)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify the raw wire message into a more semantic Rust enum.
|
||||
pub fn classify(self) -> ClassifiedEngineCoreOutputs {
|
||||
let has_request_payload = !self.outputs.is_empty()
|
||||
|| self.scheduler_stats.is_some()
|
||||
|| self.finished_requests.is_some();
|
||||
/// Classify the raw wire message into a more semantic Rust enum.
|
||||
impl TryFrom<WireEngineCoreOutputs> for EngineCoreOutputs {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: WireEngineCoreOutputs) -> Result<Self> {
|
||||
let has_request_payload = !value.outputs.is_empty()
|
||||
|| value.scheduler_stats.is_some()
|
||||
|| value.finished_requests.is_some();
|
||||
|
||||
match (
|
||||
has_request_payload,
|
||||
&self.utility_output,
|
||||
&self.wave_complete,
|
||||
&self.start_wave,
|
||||
&value.utility_output,
|
||||
&value.wave_complete,
|
||||
&value.start_wave,
|
||||
) {
|
||||
(true, None, None, None) => {
|
||||
ClassifiedEngineCoreOutputs::RequestBatch(RequestBatchOutputs {
|
||||
engine_index: self.engine_index,
|
||||
outputs: self.outputs,
|
||||
scheduler_stats: self.scheduler_stats,
|
||||
timestamp: self.timestamp,
|
||||
finished_requests: self.finished_requests,
|
||||
})
|
||||
(true, None, None, None) => Ok(RequestBatchOutputs {
|
||||
engine_index: value.engine_index,
|
||||
outputs: value.outputs,
|
||||
scheduler_stats: value.scheduler_stats,
|
||||
timestamp: value.timestamp,
|
||||
finished_requests: value.finished_requests,
|
||||
}
|
||||
(false, Some(_), None, None) => {
|
||||
ClassifiedEngineCoreOutputs::Utility(UtilityCallOutput {
|
||||
engine_index: self.engine_index,
|
||||
timestamp: self.timestamp,
|
||||
output: self.utility_output.unwrap(),
|
||||
})
|
||||
.into()),
|
||||
(false, Some(_), None, None) => Ok(UtilityCallOutput {
|
||||
engine_index: value.engine_index,
|
||||
timestamp: value.timestamp,
|
||||
output: value.utility_output.unwrap(),
|
||||
}
|
||||
(false, None, Some(_), None) => ClassifiedEngineCoreOutputs::DpControl {
|
||||
engine_index: self.engine_index,
|
||||
timestamp: self.timestamp,
|
||||
control: DpControlMessage::WaveComplete(self.wave_complete.unwrap()),
|
||||
},
|
||||
(false, None, None, Some(_)) => ClassifiedEngineCoreOutputs::DpControl {
|
||||
engine_index: self.engine_index,
|
||||
timestamp: self.timestamp,
|
||||
control: DpControlMessage::StartWave(self.start_wave.unwrap()),
|
||||
},
|
||||
_ => ClassifiedEngineCoreOutputs::Other(self),
|
||||
.into()),
|
||||
(false, None, Some(_), None) => Ok(DpControlOutput {
|
||||
engine_index: value.engine_index,
|
||||
timestamp: value.timestamp,
|
||||
control: DpControlMessage::WaveComplete(value.wave_complete.unwrap()),
|
||||
}
|
||||
.into()),
|
||||
(false, None, None, Some(_)) => Ok(DpControlOutput {
|
||||
engine_index: value.engine_index,
|
||||
timestamp: value.timestamp,
|
||||
control: DpControlMessage::StartWave(value.start_wave.unwrap()),
|
||||
}
|
||||
.into()),
|
||||
|
||||
_ => Err(Error::Decode {
|
||||
target_type: "EngineCoreOutputs",
|
||||
message: "invalid wire shape".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EngineCoreOutputs> for WireEngineCoreOutputs {
|
||||
fn from(value: EngineCoreOutputs) -> Self {
|
||||
match value {
|
||||
EngineCoreOutputs::RequestBatch(batch) => Self {
|
||||
engine_index: batch.engine_index,
|
||||
outputs: batch.outputs,
|
||||
scheduler_stats: batch.scheduler_stats,
|
||||
timestamp: batch.timestamp,
|
||||
finished_requests: batch.finished_requests,
|
||||
..Default::default()
|
||||
},
|
||||
EngineCoreOutputs::Utility(utility) => Self {
|
||||
engine_index: utility.engine_index,
|
||||
timestamp: utility.timestamp,
|
||||
utility_output: Some(utility.output),
|
||||
..Default::default()
|
||||
},
|
||||
EngineCoreOutputs::DpControl(control) => {
|
||||
let (wave_complete, start_wave) = match control.control {
|
||||
DpControlMessage::WaveComplete(wave) => (Some(wave), None),
|
||||
DpControlMessage::StartWave(wave) => (None, Some(wave)),
|
||||
};
|
||||
Self {
|
||||
engine_index: control.engine_index,
|
||||
timestamp: control.timestamp,
|
||||
wave_complete,
|
||||
start_wave,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for EngineCoreOutputs {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
WireEngineCoreOutputs::from(self.clone()).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for EngineCoreOutputs {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
WireEngineCoreOutputs::deserialize(deserializer)?
|
||||
.try_into()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode one ordinary or multipart engine-core output message into the strong
|
||||
/// typed public protocol shape.
|
||||
pub fn decode_engine_core_outputs<Frame>(frames: &[Frame]) -> Result<EngineCoreOutputs>
|
||||
@@ -281,7 +363,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn engine_core_outputs_roundtrip_finished_fields() {
|
||||
let outputs = EngineCoreOutputs {
|
||||
let outputs = WireEngineCoreOutputs {
|
||||
outputs: vec![EngineCoreOutput {
|
||||
request_id: "req-1".to_string(),
|
||||
new_token_ids: vec![42],
|
||||
@@ -302,7 +384,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let encoded = encode_msgpack(&outputs).unwrap();
|
||||
let decoded: EngineCoreOutputs = decode_msgpack(&encoded).unwrap();
|
||||
let decoded: WireEngineCoreOutputs = decode_msgpack(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.outputs.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -317,7 +399,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn engine_core_outputs_classify_request_batch() {
|
||||
let outputs = EngineCoreOutputs {
|
||||
let outputs = WireEngineCoreOutputs {
|
||||
outputs: vec![EngineCoreOutput {
|
||||
request_id: "req-1".to_string(),
|
||||
new_token_ids: vec![7],
|
||||
@@ -360,12 +442,12 @@ mod tests {
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&outputs.classify());
|
||||
.assert_debug_eq(&EngineCoreOutputs::try_from(outputs).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_core_outputs_classify_utility() {
|
||||
let outputs = EngineCoreOutputs {
|
||||
let outputs = WireEngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
call_id: 42_u64.into(),
|
||||
failure_message: None,
|
||||
@@ -387,31 +469,33 @@ mod tests {
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&outputs.classify());
|
||||
.assert_debug_eq(&EngineCoreOutputs::try_from(outputs).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_core_outputs_classify_control() {
|
||||
let outputs = EngineCoreOutputs {
|
||||
let outputs = WireEngineCoreOutputs {
|
||||
start_wave: Some(3),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
expect_test::expect![[r#"
|
||||
DpControl {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
control: StartWave(
|
||||
3,
|
||||
),
|
||||
}
|
||||
DpControl(
|
||||
DpControlOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
control: StartWave(
|
||||
3,
|
||||
),
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&outputs.classify());
|
||||
.assert_debug_eq(&EngineCoreOutputs::try_from(outputs).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_core_outputs_classify_mixed_shape_as_raw() {
|
||||
let outputs = EngineCoreOutputs {
|
||||
fn engine_core_outputs_rejects_mixed_shape() {
|
||||
let outputs = WireEngineCoreOutputs {
|
||||
outputs: vec![EngineCoreOutput {
|
||||
request_id: "req-1".to_string(),
|
||||
new_token_ids: vec![7],
|
||||
@@ -425,44 +509,10 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
expect_test::expect![[r#"
|
||||
Other(
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
outputs: [
|
||||
EngineCoreOutput {
|
||||
request_id: "req-1",
|
||||
new_token_ids: [
|
||||
7,
|
||||
],
|
||||
new_logprobs: None,
|
||||
new_prompt_logprobs_tensors: None,
|
||||
pooling_output: None,
|
||||
finish_reason: None,
|
||||
stop_reason: None,
|
||||
events: None,
|
||||
kv_transfer_params: None,
|
||||
trace_headers: None,
|
||||
prefill_stats: None,
|
||||
routed_experts: None,
|
||||
num_nans_in_logits: 0,
|
||||
},
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: Some(
|
||||
UtilityOutput {
|
||||
call_id: 1,
|
||||
failure_message: None,
|
||||
result: None,
|
||||
},
|
||||
),
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&outputs.classify());
|
||||
let error = EngineCoreOutputs::try_from(outputs).unwrap_err();
|
||||
expect_test::expect![[
|
||||
r#"messagepack decode failed for EngineCoreOutputs: invalid wire shape"#
|
||||
]]
|
||||
.assert_eq(&error.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use enum_as_inner::EnumAsInner;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Structured-output backend selected for EngineCore grammar compilation.
|
||||
///
|
||||
@@ -15,50 +19,219 @@ pub enum StructuredOutputBackend {
|
||||
LmFormatEnforcer,
|
||||
}
|
||||
|
||||
/// Parameters for configuring structured outputs (guided decoding).
|
||||
///
|
||||
/// Exactly one constraint field (`json`, `regex`, `choice`, `grammar`,
|
||||
/// `json_object`, or `structural_tag`) should be set. The engine-core
|
||||
/// backend selects the appropriate grammar compiler based on which field
|
||||
/// is present.
|
||||
///
|
||||
/// Original Python definition:
|
||||
/// <https://github.com/vllm-project/vllm/blob/f22d6e026798a74e6542a52ef776c054f2de572a/vllm/sampling_params.py#L36-L107>
|
||||
#[serde_with::skip_serializing_none]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct StructuredOutputsParams {
|
||||
/// The single structured-output constraint selected for a request.
|
||||
#[derive(Debug, Clone, PartialEq, EnumAsInner)]
|
||||
pub enum StructuredOutputConstraint {
|
||||
/// JSON schema (as a dict/object or JSON string) constraining the output.
|
||||
pub json: Option<serde_json::Value>,
|
||||
Json(Value),
|
||||
/// Regular expression the output must match.
|
||||
pub regex: Option<String>,
|
||||
Regex(String),
|
||||
/// List of allowed output strings (the model must produce one of these).
|
||||
pub choice: Option<Vec<String>>,
|
||||
Choice(Vec<String>),
|
||||
/// Context-free grammar (in EBNF-like notation) the output must conform to.
|
||||
pub grammar: Option<String>,
|
||||
/// When `true`, output must be valid JSON (free-form, no schema).
|
||||
pub json_object: Option<bool>,
|
||||
Grammar(String),
|
||||
/// Output must be valid JSON (free-form, no schema).
|
||||
JsonObject,
|
||||
/// Structural tag configuration (JSON-encoded string).
|
||||
StructuralTag(String),
|
||||
}
|
||||
|
||||
/// Additional structured-output options that do not select the constraint mode.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct StructuredOutputOptions {
|
||||
/// Disable any additional whitespace in guided JSON output.
|
||||
#[serde(skip_serializing_if = "crate::protocol::is_false")]
|
||||
pub disable_any_whitespace: bool,
|
||||
/// Disable `additionalProperties` in JSON schema output.
|
||||
#[serde(skip_serializing_if = "crate::protocol::is_false")]
|
||||
pub disable_additional_properties: bool,
|
||||
/// Custom whitespace pattern for guided JSON output.
|
||||
pub whitespace_pattern: Option<String>,
|
||||
/// Structural tag configuration (JSON-encoded string).
|
||||
pub structural_tag: Option<String>,
|
||||
}
|
||||
|
||||
/// Parameters for configuring structured outputs (guided decoding).
|
||||
///
|
||||
/// This is the semantic Rust representation: exactly one constraint mode is
|
||||
/// always selected. The Python/msgpack product-shaped representation is kept in
|
||||
/// the private wire type below and used only at serde boundaries.
|
||||
///
|
||||
/// Original Python definition:
|
||||
/// <https://github.com/vllm-project/vllm/blob/f22d6e026798a74e6542a52ef776c054f2de572a/vllm/sampling_params.py#L36-L107>
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct StructuredOutputsParams {
|
||||
pub constraint: StructuredOutputConstraint,
|
||||
pub options: StructuredOutputOptions,
|
||||
/// Structured-output backend, mirroring Python's internal `_backend`.
|
||||
///
|
||||
/// User-supplied values are ignored during deserialization. This matches
|
||||
/// Python's request boundary, where `_backend` is set by validation rather
|
||||
/// than accepted as a request-level backend selector.
|
||||
pub backend: StructuredOutputBackend,
|
||||
}
|
||||
|
||||
impl StructuredOutputsParams {
|
||||
pub fn json(json: Value) -> Self {
|
||||
Self::from_constraint(StructuredOutputConstraint::Json(json))
|
||||
}
|
||||
|
||||
pub fn regex(regex: impl Into<String>) -> Self {
|
||||
Self::from_constraint(StructuredOutputConstraint::Regex(regex.into()))
|
||||
}
|
||||
|
||||
pub fn choice(choice: Vec<String>) -> Self {
|
||||
Self::from_constraint(StructuredOutputConstraint::Choice(choice))
|
||||
}
|
||||
|
||||
pub fn grammar(grammar: impl Into<String>) -> Self {
|
||||
Self::from_constraint(StructuredOutputConstraint::Grammar(grammar.into()))
|
||||
}
|
||||
|
||||
pub fn json_object() -> Self {
|
||||
Self::from_constraint(StructuredOutputConstraint::JsonObject)
|
||||
}
|
||||
|
||||
pub fn structural_tag(structural_tag: impl Into<String>) -> Self {
|
||||
Self::from_constraint(StructuredOutputConstraint::StructuralTag(
|
||||
structural_tag.into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn from_constraint(constraint: StructuredOutputConstraint) -> Self {
|
||||
Self {
|
||||
constraint,
|
||||
options: StructuredOutputOptions::default(),
|
||||
backend: StructuredOutputBackend::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire-compatible structured-output payload used by Python engine-core.
|
||||
///
|
||||
/// Python models `StructuredOutputsParams` as a product-shaped dataclass with
|
||||
/// several optional constraint fields, then validates that exactly one of those
|
||||
/// fields is present. Rust exposes [`StructuredOutputsParams`] as an enum-backed
|
||||
/// domain type instead, while using this private wire type for ser/de.
|
||||
#[serde_with::skip_serializing_none]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct WireStructuredOutputsParams {
|
||||
json: Option<Value>,
|
||||
regex: Option<String>,
|
||||
choice: Option<Vec<String>>,
|
||||
grammar: Option<String>,
|
||||
json_object: Option<bool>,
|
||||
#[serde(skip_serializing_if = "crate::protocol::is_false")]
|
||||
disable_any_whitespace: bool,
|
||||
#[serde(skip_serializing_if = "crate::protocol::is_false")]
|
||||
disable_additional_properties: bool,
|
||||
whitespace_pattern: Option<String>,
|
||||
structural_tag: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_backend",
|
||||
deserialize_with = "serde_with::rust::deserialize_ignore_any"
|
||||
)]
|
||||
pub backend: StructuredOutputBackend,
|
||||
backend: StructuredOutputBackend,
|
||||
}
|
||||
|
||||
impl TryFrom<WireStructuredOutputsParams> for StructuredOutputsParams {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw: WireStructuredOutputsParams) -> Result<Self> {
|
||||
use StructuredOutputConstraint::*;
|
||||
|
||||
let mut constraint = None;
|
||||
|
||||
macro_rules! insert_constraint {
|
||||
($name:literal, $value:expr) => {
|
||||
if let Some(value) = $value {
|
||||
if let Some((existing, _)) = constraint {
|
||||
return Err(Error::InvalidStructuredOutputsParams {
|
||||
message: format!(
|
||||
"multiple structured output constraints specified: {existing}, {}",
|
||||
$name
|
||||
),
|
||||
});
|
||||
}
|
||||
constraint = Some(($name, value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
insert_constraint!("json", raw.json.map(Json));
|
||||
insert_constraint!("regex", raw.regex.map(Regex));
|
||||
insert_constraint!("choice", raw.choice.map(Choice));
|
||||
insert_constraint!("grammar", raw.grammar.map(Grammar));
|
||||
match raw.json_object {
|
||||
Some(true) => {
|
||||
insert_constraint!("json_object", Some(JsonObject))
|
||||
}
|
||||
Some(false) => {
|
||||
return Err(Error::InvalidStructuredOutputsParams {
|
||||
message: "structured_outputs.json_object must be true if set; omit structured_outputs to disable structured outputs".to_string(),
|
||||
});
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
insert_constraint!("structural_tag", raw.structural_tag.map(StructuralTag));
|
||||
|
||||
Ok(Self {
|
||||
constraint: constraint.map(|(_, c)| c).ok_or_else(|| {
|
||||
Error::InvalidStructuredOutputsParams {
|
||||
message: "missing structured output constraint".to_string(),
|
||||
}
|
||||
})?,
|
||||
options: StructuredOutputOptions {
|
||||
disable_any_whitespace: raw.disable_any_whitespace,
|
||||
disable_additional_properties: raw.disable_additional_properties,
|
||||
whitespace_pattern: raw.whitespace_pattern,
|
||||
},
|
||||
backend: raw.backend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StructuredOutputsParams> for WireStructuredOutputsParams {
|
||||
fn from(params: StructuredOutputsParams) -> Self {
|
||||
let mut raw = Self {
|
||||
disable_any_whitespace: params.options.disable_any_whitespace,
|
||||
disable_additional_properties: params.options.disable_additional_properties,
|
||||
whitespace_pattern: params.options.whitespace_pattern,
|
||||
backend: params.backend,
|
||||
..Self::default()
|
||||
};
|
||||
|
||||
match params.constraint {
|
||||
StructuredOutputConstraint::Json(json) => raw.json = Some(json),
|
||||
StructuredOutputConstraint::Regex(regex) => raw.regex = Some(regex),
|
||||
StructuredOutputConstraint::Choice(choice) => raw.choice = Some(choice),
|
||||
StructuredOutputConstraint::Grammar(grammar) => raw.grammar = Some(grammar),
|
||||
StructuredOutputConstraint::JsonObject => raw.json_object = Some(true),
|
||||
StructuredOutputConstraint::StructuralTag(structural_tag) => {
|
||||
raw.structural_tag = Some(structural_tag);
|
||||
}
|
||||
}
|
||||
|
||||
raw
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for StructuredOutputsParams {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
WireStructuredOutputsParams::from(self.clone()).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for StructuredOutputsParams {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
WireStructuredOutputsParams::deserialize(deserializer)?
|
||||
.try_into()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -74,8 +247,59 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(params.backend, StructuredOutputBackend::Guidance);
|
||||
assert_eq!(params.constraint, StructuredOutputConstraint::JsonObject);
|
||||
|
||||
let value = serde_json::to_value(params).unwrap();
|
||||
assert_eq!(value["_backend"], "guidance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_outputs_rejects_missing_constraint() {
|
||||
let error =
|
||||
serde_json::from_value::<StructuredOutputsParams>(serde_json::json!({})).unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("missing structured output constraint"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_outputs_rejects_multiple_constraints() {
|
||||
let error = serde_json::from_value::<StructuredOutputsParams>(serde_json::json!({
|
||||
"json": {"type": "object"},
|
||||
"regex": ".*",
|
||||
}))
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("multiple structured output constraints specified: json, regex")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_outputs_rejects_json_object_false() {
|
||||
let error = serde_json::from_value::<StructuredOutputsParams>(serde_json::json!({
|
||||
"json_object": false,
|
||||
}))
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("json_object must be true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_outputs_serializes_through_raw_shape() {
|
||||
let params = StructuredOutputsParams {
|
||||
constraint: StructuredOutputConstraint::StructuralTag(
|
||||
r#"{"type":"structural_tag"}"#.to_string(),
|
||||
),
|
||||
options: StructuredOutputOptions::default(),
|
||||
backend: StructuredOutputBackend::Xgrammar,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(params).unwrap();
|
||||
|
||||
assert_eq!(value["structural_tag"], r#"{"type":"structural_tag"}"#);
|
||||
assert_eq!(value["_backend"], "xgrammar");
|
||||
assert!(value.get("json").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ use crate::protocol::multimodal::{
|
||||
SliceSpec,
|
||||
};
|
||||
use crate::protocol::output::{
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, decode_engine_core_outputs,
|
||||
DpControlMessage, DpControlOutput, EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
|
||||
RequestBatchOutputs, UtilityCallOutput, decode_engine_core_outputs,
|
||||
};
|
||||
use crate::protocol::request::{EngineCoreRequest, EngineCoreRequestType};
|
||||
use crate::protocol::sampling::EngineCoreSamplingParams;
|
||||
@@ -571,8 +572,7 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
|
||||
|
||||
send_outputs(
|
||||
&mut data_socket.push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"req-1",
|
||||
vec![],
|
||||
@@ -580,17 +580,19 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
send_outputs(
|
||||
&mut coordinator.output_push,
|
||||
EngineCoreOutputs {
|
||||
DpControlOutput {
|
||||
engine_index: 0,
|
||||
wave_complete: Some(0),
|
||||
..Default::default()
|
||||
},
|
||||
timestamp: 0.0,
|
||||
control: DpControlMessage::WaveComplete(0),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -605,8 +607,7 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
|
||||
|
||||
send_outputs(
|
||||
&mut data_socket.push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"req-3",
|
||||
vec![],
|
||||
@@ -614,7 +615,8 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-3".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -651,7 +653,7 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
|
||||
|
||||
send_outputs(
|
||||
&mut data_socket.push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
engine_index: 1,
|
||||
outputs: vec![request_output(
|
||||
"req-2",
|
||||
@@ -660,7 +662,8 @@ async fn coordinator_wave_control_tracks_pause_running_and_rebroadcasts() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-2".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -766,11 +769,12 @@ async fn coordinator_rebroadcasts_engine_start_wave_control() {
|
||||
|
||||
send_outputs(
|
||||
&mut coordinator.output_push,
|
||||
EngineCoreOutputs {
|
||||
DpControlOutput {
|
||||
engine_index: 1,
|
||||
start_wave: Some(4),
|
||||
..Default::default()
|
||||
},
|
||||
timestamp: 0.0,
|
||||
control: DpControlMessage::StartWave(4),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -821,15 +825,16 @@ async fn coordinator_accepts_stats_only_outputs() {
|
||||
|
||||
send_outputs(
|
||||
&mut coordinator.output_push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: Vec::new(),
|
||||
scheduler_stats: Some(Box::new(SchedulerStats {
|
||||
num_running_reqs: 1,
|
||||
current_wave: 0,
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -840,8 +845,7 @@ async fn coordinator_accepts_stats_only_outputs() {
|
||||
|
||||
send_outputs(
|
||||
&mut data_socket.push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"req-stats",
|
||||
vec![],
|
||||
@@ -849,7 +853,8 @@ async fn coordinator_accepts_stats_only_outputs() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-stats".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -910,30 +915,34 @@ async fn client_fail_closes_when_main_output_path_receives_dp_control() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
output: UtilityOutput {
|
||||
call_id: 1_u64.into(),
|
||||
failure_message: None,
|
||||
result: None,
|
||||
}),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
start_wave: Some(3),
|
||||
..Default::default()
|
||||
},
|
||||
DpControlOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
control: DpControlMessage::StartWave(3),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output("req-1", vec![999], None)],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -985,91 +994,6 @@ async fn client_fail_closes_when_main_output_path_receives_dp_control() {
|
||||
client.shutdown().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn client_fail_closes_when_main_output_path_receives_mixed_shape_output() {
|
||||
init_tracing();
|
||||
let ipc = IpcNamespace::new().unwrap();
|
||||
let handshake_address = ipc.handshake_endpoint();
|
||||
let engine_id = b"engine-0".to_vec();
|
||||
|
||||
let (shutdown_tx, engine_task) = spawn_mock_engine_task(
|
||||
handshake_address.clone(),
|
||||
engine_id.clone(),
|
||||
|dealer, push| {
|
||||
Box::pin(async move {
|
||||
let add_1 = recv_engine_message(dealer).await;
|
||||
assert_eq!(add_1[0].as_ref(), &[0x00]);
|
||||
let request_1: EngineCoreRequest = rmp_serde::from_slice(&add_1[1]).unwrap();
|
||||
assert_eq!(request_1.client_index, 7);
|
||||
assert_eq!(request_1.request_id, "req-1");
|
||||
|
||||
let add_2 = recv_engine_message(dealer).await;
|
||||
assert_eq!(add_2[0].as_ref(), &[0x00]);
|
||||
let request_2: EngineCoreRequest = rmp_serde::from_slice(&add_2[1]).unwrap();
|
||||
assert_eq!(request_2.client_index, 7);
|
||||
assert_eq!(request_2.request_id, "req-2");
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
call_id: 1_u64.into(),
|
||||
failure_message: None,
|
||||
result: None,
|
||||
}),
|
||||
outputs: vec![request_output("req-1", vec![999], None)],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let client = connect_client_with_ipc(
|
||||
handshake_test_config(
|
||||
handshake_address,
|
||||
1,
|
||||
"test-model",
|
||||
Duration::from_secs(2),
|
||||
7,
|
||||
None,
|
||||
),
|
||||
&ipc,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(client.engine_identities()[0], b"engine-0");
|
||||
assert!(client.ready_responses()[0].max_model_len > 0);
|
||||
|
||||
let mut stream_1 = client.call(sample_request_with_id("req-1")).await.unwrap();
|
||||
let mut stream_2 = client.call(sample_request_with_id("req-2")).await.unwrap();
|
||||
|
||||
let error_2 = timeout(Duration::from_secs(1), stream_2.next())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap_err();
|
||||
assert!(is_unexpected_dispatcher_output(&error_2));
|
||||
|
||||
let error_1 = timeout(Duration::from_secs(1), stream_1.next())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap_err();
|
||||
assert!(is_unexpected_dispatcher_output(&error_1));
|
||||
|
||||
assert!(matches!(
|
||||
client.health_error().as_deref(),
|
||||
Some(error) if is_unexpected_dispatcher_output(error)
|
||||
));
|
||||
|
||||
let _ = shutdown_tx.send(());
|
||||
engine_task.await.unwrap();
|
||||
client.shutdown().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn duplicate_request_ids_are_rejected_without_sending_a_second_add() {
|
||||
init_tracing();
|
||||
@@ -1091,7 +1015,7 @@ async fn duplicate_request_ids_are_rejected_without_sending_a_second_add() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"req-1",
|
||||
vec![],
|
||||
@@ -1099,7 +1023,8 @@ async fn duplicate_request_ids_are_rejected_without_sending_a_second_add() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1158,10 +1083,12 @@ async fn finished_requests_without_final_output_is_treated_as_unexpected_close()
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: Vec::new(),
|
||||
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1217,10 +1144,11 @@ async fn dropping_a_live_stream_triggers_abort() {
|
||||
assert_eq!(add[0].as_ref(), &[0x00]);
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output("req-1", vec![99], None)],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1275,14 +1203,15 @@ async fn dropping_multiple_live_streams_aborts_all_in_a_burst() {
|
||||
}
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output("req-1", vec![99], None),
|
||||
request_output("req-2", vec![99], None),
|
||||
request_output("req-3", vec![99], None),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1436,14 +1365,16 @@ async fn is_sleeping_wrapper_sends_typed_request_and_returns_typed_response() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: None,
|
||||
result: Some(utility_result_value(true)),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1491,14 +1422,16 @@ async fn call_utility_failure_message_surfaces_as_error() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: Some("boom".to_string()),
|
||||
result: None,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1815,8 +1748,7 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
|
||||
finish_req_1_rx.await.unwrap();
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
&request_1.request_id,
|
||||
vec![10],
|
||||
@@ -1824,7 +1756,8 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from([request_1.request_id.clone()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1836,8 +1769,7 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
|
||||
finish_req_3_rx.await.unwrap();
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
&request_3.request_id,
|
||||
vec![30],
|
||||
@@ -1845,7 +1777,8 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from([request_3.request_id.clone()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1864,7 +1797,7 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
|
||||
finish_req_2_rx.await.unwrap();
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
engine_index: 1,
|
||||
outputs: vec![request_output(
|
||||
&request_2.request_id,
|
||||
@@ -1873,7 +1806,8 @@ async fn multi_engine_client_shares_transport_and_routes_by_inflight_count() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from([request_2.request_id.clone()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -1990,14 +1924,16 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
|
||||
assert_eq!(array[2], Value::from("is_sleeping"));
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: None,
|
||||
result: Some(utility_result_value(true)),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -2012,8 +1948,7 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
|
||||
assert_eq!(aborted_ids, vec!["req-1".to_string()]);
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"req-1",
|
||||
vec![],
|
||||
@@ -2021,7 +1956,8 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -2044,14 +1980,16 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
|
||||
assert_eq!(array[2], Value::from("is_sleeping"));
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: None,
|
||||
result: Some(utility_result_value(true)),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -2066,7 +2004,7 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
|
||||
assert_eq!(aborted_ids, vec!["req-2".to_string()]);
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
engine_index: 1,
|
||||
outputs: vec![request_output(
|
||||
"req-2",
|
||||
@@ -2075,7 +2013,8 @@ async fn multi_engine_abort_is_grouped_and_utility_fans_out_to_all_engines() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-2".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -2155,14 +2094,16 @@ async fn collective_rpc_flattens_results_from_all_engines() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: None,
|
||||
result: Some(utility_result_value(vec!["engine-0-worker"])),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -2186,14 +2127,16 @@ async fn collective_rpc_flattens_results_from_all_engines() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: None,
|
||||
result: Some(utility_result_value(vec!["engine-1-worker"])),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -2264,14 +2207,16 @@ fn spawn_mock_utility_engine(
|
||||
assert_eq!(array[3], expected_args, "unexpected utility args");
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
engine_index: 0,
|
||||
timestamp: 0.0,
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: None,
|
||||
result: Some(utility_result_value(result)),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -2552,41 +2497,40 @@ fn python_msgpack_fixtures_match_rust_encoding() {
|
||||
|
||||
let decoded_outputs: EngineCoreOutputs = rmp_serde::from_slice(&outputs_bytes).unwrap();
|
||||
expect_test::expect![[r#"
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
outputs: [
|
||||
EngineCoreOutput {
|
||||
request_id: "req-1",
|
||||
new_token_ids: [
|
||||
7,
|
||||
8,
|
||||
],
|
||||
new_logprobs: None,
|
||||
new_prompt_logprobs_tensors: None,
|
||||
pooling_output: None,
|
||||
finish_reason: Some(
|
||||
Length,
|
||||
),
|
||||
stop_reason: None,
|
||||
events: None,
|
||||
kv_transfer_params: None,
|
||||
trace_headers: None,
|
||||
prefill_stats: None,
|
||||
routed_experts: None,
|
||||
num_nans_in_logits: 0,
|
||||
},
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: Some(
|
||||
{
|
||||
"req-1",
|
||||
},
|
||||
),
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
}
|
||||
RequestBatch(
|
||||
RequestBatchOutputs {
|
||||
engine_index: 0,
|
||||
outputs: [
|
||||
EngineCoreOutput {
|
||||
request_id: "req-1",
|
||||
new_token_ids: [
|
||||
7,
|
||||
8,
|
||||
],
|
||||
new_logprobs: None,
|
||||
new_prompt_logprobs_tensors: None,
|
||||
pooling_output: None,
|
||||
finish_reason: Some(
|
||||
Length,
|
||||
),
|
||||
stop_reason: None,
|
||||
events: None,
|
||||
kv_transfer_params: None,
|
||||
trace_headers: None,
|
||||
prefill_stats: None,
|
||||
routed_experts: None,
|
||||
num_nans_in_logits: 0,
|
||||
},
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
finished_requests: Some(
|
||||
{
|
||||
"req-1",
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&decoded_outputs);
|
||||
|
||||
@@ -2599,7 +2543,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
|
||||
let inline_logprobs =
|
||||
decode_engine_core_outputs(&decode_frames(inline_logprobs_frames)).unwrap();
|
||||
expect_sample_logprobs(
|
||||
inline_logprobs.outputs[0]
|
||||
inline_logprobs.as_request_batch().unwrap().outputs[0]
|
||||
.new_logprobs
|
||||
.as_ref()
|
||||
.expect("inline logprobs decoded"),
|
||||
@@ -2608,7 +2552,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
|
||||
let multipart_logprobs =
|
||||
decode_engine_core_outputs(&decode_frames(multipart_logprobs_frames)).unwrap();
|
||||
expect_sample_logprobs(
|
||||
multipart_logprobs.outputs[0]
|
||||
multipart_logprobs.as_request_batch().unwrap().outputs[0]
|
||||
.new_logprobs
|
||||
.as_ref()
|
||||
.expect("multipart logprobs decoded"),
|
||||
@@ -2616,7 +2560,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
|
||||
|
||||
let inline_prompt = decode_engine_core_outputs(&decode_frames(inline_prompt_frames)).unwrap();
|
||||
expect_prompt_logprobs(
|
||||
inline_prompt.outputs[0]
|
||||
inline_prompt.as_request_batch().unwrap().outputs[0]
|
||||
.new_prompt_logprobs_tensors
|
||||
.as_ref()
|
||||
.expect("inline prompt logprobs decoded"),
|
||||
@@ -2625,7 +2569,7 @@ fn python_msgpack_fixtures_match_rust_encoding() {
|
||||
let multipart_prompt =
|
||||
decode_engine_core_outputs(&decode_frames(multipart_prompt_frames)).unwrap();
|
||||
expect_prompt_logprobs(
|
||||
multipart_prompt.outputs[0]
|
||||
multipart_prompt.as_request_batch().unwrap().outputs[0]
|
||||
.new_prompt_logprobs_tensors
|
||||
.as_ref()
|
||||
.expect("multipart prompt logprobs decoded"),
|
||||
@@ -2945,8 +2889,7 @@ async fn bootstrapped_external_coordinator_updates_wave_ignores_counts_and_sends
|
||||
|
||||
send_outputs(
|
||||
&mut push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"req-1",
|
||||
vec![],
|
||||
@@ -2954,7 +2897,8 @@ async fn bootstrapped_external_coordinator_updates_wave_ignores_counts_and_sends
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -3020,8 +2964,7 @@ async fn bootstrapped_external_coordinator_running_state_suppresses_wakeup() {
|
||||
|
||||
send_outputs(
|
||||
&mut push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
"req-1",
|
||||
vec![],
|
||||
@@ -3029,7 +2972,8 @@ async fn bootstrapped_external_coordinator_running_state_suppresses_wakeup() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from(["req-1".to_string()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use vllm_engine_core_client::protocol::logprobs::{
|
||||
};
|
||||
use vllm_engine_core_client::protocol::output::{
|
||||
EngineCoreEvent, EngineCoreEventType, EngineCoreFinishReason, EngineCoreOutput,
|
||||
EngineCoreOutputs,
|
||||
EngineCoreOutputs, RequestBatchOutputs,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
|
||||
use vllm_engine_core_client::protocol::sampling::EngineCoreSamplingParams;
|
||||
@@ -251,7 +251,7 @@ async fn generate_streams_outputs() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
@@ -270,7 +270,8 @@ async fn generate_streams_outputs() {
|
||||
],
|
||||
finished_requests: Some(BTreeSet::from([request.request_id.clone()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -331,8 +332,7 @@ async fn collect_output_aggregates_raw_tokens_logprobs_and_terminal_metadata() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
EngineCoreOutput {
|
||||
prefill_stats: Some(PrefillStats {
|
||||
@@ -358,13 +358,9 @@ async fn collect_output_aggregates_raw_tokens_logprobs_and_terminal_metadata() {
|
||||
Some(serde_json::json!({"connector": "x"})),
|
||||
),
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -412,10 +408,12 @@ async fn generate_propagates_unexpected_close_errors() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: Vec::new(),
|
||||
finished_requests: Some(BTreeSet::from([request.request_id])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -459,10 +457,11 @@ async fn dropping_a_live_generate_stream_triggers_abort() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(&request.request_id, vec![99], None)],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -513,7 +512,7 @@ async fn duplicate_external_request_ids_are_randomized_before_reaching_engine_co
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
&request_1.request_id,
|
||||
vec![],
|
||||
@@ -521,13 +520,14 @@ async fn duplicate_external_request_ids_are_randomized_before_reaching_engine_co
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from([request_1.request_id.clone()])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(
|
||||
&request_2.request_id,
|
||||
vec![],
|
||||
@@ -535,7 +535,8 @@ async fn duplicate_external_request_ids_are_randomized_before_reaching_engine_co
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from([request_2.request_id])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -576,10 +577,11 @@ async fn abort_resolves_external_request_id_to_internal_before_reaching_engine()
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output(&request.request_id, vec![7], None)],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -643,13 +645,14 @@ async fn abort_by_external_id_aborts_all_internal_requests() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output(&request_1.request_id, vec![7], None),
|
||||
request_output(&request_2.request_id, vec![8], None),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -710,7 +713,7 @@ async fn generate_records_request_metrics_in_prometheus_output() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
engine_index: 4,
|
||||
timestamp: 10.0,
|
||||
outputs: vec![EngineCoreOutput {
|
||||
@@ -736,13 +739,14 @@ async fn generate_records_request_metrics_in_prometheus_output() {
|
||||
)
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
engine_index: 4,
|
||||
timestamp: 11.5,
|
||||
outputs: vec![request_output_with_events(
|
||||
@@ -756,7 +760,8 @@ async fn generate_records_request_metrics_in_prometheus_output() {
|
||||
)],
|
||||
finished_requests: Some(BTreeSet::from([request.request_id])),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -843,7 +848,7 @@ async fn dropping_stream_records_abort_terminal_request_metrics() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
engine_index: 5,
|
||||
timestamp: 10.0,
|
||||
outputs: vec![request_output_with_events(
|
||||
@@ -862,7 +867,8 @@ async fn dropping_stream_records_abort_terminal_request_metrics() {
|
||||
]),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ use tokio::task::yield_now;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info, warn};
|
||||
use vllm_engine_core_client::protocol::output::{
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs,
|
||||
UtilityCallOutput,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
|
||||
use vllm_engine_core_client::protocol::utility::{
|
||||
@@ -61,13 +62,14 @@ fn empty_finish_outputs(
|
||||
let output = request_output(request_id, Vec::new(), Some(finish_reason));
|
||||
let finished_requests = BTreeSet::from([output.request_id.clone()]);
|
||||
|
||||
EngineCoreOutputs {
|
||||
RequestBatchOutputs {
|
||||
engine_index,
|
||||
outputs: vec![output],
|
||||
timestamp: now_secs(),
|
||||
finished_requests: Some(finished_requests),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Encode a utility result into the protocol's msgpack value envelope.
|
||||
@@ -98,16 +100,16 @@ fn utility_response(
|
||||
_ => utility_envelope(Value::Nil),
|
||||
}?;
|
||||
|
||||
Ok(EngineCoreOutputs {
|
||||
Ok(UtilityCallOutput {
|
||||
engine_index,
|
||||
utility_output: Some(UtilityOutput {
|
||||
timestamp: now_secs(),
|
||||
output: UtilityOutput {
|
||||
call_id: request.call_id,
|
||||
failure_message: None,
|
||||
result: Some(result),
|
||||
}),
|
||||
timestamp: now_secs(),
|
||||
..Default::default()
|
||||
})
|
||||
},
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Message sent from the frontend to the mock engine task to drive the engine loop.
|
||||
@@ -270,13 +272,14 @@ impl Engine {
|
||||
}
|
||||
for (client_index, (client_outputs, finished_requests)) in outputs_by_client {
|
||||
outputs.push({
|
||||
let outputs = EngineCoreOutputs {
|
||||
let outputs = RequestBatchOutputs {
|
||||
engine_index: self.engine_index,
|
||||
outputs: client_outputs,
|
||||
timestamp: now_secs(),
|
||||
finished_requests: Some(finished_requests),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
.into();
|
||||
EngineOutput {
|
||||
client_index,
|
||||
outputs,
|
||||
@@ -358,14 +361,15 @@ impl Engine {
|
||||
.filter_map(|(client_index, (outputs, finished_requests))| {
|
||||
(!outputs.is_empty()).then(|| EngineOutput {
|
||||
client_index,
|
||||
outputs: EngineCoreOutputs {
|
||||
outputs: RequestBatchOutputs {
|
||||
engine_index: self.engine_index,
|
||||
outputs,
|
||||
timestamp: now_secs(),
|
||||
finished_requests: (!finished_requests.is_empty())
|
||||
.then_some(finished_requests),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -231,32 +231,18 @@ fn convert_structured_output(
|
||||
StructuredOutput::Json(schema) => {
|
||||
let json: serde_json::Value = serde_json::from_str(schema)
|
||||
.map_err(|e| Status::invalid_argument(format!("invalid json schema: {e}")))?;
|
||||
StructuredOutputsParams {
|
||||
json: Some(json),
|
||||
..Default::default()
|
||||
}
|
||||
StructuredOutputsParams::json(json)
|
||||
}
|
||||
StructuredOutput::Regex(regex) => StructuredOutputsParams {
|
||||
regex: Some(regex.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
StructuredOutput::Choice(choices) => StructuredOutputsParams {
|
||||
choice: Some(choices.choices.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
StructuredOutput::Grammar(grammar) => StructuredOutputsParams {
|
||||
grammar: Some(grammar.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
StructuredOutput::JsonObject(true) => StructuredOutputsParams {
|
||||
json_object: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
StructuredOutput::Regex(regex) => StructuredOutputsParams::regex(regex.clone()),
|
||||
StructuredOutput::Choice(choices) => {
|
||||
StructuredOutputsParams::choice(choices.choices.clone())
|
||||
}
|
||||
StructuredOutput::Grammar(grammar) => StructuredOutputsParams::grammar(grammar.clone()),
|
||||
StructuredOutput::JsonObject(true) => StructuredOutputsParams::json_object(),
|
||||
StructuredOutput::JsonObject(false) => return Ok(None),
|
||||
StructuredOutput::StructuralTag(tag) => StructuredOutputsParams {
|
||||
structural_tag: Some(tag.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
StructuredOutput::StructuralTag(tag) => {
|
||||
StructuredOutputsParams::structural_tag(tag.clone())
|
||||
}
|
||||
};
|
||||
Ok(Some(params))
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use vllm_chat::{
|
||||
DynChatOutputProcessor, DynChatRenderer, NewChatOutputProcessorOptions, RenderedPrompt,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::output::{
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
|
||||
use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
|
||||
@@ -115,19 +115,14 @@ fn engine_outputs_for_request(
|
||||
request_id: &str,
|
||||
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
|
||||
) -> EngineCoreOutputs {
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: output_specs
|
||||
.into_iter()
|
||||
.map(|(token_ids, finish_reason)| request_output(request_id, token_ids, finish_reason))
|
||||
.collect(),
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn default_stream_output_specs() -> Vec<(Vec<u32>, Option<EngineCoreFinishReason>)> {
|
||||
|
||||
@@ -19,7 +19,7 @@ use vllm_chat::{
|
||||
DynChatOutputProcessor, DynChatRenderer, NewChatOutputProcessorOptions, RenderedPrompt,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::output::{
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs,
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
|
||||
use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
|
||||
@@ -111,19 +111,14 @@ fn engine_outputs_for_request(
|
||||
request_id: &str,
|
||||
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
|
||||
) -> EngineCoreOutputs {
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: output_specs
|
||||
.into_iter()
|
||||
.map(|(token_ids, finish_reason)| request_output(request_id, token_ids, finish_reason))
|
||||
.collect(),
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn default_stream_output_specs() -> Vec<(Vec<u32>, Option<EngineCoreFinishReason>)> {
|
||||
|
||||
@@ -37,7 +37,7 @@ pub enum ResponseFormat {
|
||||
},
|
||||
/// vLLM-specific structural tag format. The entire object (including the
|
||||
/// `type` field) is JSON-serialized and passed as
|
||||
/// `StructuredOutputsParams.structural_tag`.
|
||||
/// `StructuredOutputConstraint::StructuralTag`.
|
||||
///
|
||||
/// We capture the payload as a catch-all map so both the legacy
|
||||
/// (`structures`/`triggers`) and current (`format`) shapes are
|
||||
@@ -79,14 +79,10 @@ pub fn convert_from_response_format(
|
||||
};
|
||||
match fmt {
|
||||
ResponseFormat::Text => Ok(None),
|
||||
ResponseFormat::JsonObject => Ok(Some(StructuredOutputsParams {
|
||||
json_object: Some(true),
|
||||
..Default::default()
|
||||
})),
|
||||
ResponseFormat::JsonSchema { json_schema } => Ok(Some(StructuredOutputsParams {
|
||||
json: Some(json_schema.schema.clone()),
|
||||
..Default::default()
|
||||
})),
|
||||
ResponseFormat::JsonObject => Ok(Some(StructuredOutputsParams::json_object())),
|
||||
ResponseFormat::JsonSchema { json_schema } => Ok(Some(StructuredOutputsParams::json(
|
||||
json_schema.schema.clone(),
|
||||
))),
|
||||
ResponseFormat::StructuralTag { .. } => {
|
||||
// The Python frontend dumps the entire response_format object (including the
|
||||
// `type` field) as a JSON string for the engine-core backend.
|
||||
@@ -96,10 +92,7 @@ pub fn convert_from_response_format(
|
||||
Some("response_format"),
|
||||
)
|
||||
})?;
|
||||
Ok(Some(StructuredOutputsParams {
|
||||
structural_tag: Some(tag_json),
|
||||
..Default::default()
|
||||
}))
|
||||
Ok(Some(StructuredOutputsParams::structural_tag(tag_json)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ use vllm_engine_core_client::protocol::logprobs::{
|
||||
Logprobs, MaybeWireLogprobs, PositionLogprobs, TokenLogprob,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::output::{
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, StopReason,
|
||||
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, RequestBatchOutputs, StopReason,
|
||||
UtilityCallOutput,
|
||||
};
|
||||
use vllm_engine_core_client::protocol::request::EngineCoreRequest;
|
||||
use vllm_engine_core_client::protocol::utility::{UtilityOutput, UtilityResultEnvelope};
|
||||
@@ -232,19 +233,14 @@ fn engine_outputs_for_request(
|
||||
request_id: &str,
|
||||
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
|
||||
) -> EngineCoreOutputs {
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: output_specs
|
||||
.into_iter()
|
||||
.map(|(token_ids, finish_reason)| request_output(request_id, token_ids, finish_reason))
|
||||
.collect(),
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn test_llm(client: EngineCoreClient) -> Llm {
|
||||
@@ -390,14 +386,15 @@ fn utility_none_result() -> UtilityResultEnvelope {
|
||||
}
|
||||
|
||||
fn utility_outputs(call_id: u64, result: UtilityResultEnvelope) -> EngineCoreOutputs {
|
||||
EngineCoreOutputs {
|
||||
utility_output: Some(UtilityOutput {
|
||||
UtilityCallOutput {
|
||||
output: UtilityOutput {
|
||||
call_id: call_id.into(),
|
||||
failure_message: None,
|
||||
result: Some(result),
|
||||
}),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn send_outputs(push: &mut PushSocket, outputs: EngineCoreOutputs) {
|
||||
@@ -2281,8 +2278,7 @@ async fn non_stream_chat_includes_logprobs_and_prompt_logprobs() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
bytes_to_token_ids(b"hi"),
|
||||
@@ -2291,13 +2287,9 @@ async fn non_stream_chat_includes_logprobs_and_prompt_logprobs() {
|
||||
Some(sample_logprobs_for_tokens(&bytes_to_token_ids(b"hi"))),
|
||||
Some(prompt_logprobs_for_tokens(&prompt_token_ids)),
|
||||
)],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -3121,8 +3113,7 @@ async fn non_stream_completions_include_logprobs() {
|
||||
rmp_serde::from_slice(&add[1]).expect("decode request");
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
@@ -3141,13 +3132,10 @@ async fn non_stream_completions_include_logprobs() {
|
||||
None,
|
||||
),
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: Some(BTreeSet::from([request.request_id.clone()])),
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -3224,8 +3212,7 @@ async fn non_stream_completions_include_prompt_logprobs() {
|
||||
rmp_serde::from_slice(&add[1]).expect("decode request");
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
vec![b'h' as u32, b'i' as u32, b'!' as u32],
|
||||
@@ -3246,13 +3233,9 @@ async fn non_stream_completions_include_prompt_logprobs() {
|
||||
}),
|
||||
Some(prompt_logprobs_for_hello()),
|
||||
)],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -3588,8 +3571,7 @@ async fn non_stream_raw_generate_returns_token_output_envelope() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
@@ -3609,13 +3591,9 @@ async fn non_stream_raw_generate_returns_token_output_envelope() {
|
||||
Some(json!({"connector": "x"})),
|
||||
),
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -3708,8 +3686,7 @@ async fn stream_raw_generate_returns_sse_chunks_and_usage() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
@@ -3728,13 +3705,9 @@ async fn stream_raw_generate_returns_sse_chunks_and_usage() {
|
||||
None,
|
||||
),
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -4426,8 +4399,7 @@ async fn include_reasoning_false_suppresses_non_stream_output_metadata() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![
|
||||
request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
@@ -4446,13 +4418,9 @@ async fn include_reasoning_false_suppresses_non_stream_output_metadata() {
|
||||
None,
|
||||
),
|
||||
],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
@@ -4601,8 +4569,7 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
|
||||
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
bytes_to_token_ids(b"<think>Need tool.</think>"),
|
||||
@@ -4613,19 +4580,14 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
|
||||
))),
|
||||
None,
|
||||
)],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
bytes_to_token_ids(b"<tool_call>\n{\"name\":\"get_weather\", "),
|
||||
@@ -4636,19 +4598,14 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
|
||||
))),
|
||||
None,
|
||||
)],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: None,
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
send_outputs(
|
||||
push,
|
||||
EngineCoreOutputs {
|
||||
engine_index: 0,
|
||||
RequestBatchOutputs {
|
||||
outputs: vec![request_output_with_logprobs(
|
||||
&request.request_id,
|
||||
bytes_to_token_ids(
|
||||
@@ -4661,13 +4618,10 @@ async fn tool_call_sse_chunks_can_carry_logprobs() {
|
||||
))),
|
||||
None,
|
||||
)],
|
||||
scheduler_stats: None,
|
||||
timestamp: 0.0,
|
||||
utility_output: None,
|
||||
finished_requests: Some(BTreeSet::from([request.request_id.clone()])),
|
||||
wave_complete: None,
|
||||
start_wave: None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user