use serde::{Deserialize, Serialize};
pub use crate::embed::cache::key::CacheKey;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BatchId(pub u64);
impl BatchId {
pub fn new(id: u64) -> Self {
Self(id)
}
}
impl std::fmt::Display for BatchId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "batch-{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrameHeader {
pub batch_id: BatchId,
pub msg_type: MsgType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MsgType {
EmbedRequest,
EmbedResponse,
RerankRequest,
RerankResponse,
Error,
HealthRequest,
HealthResponse,
CacheProbe,
CacheProbeResponse,
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
pub header: FrameHeader,
pub payload: Vec<u8>,
}
impl Frame {
pub fn new<T: Serialize>(header: FrameHeader, payload: &T) -> anyhow::Result<Self> {
let payload_bytes = bincode::serialize(payload)?;
Ok(Self {
header,
payload: payload_bytes,
})
}
pub fn decode_payload<T: for<'de> Deserialize<'de>>(&self) -> anyhow::Result<T> {
Ok(bincode::deserialize(&self.payload)?)
}
pub fn encode_wire(&self) -> anyhow::Result<Vec<u8>> {
let frame_bytes = bincode::serialize(self)?;
let len = u32::try_from(frame_bytes.len()).map_err(|_| {
anyhow::anyhow!(
"frame payload too large: {} bytes exceeds u32::MAX",
frame_bytes.len()
)
})?;
let mut wire = Vec::with_capacity(4 + frame_bytes.len());
wire.extend_from_slice(&len.to_le_bytes());
wire.extend_from_slice(&frame_bytes);
Ok(wire)
}
pub fn from_wire_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
Ok(bincode::deserialize(bytes)?)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbedRequest {
pub texts: Vec<String>,
pub expected_dim: usize,
#[serde(default)]
pub cache_keys: Vec<CacheKey>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbedResponse {
pub vectors: Vec<f32>,
pub count: usize,
pub dimension: usize,
}
impl EmbedResponse {
pub fn new(vectors: Vec<f32>, count: usize, dimension: usize) -> Self {
Self::try_new(vectors, count, dimension)
.expect("EmbedResponse vectors must equal count * dimension")
}
pub fn try_new(vectors: Vec<f32>, count: usize, dimension: usize) -> Result<Self, String> {
let expected = count
.checked_mul(dimension)
.ok_or_else(|| "embedding response size overflow".to_string())?;
if vectors.len() != expected {
return Err(format!(
"embedding response length mismatch: got {}, expected {} ({} rows x {} dim)",
vectors.len(),
expected,
count,
dimension
));
}
Ok(Self {
vectors,
count,
dimension,
})
}
pub fn get_embedding(&self, index: usize) -> Option<&[f32]> {
if index >= self.count {
return None;
}
let start = index * self.dimension;
let end = start + self.dimension;
Some(&self.vectors[start..end])
}
pub fn into_vectors(self) -> Vec<Vec<f32>> {
let dim = self.dimension;
self.vectors
.chunks_exact(dim)
.map(|chunk| chunk.to_vec())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankRequest {
pub query: String,
pub documents: Vec<RerankDocument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankDocument {
pub id: String,
pub content: String,
pub initial_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankResponse {
pub results: Vec<RerankResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HealthRequest;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerState {
Initializing,
Ready,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthResponse {
pub state: WorkerState,
pub phase: String,
pub started_unix_ms: u64,
pub provider: Option<String>,
pub model: String,
pub error: Option<String>,
#[serde(default)]
pub model_digest: Option<[u8; 32]>,
#[serde(default)]
pub tokenizer_digest: Option<[u8; 32]>,
#[serde(default)]
pub config_digest: Option<[u8; 32]>,
#[serde(default)]
pub host_rss_mib: Option<u64>,
#[serde(default)]
pub gpu_vram_mib: Option<u64>,
#[serde(default)]
pub provider_compile_cache: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RerankResult {
pub id: String,
pub original_score: f32,
pub rerank_score: f32,
pub combined_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheProbeRequest {
pub keys: Vec<CacheKey>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheProbeResponse {
pub hit_indices: Vec<usize>,
pub miss_indices: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelRequest {
#[serde(default)]
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelResponse {
pub acknowledged: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Request {
Embed(EmbedRequest),
Rerank(RerankRequest),
Health(HealthRequest),
CacheProbe(CacheProbeRequest),
Cancel(CancelRequest),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Response {
Embed(EmbedResponse),
Rerank(RerankResponse),
Error(WorkerError),
Health(Box<HealthResponse>),
CacheProbe(CacheProbeResponse),
Cancel(CancelResponse),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerError {
pub kind: ErrorKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorKind {
Initializing,
OnnxRuntime,
ModelNotFound,
Tokenizer,
Inference,
InvalidRequest,
Internal,
FrameTooLarge,
}
impl std::fmt::Display for WorkerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}: {}", self.kind, self.message)
}
}
impl std::error::Error for WorkerError {}
pub fn embed_request_frame(batch_id: BatchId, request: EmbedRequest) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::EmbedRequest,
},
&Request::Embed(request),
)
}
pub fn embed_response_frame(batch_id: BatchId, response: EmbedResponse) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::EmbedResponse,
},
&Response::Embed(response),
)
}
pub fn rerank_request_frame(batch_id: BatchId, request: RerankRequest) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::RerankRequest,
},
&Request::Rerank(request),
)
}
pub fn rerank_response_frame(batch_id: BatchId, response: RerankResponse) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::RerankResponse,
},
&Response::Rerank(response),
)
}
pub fn error_frame(batch_id: BatchId, error: WorkerError) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::Error,
},
&Response::Error(error),
)
}
pub fn health_request_frame(batch_id: BatchId) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::HealthRequest,
},
&Request::Health(HealthRequest),
)
}
pub fn health_response_frame(batch_id: BatchId, response: HealthResponse) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::HealthResponse,
},
&Response::Health(Box::new(response)),
)
}
pub fn cache_probe_request_frame(
batch_id: BatchId,
request: CacheProbeRequest,
) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::CacheProbe,
},
&Request::CacheProbe(request),
)
}
pub fn cache_probe_response_frame(
batch_id: BatchId,
response: CacheProbeResponse,
) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::CacheProbeResponse,
},
&Response::CacheProbe(response),
)
}
pub fn cancel_request_frame(batch_id: BatchId, request: CancelRequest) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::Cancel,
},
&Request::Cancel(request),
)
}
pub fn cancel_response_frame(batch_id: BatchId, response: CancelResponse) -> anyhow::Result<Frame> {
Frame::new(
FrameHeader {
batch_id,
msg_type: MsgType::Cancel,
},
&Response::Cancel(response),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_id_display() {
let id = BatchId::new(42);
assert_eq!(format!("{}", id), "batch-42");
}
#[test]
fn test_batch_id_equality() {
assert_eq!(BatchId::new(7), BatchId::new(7));
assert_ne!(BatchId::new(7), BatchId::new(8));
}
#[test]
fn test_embed_response_get_embedding() {
let resp = EmbedResponse::new(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
2, 3, );
assert_eq!(resp.get_embedding(0), Some(&[1.0, 2.0, 3.0][..]));
assert_eq!(resp.get_embedding(1), Some(&[4.0, 5.0, 6.0][..]));
assert_eq!(resp.get_embedding(2), None);
}
#[test]
fn test_embed_response_into_vectors() {
let resp = EmbedResponse::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3);
let vecs = resp.into_vectors();
assert_eq!(vecs.len(), 2);
assert_eq!(vecs[0], vec![1.0, 2.0, 3.0]);
assert_eq!(vecs[1], vec![4.0, 5.0, 6.0]);
}
#[test]
fn test_frame_roundtrip_embed_request() {
let batch_id = BatchId::new(123);
let request = EmbedRequest {
texts: vec!["hello world".to_string(), "foo bar".to_string()],
expected_dim: 1024,
cache_keys: vec![],
};
let frame = embed_request_frame(batch_id, request.clone()).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded_frame = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded_frame.header.batch_id, batch_id);
assert_eq!(decoded_frame.header.msg_type, MsgType::EmbedRequest);
let decoded_request: Request = decoded_frame.decode_payload().unwrap();
match decoded_request {
Request::Embed(embed_req) => {
assert_eq!(embed_req.texts, request.texts);
assert_eq!(embed_req.expected_dim, request.expected_dim);
}
_ => panic!("Expected Embed request"),
}
}
#[test]
fn test_frame_roundtrip_embed_response() {
let batch_id = BatchId::new(456);
let response = EmbedResponse::new(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], 2, 3);
let frame = embed_response_frame(batch_id, response.clone()).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded_frame = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded_frame.header.batch_id, batch_id);
assert_eq!(decoded_frame.header.msg_type, MsgType::EmbedResponse);
let decoded_response: Response = decoded_frame.decode_payload().unwrap();
match decoded_response {
Response::Embed(embed_resp) => {
assert_eq!(embed_resp.vectors, response.vectors);
assert_eq!(embed_resp.count, response.count);
assert_eq!(embed_resp.dimension, response.dimension);
}
_ => panic!("Expected Embed response"),
}
}
#[test]
fn test_frame_roundtrip_rerank_request() {
let batch_id = BatchId::new(789);
let request = RerankRequest {
query: "test query".to_string(),
documents: vec![
RerankDocument {
id: "doc1".to_string(),
content: "first doc".to_string(),
initial_score: 0.9,
},
RerankDocument {
id: "doc2".to_string(),
content: "second doc".to_string(),
initial_score: 0.7,
},
],
};
let frame = rerank_request_frame(batch_id, request.clone()).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded_frame = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded_frame.header.batch_id, batch_id);
assert_eq!(decoded_frame.header.msg_type, MsgType::RerankRequest);
let decoded_request: Request = decoded_frame.decode_payload().unwrap();
match decoded_request {
Request::Rerank(rerank_req) => {
assert_eq!(rerank_req.query, request.query);
assert_eq!(rerank_req.documents.len(), 2);
assert_eq!(rerank_req.documents[0].id, "doc1");
assert_eq!(rerank_req.documents[1].id, "doc2");
}
_ => panic!("Expected Rerank request"),
}
}
#[test]
fn test_frame_roundtrip_error() {
let batch_id = BatchId::new(999);
let error = WorkerError {
kind: ErrorKind::ModelNotFound,
message: "model file missing".to_string(),
};
let frame = error_frame(batch_id, error.clone()).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded_frame = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded_frame.header.batch_id, batch_id);
assert_eq!(decoded_frame.header.msg_type, MsgType::Error);
let decoded_response: Response = decoded_frame.decode_payload().unwrap();
match decoded_response {
Response::Error(err) => {
assert_eq!(err.kind, ErrorKind::ModelNotFound);
assert_eq!(err.message, "model file missing");
}
_ => panic!("Expected Error response"),
}
}
#[test]
fn test_batch_id_preserved_through_wire() {
let original_id = BatchId::new(0xDEADBEEF);
let request = EmbedRequest {
texts: vec!["test".to_string()],
expected_dim: 4,
cache_keys: vec![],
};
let frame = embed_request_frame(original_id, request).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded.header.batch_id, original_id);
}
#[test]
fn test_payload_ordering_preserved() {
let texts: Vec<String> = (0..10).map(|i| format!("text {}", i)).collect();
let request = EmbedRequest {
texts: texts.clone(),
expected_dim: 4,
cache_keys: vec![],
};
let frame = embed_request_frame(BatchId::new(1), request).unwrap();
let decoded: Request = frame.decode_payload().unwrap();
match decoded {
Request::Embed(embed_req) => {
assert_eq!(embed_req.texts, texts);
}
_ => panic!("Expected Embed request"),
}
}
#[test]
fn test_empty_batch_roundtrip() {
let request = EmbedRequest {
texts: vec![],
expected_dim: 1024,
cache_keys: vec![],
};
let frame = embed_request_frame(BatchId::new(0), request).unwrap();
let decoded: Request = frame.decode_payload().unwrap();
match decoded {
Request::Embed(embed_req) => {
assert!(embed_req.texts.is_empty());
}
_ => panic!("Expected Embed request"),
}
}
#[test]
fn test_health_roundtrip_preserves_lifecycle_state() {
let batch_id = BatchId::new(77);
let health = HealthResponse {
state: WorkerState::Initializing,
phase: "initializing".to_string(),
started_unix_ms: 1234,
provider: Some("migraphx".to_string()),
model: "qwen3-embed-0.6b".to_string(),
error: None,
model_digest: None,
tokenizer_digest: None,
config_digest: None,
host_rss_mib: None,
gpu_vram_mib: None,
provider_compile_cache: None,
};
let request = health_request_frame(batch_id).unwrap();
let decoded_request: Request = request.decode_payload().unwrap();
assert!(matches!(decoded_request, Request::Health(HealthRequest)));
let response = health_response_frame(batch_id, health.clone()).unwrap();
assert_eq!(response.header.msg_type, MsgType::HealthResponse);
let decoded: Response = response.decode_payload().unwrap();
match decoded {
Response::Health(actual) => assert_eq!(actual.state, health.state),
_ => panic!("expected health response"),
}
}
#[test]
fn test_health_response_carries_digests_and_memory() {
let health = HealthResponse {
state: WorkerState::Ready,
phase: "ready".to_string(),
started_unix_ms: 999,
provider: Some("migraphx".to_string()),
model: "qwen3-embed-0.6b".to_string(),
error: None,
model_digest: Some([0xAA; 32]),
tokenizer_digest: Some([0xBB; 32]),
config_digest: Some([0xCC; 32]),
host_rss_mib: Some(350),
gpu_vram_mib: Some(512),
provider_compile_cache: Some("/tmp/migraphx-cache".to_string()),
};
let frame = health_response_frame(BatchId::new(55), health.clone()).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded_frame = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded_frame.header.msg_type, MsgType::HealthResponse);
let decoded: Response = decoded_frame.decode_payload().unwrap();
match decoded {
Response::Health(actual) => {
assert_eq!(actual.model_digest, Some([0xAA; 32]));
assert_eq!(actual.tokenizer_digest, Some([0xBB; 32]));
assert_eq!(actual.config_digest, Some([0xCC; 32]));
assert_eq!(actual.host_rss_mib, Some(350));
assert_eq!(actual.gpu_vram_mib, Some(512));
assert_eq!(
actual.provider_compile_cache,
Some("/tmp/migraphx-cache".to_string())
);
}
_ => panic!("expected health response"),
}
}
#[test]
fn test_health_response_new_fields_default_to_none() {
let health = HealthResponse {
state: WorkerState::Ready,
phase: "ready".to_string(),
started_unix_ms: 0,
provider: Some("cpu".to_string()),
model: "test".to_string(),
error: None,
model_digest: None,
tokenizer_digest: None,
config_digest: None,
host_rss_mib: None,
gpu_vram_mib: None,
provider_compile_cache: None,
};
assert!(health.model_digest.is_none());
assert!(health.tokenizer_digest.is_none());
assert!(health.config_digest.is_none());
assert!(health.host_rss_mib.is_none());
assert!(health.gpu_vram_mib.is_none());
assert!(health.provider_compile_cache.is_none());
}
#[test]
fn test_embed_request_with_cache_keys_roundtrip() {
use crate::embed::cache::key::{CacheKey, Normalization, Pooling};
let key1 = CacheKey {
model_digest: CacheKey::model_digest(b"model"),
tokenizer_digest: CacheKey::tokenizer_digest(b"tok"),
prompt_role_and_version: 0,
pooling: Pooling::Mean,
normalization: Normalization::L2,
output_dimensions: 1024,
content_hash: CacheKey::content_hash("hello"),
};
let key2 = CacheKey {
content_hash: CacheKey::content_hash("world"),
..key1.clone()
};
let request = EmbedRequest {
texts: vec!["hello".to_string(), "world".to_string()],
expected_dim: 1024,
cache_keys: vec![key1, key2],
};
let frame = embed_request_frame(BatchId::new(1), request).unwrap();
let decoded: Request = frame.decode_payload().unwrap();
match decoded {
Request::Embed(embed_req) => {
assert_eq!(embed_req.texts.len(), 2);
assert_eq!(embed_req.cache_keys.len(), 2);
assert_eq!(
embed_req.cache_keys[0].content_hash,
CacheKey::content_hash("hello")
);
assert_eq!(
embed_req.cache_keys[1].content_hash,
CacheKey::content_hash("world")
);
}
_ => panic!("expected embed request"),
}
}
#[test]
fn test_embed_request_without_cache_keys_defaults_empty() {
let request = EmbedRequest {
texts: vec!["hello".to_string()],
expected_dim: 4,
cache_keys: vec![],
};
let frame = embed_request_frame(BatchId::new(1), request).unwrap();
let decoded: Request = frame.decode_payload().unwrap();
match decoded {
Request::Embed(embed_req) => {
assert!(embed_req.cache_keys.is_empty());
}
_ => panic!("expected embed request"),
}
}
#[test]
fn test_cache_probe_request_response_roundtrip() {
use crate::embed::cache::key::{CacheKey, Normalization, Pooling};
let key = CacheKey {
model_digest: CacheKey::model_digest(b"m"),
tokenizer_digest: CacheKey::tokenizer_digest(b"t"),
prompt_role_and_version: 0,
pooling: Pooling::Cls,
normalization: Normalization::None,
output_dimensions: 768,
content_hash: CacheKey::content_hash("text"),
};
let probe_req = CacheProbeRequest {
keys: vec![key.clone(); 3],
};
let frame = cache_probe_request_frame(BatchId::new(10), probe_req).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded_frame = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded_frame.header.msg_type, MsgType::CacheProbe);
let decoded_req: Request = decoded_frame.decode_payload().unwrap();
match decoded_req {
Request::CacheProbe(req) => assert_eq!(req.keys.len(), 3),
_ => panic!("expected cache probe request"),
}
let probe_resp = CacheProbeResponse {
hit_indices: vec![0, 2],
miss_indices: vec![1],
};
let resp_frame = cache_probe_response_frame(BatchId::new(10), probe_resp).unwrap();
let resp_wire = resp_frame.encode_wire().unwrap();
let decoded_resp_frame = Frame::from_wire_bytes(&resp_wire[4..]).unwrap();
assert_eq!(
decoded_resp_frame.header.msg_type,
MsgType::CacheProbeResponse
);
let decoded_resp: Response = decoded_resp_frame.decode_payload().unwrap();
match decoded_resp {
Response::CacheProbe(resp) => {
assert_eq!(resp.hit_indices, vec![0, 2]);
assert_eq!(resp.miss_indices, vec![1]);
}
_ => panic!("expected cache probe response"),
}
}
#[test]
fn test_cancel_request_response_roundtrip() {
let cancel_req = CancelRequest {
reason: "index cancelled by scheduler".to_string(),
};
let frame = cancel_request_frame(BatchId::new(5), cancel_req).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded_frame = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded_frame.header.msg_type, MsgType::Cancel);
let decoded_req: Request = decoded_frame.decode_payload().unwrap();
match decoded_req {
Request::Cancel(req) => {
assert_eq!(req.reason, "index cancelled by scheduler");
}
_ => panic!("expected cancel request"),
}
let cancel_resp = CancelResponse { acknowledged: true };
let resp_frame = cancel_response_frame(BatchId::new(5), cancel_resp).unwrap();
let resp_wire = resp_frame.encode_wire().unwrap();
let decoded_resp_frame = Frame::from_wire_bytes(&resp_wire[4..]).unwrap();
let decoded_resp: Response = decoded_resp_frame.decode_payload().unwrap();
match decoded_resp {
Response::Cancel(resp) => assert!(resp.acknowledged),
_ => panic!("expected cancel response"),
}
}
}