use std::collections::HashSet;
use std::sync::{Arc, Mutex, OnceLock};
use derive_builder::Builder;
pub use dynamo_kv_router::kv_hints::{
KV_HINT_TRANSFER_CAPABILITY_KEY, KvHint, KvHintAction, KvSourceLocationsPayload,
};
use dynamo_kv_router::{
config::RouterConfigOverride,
protocols::{BlockExtraInfo, RoutingConstraints, WorkerId},
scheduling::{AbortCause, RequestLifecycle},
};
use dynamo_runtime::error::{DynamoError, ErrorType, match_error_chain};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::extensions::{AgentContext, RouterParams};
use super::llm_backend::DecoderCheckpoint;
use super::timing::{RequestPhase, RequestTracker};
use super::{OutputOptions, SamplingOptions, StopConditions};
use crate::preprocessor::media::RdmaMediaDataDescriptor;
use crate::protocols::TokenIdType;
#[derive(Serialize, Deserialize, Debug, Clone, Default, Builder)]
#[builder(default)]
pub struct RoutingHints {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backend_instance_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefill_worker_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decode_worker_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dp_rank: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefill_dp_rank: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_output_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lora_name: Option<String>,
#[serde(
default,
rename = "cache_salt",
skip_serializing_if = "Option::is_none"
)]
pub cache_namespace: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority_jump: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strict_priority: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_worker_ids: Option<HashSet<WorkerId>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routing_constraints: Option<RoutingConstraints>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BootstrapInfo {
pub bootstrap_host: String,
pub bootstrap_port: u16,
pub bootstrap_room: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handoff_id: Option<Uuid>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct TraceLink {
pub trace_id: String,
pub span_id: String,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct MigrationState {
inner: Arc<OnceLock<Mutex<MigrationStateInner>>>,
}
#[derive(Debug, Default)]
struct MigrationStateInner {
excluded_worker_ids: Vec<WorkerId>,
last_error: Option<DynamoError>,
request_lifecycle: Option<Box<RequestLifecycle>>,
}
impl MigrationState {
pub(crate) fn record_failure(&self, worker_id: WorkerId, error: Option<DynamoError>) {
let mut inner = self
.inner
.get_or_init(Default::default)
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !inner.excluded_worker_ids.contains(&worker_id) {
inner.excluded_worker_ids.push(worker_id);
}
if error.is_some() {
inner.last_error = error;
}
}
pub(crate) fn excluded_worker_ids(&self) -> Vec<WorkerId> {
let Some(inner) = self.inner.get() else {
return Vec::new();
};
inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.excluded_worker_ids
.clone()
}
pub(crate) fn exhausted_error(&self) -> Option<DynamoError> {
let inner = self.inner.get()?;
let last_error = inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.last_error
.clone()?;
let (error_type, message) = if match_error_chain(
&last_error,
&[ErrorType::WorkerOverloaded],
&[ErrorType::ResourceExhausted],
) {
(
ErrorType::ResourceExhausted,
"all eligible workers rejected the request as overloaded",
)
} else {
(
ErrorType::Unavailable,
"no untried eligible worker remains after migration",
)
};
Some(
DynamoError::builder()
.error_type(error_type)
.message(message)
.build(),
)
}
pub(crate) fn take_request_lifecycle(&self) -> Option<Box<RequestLifecycle>> {
self.inner
.get()?
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.request_lifecycle
.take()
}
pub(crate) fn store_request_lifecycle(&self, lifecycle: Box<RequestLifecycle>) {
let replaced = self
.inner
.get_or_init(Default::default)
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.request_lifecycle
.replace(lifecycle);
if let Some(replaced) = replaced {
tracing::warn!(
replaced = ?replaced,
"Replacing an unclaimed request-classifier migration lifecycle"
);
}
}
pub(crate) fn abort_request_lifecycle(&self, error: Option<&AbortCause>) {
let Some(mut lifecycle) = self.take_request_lifecycle() else {
return;
};
lifecycle.abort(error.map(owned_abort_error));
}
}
pub(crate) fn owned_abort_error(error: &AbortCause) -> Arc<AbortCause> {
let mut cause: Option<&(dyn std::error::Error + 'static)> = Some(error);
while let Some(current) = cause {
if let Some(dynamo_error) = current.downcast_ref::<DynamoError>() {
return Arc::new(dynamo_error.clone());
}
cause = current.source();
}
Arc::new(DynamoError::from(
error as &(dyn std::error::Error + 'static),
))
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PrefillResult {
pub disaggregated_params: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_tokens_details: Option<dynamo_protocols::types::PromptTokensDetails>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, Builder)]
#[builder(default)]
pub struct MmRoutingInfo {
pub routing_token_ids: Vec<TokenIdType>,
pub block_mm_infos: Vec<Option<BlockExtraInfo>>,
#[serde(default)]
pub expanded_prompt_len: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum MultimodalData {
Url(url::Url),
#[serde(rename(serialize = "Url"))]
RawUrl(String),
Decoded(RdmaMediaDataDescriptor),
UuidOnly(String),
}
pub type MultimodalDataMap = std::collections::HashMap<String, Vec<MultimodalData>>;
pub type MultimodalUuidMap = std::collections::HashMap<String, Vec<Option<String>>>;
#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
pub struct PreprocessedRequest {
#[serde(default)]
pub model: String,
#[builder(default)]
#[serde(skip)]
pub(crate) migration_state: Option<MigrationState>,
#[builder(default)]
#[serde(skip)]
pub(crate) staged_kv_cleanup: bool,
#[builder(setter(into))]
#[serde(
serialize_with = "serialize_token_ids",
deserialize_with = "deserialize_token_ids"
)]
pub token_ids: Arc<Vec<TokenIdType>>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_embeds: Option<String>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_modal_data: Option<MultimodalDataMap>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_modal_uuids: Option<MultimodalUuidMap>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mm_routing_info: Option<MmRoutingInfo>,
#[serde(default)]
pub stop_conditions: StopConditions,
#[serde(default)]
pub sampling_options: SamplingOptions,
#[serde(default)]
pub output_options: OutputOptions,
#[builder(default)]
#[serde(default)]
pub eos_token_ids: Vec<TokenIdType>,
#[builder(default)]
pub mdc_sum: Option<String>,
#[builder(default)]
#[serde(default)]
pub annotations: Vec<String>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routing: Option<RoutingHints>,
#[builder(default)]
pub router_config_override: Option<RouterConfigOverride>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefill_result: Option<PrefillResult>,
#[builder(default)]
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_object"
)]
pub encoder_result: Option<serde_json::Value>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub migration_link: Option<TraceLink>,
#[builder(default)]
#[serde(skip)]
pub(crate) jail_seed: Option<DecoderCheckpoint>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bootstrap_info: Option<BootstrapInfo>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra_args: Option<serde_json::Value>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kv_hint: Option<KvHint>,
#[builder(default)]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub require_reasoning: bool,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub router: Option<RouterParams>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_context: Option<AgentContext>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_cache_scope: Option<String>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mm_processor_kwargs: Option<serde_json::Value>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_io_kwargs: Option<serde_json::Value>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_timestamp_ms: Option<f64>,
#[builder(default)]
#[serde(skip)]
pub tracker: Option<Arc<RequestTracker>>,
#[builder(default)]
#[serde(
default,
rename = "_HEALTH_CHECK",
skip_serializing_if = "std::ops::Not::not"
)]
pub is_probe: bool,
}
static TOKEN_IDS_AS_BYTES: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
dynamo_runtime::config::env_is_truthy(
dynamo_runtime::config::environment_names::request_plane::DYN_TOKEN_IDS_AS_BYTES,
)
});
fn token_ids_fit_i32(ids: &[TokenIdType]) -> bool {
ids.iter().all(|&id| id <= i32::MAX as TokenIdType)
}
const TOKEN_IDS_PREALLOC_CAP: usize = 1 << 20;
fn serialize_token_ids<S>(ids: &Arc<Vec<TokenIdType>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_token_ids_with(ids, serializer, *TOKEN_IDS_AS_BYTES)
}
fn serialize_token_ids_with<S>(
ids: &[TokenIdType],
serializer: S,
packed: bool,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if packed && !serializer.is_human_readable() && token_ids_fit_i32(ids) {
let mut buf = Vec::with_capacity(ids.len() * 4);
for id in ids {
buf.extend_from_slice(&id.to_le_bytes());
}
return serializer.serialize_bytes(&buf);
}
ids.serialize(serializer)
}
fn deserialize_token_ids<'de, D>(deserializer: D) -> Result<Arc<Vec<TokenIdType>>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TokenIdsVisitor;
impl<'de> serde::de::Visitor<'de> for TokenIdsVisitor {
type Value = Vec<TokenIdType>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a sequence of token ids or packed little-endian int32 bytes")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut out =
Vec::with_capacity(seq.size_hint().unwrap_or(0).min(TOKEN_IDS_PREALLOC_CAP));
while let Some(id) = seq.next_element::<TokenIdType>()? {
out.push(id);
}
Ok(out)
}
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if !v.len().is_multiple_of(4) {
return Err(E::custom(
"packed token_ids byte length is not a multiple of 4",
));
}
Ok(v.chunks_exact(4)
.map(|c| TokenIdType::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_bytes(&v)
}
}
deserializer.deserialize_any(TokenIdsVisitor).map(Arc::new)
}
fn deserialize_optional_object<'de, D>(
deserializer: D,
) -> Result<Option<serde_json::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
if let Some(v) = &value
&& !v.is_object()
{
return Err(serde::de::Error::custom(
"encoder_result must be a JSON object",
));
}
Ok(value)
}
impl PreprocessedRequest {
pub fn phase(&self) -> RequestPhase {
self.tracker
.as_ref()
.map(|tracker| tracker.phase())
.unwrap_or_default()
}
pub fn has_annotation(&self, annotation: &str) -> bool {
self.annotations.contains(&annotation.to_string())
}
pub fn get_annotation_value(&self, key: &str) -> Option<String> {
let prefix = format!("{}:", key);
self.annotations
.iter()
.find(|a| a.starts_with(&prefix))
.map(|a| a[prefix.len()..].to_string())
}
pub fn builder() -> PreprocessedRequestBuilder {
PreprocessedRequestBuilder::default()
}
pub fn routing_mut(&mut self) -> &mut RoutingHints {
self.routing.get_or_insert_with(RoutingHints::default)
}
pub fn block_mm_routing_info(&self) -> (&[TokenIdType], Option<&[Option<BlockExtraInfo>]>) {
let Some(mm) = self.mm_routing_info.as_ref() else {
return (&self.token_ids, None);
};
let tokens = mm.routing_token_ids.as_slice();
if tokens.is_empty() {
return (&self.token_ids, None);
}
(tokens, Some(mm.block_mm_infos.as_slice()))
}
pub(crate) fn expanded_prompt_token_count(&self) -> usize {
self.mm_routing_info
.as_ref()
.filter(|mm| !mm.routing_token_ids.is_empty() && mm.expanded_prompt_len > 0)
.map_or(self.token_ids.len(), |mm| mm.expanded_prompt_len)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
pub struct PreprocessedEmbeddingRequest {
pub token_ids: Vec<Vec<TokenIdType>>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
pub truncate_prompt_tokens: Option<i64>,
pub dimensions: Option<u32>,
#[builder(default)]
pub mdc_sum: Option<String>,
#[builder(default)]
pub annotations: Vec<String>,
}
impl PreprocessedEmbeddingRequest {
pub fn has_annotation(&self, annotation: &str) -> bool {
self.annotations.contains(&annotation.to_string())
}
}
impl PreprocessedEmbeddingRequest {
pub fn builder() -> PreprocessedEmbeddingRequestBuilder {
PreprocessedEmbeddingRequestBuilder::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(serde::Deserialize)]
struct TokenIdsOnly {
#[serde(deserialize_with = "deserialize_token_ids")]
token_ids: Arc<Vec<TokenIdType>>,
}
struct Bytes<'a>(&'a [u8]);
impl serde::Serialize for Bytes<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(self.0)
}
}
struct PackedTokenIds<'a>(&'a [u8]);
impl serde::Serialize for PackedTokenIds<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut st = serializer.serialize_struct("TokenIdsOnly", 1)?;
st.serialize_field("token_ids", &Bytes(self.0))?;
st.end()
}
}
fn le_bytes(ids: &[TokenIdType]) -> Vec<u8> {
ids.iter().flat_map(|id| id.to_le_bytes()).collect()
}
#[test]
fn packed_token_ids_deserialize_from_msgpack_bytes() {
let ids = vec![0u32, 1, 128_000, i32::MAX as u32];
let payload = rmp_serde::to_vec_named(&PackedTokenIds(&le_bytes(&ids))).unwrap();
let decoded: TokenIdsOnly = rmp_serde::from_slice(&payload).unwrap();
assert_eq!(*decoded.token_ids, ids);
}
#[test]
fn packed_token_ids_reject_odd_byte_length() {
let payload = rmp_serde::to_vec_named(&PackedTokenIds(&[1, 0, 0])).unwrap();
assert!(rmp_serde::from_slice::<TokenIdsOnly>(&payload).is_err());
}
#[test]
fn sequence_token_ids_still_deserialize() {
#[derive(serde::Serialize)]
struct Seq {
token_ids: Vec<TokenIdType>,
}
let ids = vec![7u32, 8, 9];
let payload = rmp_serde::to_vec_named(&Seq {
token_ids: ids.clone(),
})
.unwrap();
let decoded: TokenIdsOnly = rmp_serde::from_slice(&payload).unwrap();
assert_eq!(*decoded.token_ids, ids);
}
struct TokenIdsField<'a> {
ids: &'a [TokenIdType],
packed: bool,
}
impl serde::Serialize for TokenIdsField<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serialize_token_ids_with(self.ids, serializer, self.packed)
}
}
struct Request<'a> {
ids: &'a [TokenIdType],
packed: bool,
}
impl serde::Serialize for Request<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut st = serializer.serialize_struct("TokenIdsOnly", 1)?;
st.serialize_field(
"token_ids",
&TokenIdsField {
ids: self.ids,
packed: self.packed,
},
)?;
st.end()
}
}
fn encode(ids: &[TokenIdType], packed: bool) -> Vec<u8> {
rmp_serde::to_vec_named(&Request { ids, packed }).unwrap()
}
const VALUE_OFFSET: usize = 1 + 1 + "token_ids".len();
#[test]
fn packed_serialization_emits_int32_bytes_that_round_trip() {
let ids = vec![0u32, 1, 128_000, i32::MAX as u32];
let payload = encode(&ids, true);
assert_eq!(payload[VALUE_OFFSET], 0xc4, "msgpack bin8 marker");
assert_eq!(&payload[VALUE_OFFSET + 2..], le_bytes(&ids).as_slice());
let decoded: TokenIdsOnly = rmp_serde::from_slice(&payload).unwrap();
assert_eq!(*decoded.token_ids, ids);
}
#[test]
fn ids_above_i32_max_keep_the_sequence_form() {
let ids = vec![7u32, i32::MAX as u32 + 1];
let payload = encode(&ids, true);
assert_eq!(payload[VALUE_OFFSET], 0x92, "msgpack fixarray(2), not bin");
assert_eq!(payload, encode(&ids, false));
let decoded: TokenIdsOnly = rmp_serde::from_slice(&payload).unwrap();
assert_eq!(*decoded.token_ids, ids);
}
#[test]
fn packed_flag_leaves_json_as_a_sequence() {
let ids = vec![1u32, 2];
let json = serde_json::to_string(&Request {
ids: &ids,
packed: true,
})
.unwrap();
assert_eq!(json, r#"{"token_ids":[1,2]}"#);
}
fn request_with_tokens(token_ids: Vec<TokenIdType>) -> PreprocessedRequest {
PreprocessedRequest::builder()
.model("test-model".to_string())
.token_ids(token_ids)
.stop_conditions(StopConditions::default())
.sampling_options(SamplingOptions::default())
.output_options(OutputOptions::default())
.build()
.expect("valid request")
}
#[test]
fn clone_shares_token_storage_until_mutated() {
let request = request_with_tokens(vec![1, 2, 3]);
let mut cloned = request.clone();
assert!(Arc::ptr_eq(&request.token_ids, &cloned.token_ids));
Arc::make_mut(&mut cloned.token_ids).push(4);
assert!(!Arc::ptr_eq(&request.token_ids, &cloned.token_ids));
assert_eq!(request.token_ids.as_slice(), &[1, 2, 3]);
assert_eq!(cloned.token_ids.as_slice(), &[1, 2, 3, 4]);
}
#[test]
fn shared_tokens_preserve_json_wire_format() {
let request = request_with_tokens(vec![11, 22, 33]);
let json = serde_json::to_string(&request).expect("serializes");
assert!(json.contains(r#""token_ids":[11,22,33]"#), "{json}");
let decoded: PreprocessedRequest = serde_json::from_str(&json).expect("deserializes");
assert_eq!(decoded.token_ids.as_slice(), &[11, 22, 33]);
}
#[test]
fn embedding_encoding_format_serde_omits_none() {
let mut request = PreprocessedEmbeddingRequest {
token_ids: vec![vec![1, 2, 3]],
model: "test-model".to_string(),
encoding_format: None,
truncate_prompt_tokens: None,
dimensions: None,
mdc_sum: None,
annotations: Vec::new(),
};
let omitted = serde_json::to_value(&request).unwrap();
assert!(omitted.get("encoding_format").is_none());
assert!(omitted.get("truncate_prompt_tokens").is_none());
let round_trip: PreprocessedEmbeddingRequest = serde_json::from_value(omitted).unwrap();
assert!(round_trip.encoding_format.is_none());
assert!(round_trip.truncate_prompt_tokens.is_none());
request.encoding_format = Some("float".to_string());
request.truncate_prompt_tokens = Some(-1);
let explicit = serde_json::to_value(&request).unwrap();
assert_eq!(explicit["encoding_format"], "float");
assert_eq!(explicit["truncate_prompt_tokens"], -1);
}
#[test]
fn bootstrap_info_carries_only_stable_handoff_identity() {
let handoff_id = Uuid::from_u128(42);
let info = BootstrapInfo {
bootstrap_host: "127.0.0.1".to_string(),
bootstrap_port: 1234,
bootstrap_room: 7,
handoff_id: Some(handoff_id),
};
let value = serde_json::to_value(&info).unwrap();
assert_eq!(value["handoff_id"], handoff_id.to_string());
assert!(value.get("mocker_handoff_protocol_version").is_none());
assert!(value.get("mocker_handoff_role").is_none());
assert!(value.get("mocker_handoff_engine_type").is_none());
assert_eq!(
serde_json::from_value::<BootstrapInfo>(value)
.unwrap()
.handoff_id,
Some(handoff_id)
);
}
#[test]
fn is_probe_serde_round_trip() {
let mut req = PreprocessedRequest::builder()
.model("t".to_string())
.token_ids(vec![1])
.stop_conditions(StopConditions::default())
.sampling_options(SamplingOptions::default())
.output_options(OutputOptions::default())
.build()
.unwrap();
assert!(!req.is_probe);
let normal = serde_json::to_string(&req).unwrap();
assert!(!normal.contains("_HEALTH_CHECK"), "got: {normal}");
let back: PreprocessedRequest = serde_json::from_str(&normal).unwrap();
assert!(!back.is_probe);
req.is_probe = true;
let probe = serde_json::to_string(&req).unwrap();
assert!(probe.contains("\"_HEALTH_CHECK\":true"), "got: {probe}");
let back: PreprocessedRequest = serde_json::from_str(&probe).unwrap();
assert!(back.is_probe);
}
#[test]
fn require_reasoning_serde_round_trip() {
let mut req = PreprocessedRequest::builder()
.model("t".to_string())
.token_ids(vec![1])
.stop_conditions(StopConditions::default())
.sampling_options(SamplingOptions::default())
.output_options(OutputOptions::default())
.build()
.unwrap();
let normal = serde_json::to_value(&req).unwrap();
assert!(
!normal
.as_object()
.unwrap()
.contains_key("require_reasoning")
);
let back: PreprocessedRequest = serde_json::from_value(normal).unwrap();
assert!(!back.require_reasoning);
req.require_reasoning = true;
let guided = serde_json::to_value(&req).unwrap();
assert_eq!(guided["require_reasoning"], true);
let back: PreprocessedRequest = serde_json::from_value(guided).unwrap();
assert!(back.require_reasoning);
}
#[test]
fn disabled_reasoning_with_thinking_budget_serde_round_trip() {
let req = PreprocessedRequest::builder()
.model("t".to_string())
.token_ids(vec![1])
.stop_conditions(StopConditions {
max_thinking_tokens: Some(0),
..Default::default()
})
.sampling_options(SamplingOptions::default())
.output_options(OutputOptions::default())
.require_reasoning(false)
.build()
.unwrap();
let value = serde_json::to_value(&req).unwrap();
assert!(value.get("require_reasoning").is_none());
assert_eq!(value["stop_conditions"]["max_thinking_tokens"], 0);
let back: PreprocessedRequest = serde_json::from_value(value).unwrap();
assert!(!back.require_reasoning);
assert_eq!(back.stop_conditions.max_thinking_tokens, Some(0));
}
#[test]
fn minimal_canary_payload_deserializes() {
let req: PreprocessedRequest = serde_json::from_value(serde_json::json!({
"token_ids": [1],
"_HEALTH_CHECK": true,
}))
.unwrap();
assert_eq!(req.token_ids.as_slice(), &[1]);
assert!(req.is_probe);
assert_eq!(req.model, "");
}
#[test]
fn encoder_result_round_trips_through_serde() {
let payload = serde_json::json!({
"embedding_handle": {
"shape": [1, 1024],
"dtype": "fp16",
"uri": "nixl://encoder-0/embedding-42",
},
"processed_token_ids": [128_000_u32, 200_001_u32, 200_002_u32],
});
let req = PreprocessedRequest::builder()
.model("test/model".to_string())
.token_ids(vec![1, 2, 3])
.stop_conditions(StopConditions::default())
.sampling_options(SamplingOptions::default())
.output_options(OutputOptions::default())
.encoder_result(Some(payload.clone()))
.build()
.unwrap();
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["encoder_result"], payload);
let back: PreprocessedRequest = serde_json::from_value(json).unwrap();
assert_eq!(back.encoder_result, Some(payload));
}
#[test]
fn encoder_result_is_absent_when_none() {
let req = PreprocessedRequest::builder()
.model("test/model".to_string())
.token_ids(vec![1, 2, 3])
.stop_conditions(StopConditions::default())
.sampling_options(SamplingOptions::default())
.output_options(OutputOptions::default())
.build()
.unwrap();
assert!(req.encoder_result.is_none());
let json = serde_json::to_value(&req).unwrap();
assert!(
!json.as_object().unwrap().contains_key("encoder_result"),
"encoder_result must be absent from wire when None; got {json}"
);
}
#[test]
fn routing_hints_cache_namespace_serializes_as_cache_salt() {
let hints = RoutingHints {
cache_namespace: Some("tenant-a".to_string()),
..Default::default()
};
let value = serde_json::to_value(&hints).unwrap();
assert_eq!(value["cache_salt"], "tenant-a");
assert!(value.get("cache_namespace").is_none());
let decoded: RoutingHints = serde_json::from_value(value).unwrap();
assert_eq!(decoded.cache_namespace.as_deref(), Some("tenant-a"));
}
}