use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RerankModel {
#[default]
Rerank,
}
#[derive(Clone, Serialize)]
pub struct RerankBody {
pub model: RerankModel,
pub query: String,
pub documents: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_n: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_documents: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_raw_scores: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}
impl std::fmt::Debug for RerankBody {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RerankBody")
.field("model", &self.model)
.field("query", &"[REDACTED]")
.field("documents_len", &self.documents.len())
.field("top_n", &self.top_n)
.field("return_documents", &self.return_documents)
.field("return_raw_scores", &self.return_raw_scores)
.field(
"request_id",
&self.request_id.as_ref().map(|_| "[REDACTED]"),
)
.field("user_id", &self.user_id.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}
impl RerankBody {
pub fn new(model: RerankModel, query: impl Into<String>, documents: Vec<String>) -> Self {
Self {
model,
query: query.into(),
documents,
top_n: None,
return_documents: None,
return_raw_scores: None,
request_id: None,
user_id: None,
}
}
pub fn with_top_n(mut self, n: usize) -> Self {
self.top_n = Some(n);
self
}
pub fn with_return_documents(mut self, v: bool) -> Self {
self.return_documents = Some(v);
self
}
pub fn with_return_raw_scores(mut self, v: bool) -> Self {
self.return_raw_scores = Some(v);
self
}
pub fn with_request_id(mut self, v: impl Into<String>) -> Self {
self.request_id = Some(v.into());
self
}
pub fn with_user_id(mut self, v: impl Into<String>) -> Self {
self.user_id = Some(v.into());
self
}
pub fn validate_constraints(&self) -> crate::ZaiResult<()> {
if self.query.trim().is_empty() {
return Err(crate::client::validation::invalid(
"query must not be empty",
));
}
if self.query.chars().count() > 4096 {
return Err(crate::client::validation::invalid(
"query length exceeds 4096 characters",
));
}
if self.documents.is_empty() {
return Err(crate::client::validation::invalid(
"documents must not be empty",
));
}
if self.documents.len() > 128 {
return Err(crate::client::validation::invalid(
"documents length exceeds 128",
));
}
for (i, d) in self.documents.iter().enumerate() {
if d.trim().is_empty() {
return Err(crate::client::validation::invalid(format!(
"document at index {i} must not be empty"
)));
}
if d.chars().count() > 4096 {
return Err(crate::client::validation::invalid(format!(
"document at index {i} exceeds 4096 characters"
)));
}
}
if let Some(n) = self.top_n
&& n > self.documents.len()
{
return Err(crate::client::validation::invalid(
"top_n cannot exceed documents length",
));
}
if let Some(request_id) = self.request_id.as_deref()
&& !(6..=64).contains(&request_id.chars().count())
{
return Err(crate::client::validation::invalid(
"request_id must contain between 6 and 64 characters",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_rejects_blank_values_and_accepts_zero_top_n() {
assert!(
RerankBody::new(RerankModel::Rerank, " ", vec!["doc".into()])
.validate_constraints()
.is_err()
);
assert!(
RerankBody::new(RerankModel::Rerank, "query", vec![" ".into()])
.validate_constraints()
.is_err()
);
assert!(
RerankBody::new(RerankModel::Rerank, "query", vec!["doc".into()])
.with_top_n(0)
.validate_constraints()
.is_ok()
);
}
#[test]
fn debug_redacts_query_documents_and_identifiers() {
let body = RerankBody::new(
RerankModel::Rerank,
"private query",
vec!["private document".to_owned()],
)
.with_request_id("private-request")
.with_user_id("private-user");
let debug = format!("{body:?}");
for secret in [
"private query",
"private document",
"private-request",
"private-user",
] {
assert!(!debug.contains(secret));
}
}
}