use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TokenizerModel {
#[serde(rename = "glm-4.6v")]
#[default]
Glm46V,
#[serde(rename = "glm-4.6")]
Glm46,
#[serde(rename = "glm-4.5")]
Glm45,
#[serde(rename = "glm-4.5-air")]
Glm45Air,
#[serde(rename = "glm-4-0520")]
Glm40520,
#[serde(rename = "glm-4-long")]
Glm4Long,
#[serde(rename = "glm-4-air")]
Glm4Air,
#[serde(rename = "glm-4-flash")]
Glm4Flash,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum TokenizerMessage {
User {
content: String,
},
System {
content: String,
},
Assistant {
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
},
}
impl std::fmt::Debug for TokenizerMessage {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (role, configured) = match self {
Self::User { .. } => ("user", true),
Self::System { .. } => ("system", true),
Self::Assistant { content } => ("assistant", content.is_some()),
};
formatter
.debug_struct("TokenizerMessage")
.field("role", &role)
.field("content_configured", &configured)
.finish()
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct TokenizerBody {
pub model: TokenizerModel,
pub messages: Vec<TokenizerMessage>,
#[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 TokenizerBody {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TokenizerBody")
.field("model", &self.model)
.field("messages_len", &self.messages.len())
.field(
"request_id",
&self.request_id.as_ref().map(|_| "[REDACTED]"),
)
.field("user_id", &self.user_id.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}
impl TokenizerBody {
pub fn new(model: TokenizerModel, messages: Vec<TokenizerMessage>) -> Self {
Self {
model,
messages,
request_id: None,
user_id: None,
}
}
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(&self) -> crate::ZaiResult<()> {
if self.messages.is_empty() {
return Err(crate::ZaiError::ApiError {
code: crate::client::error::codes::SDK_VALIDATION,
message: "messages must not be empty".to_owned(),
});
}
let has_blank_content = self.messages.iter().any(|message| match message {
TokenizerMessage::User { content } | TokenizerMessage::System { content } => {
content.trim().is_empty()
},
TokenizerMessage::Assistant { content } => content
.as_deref()
.is_some_and(|content| content.trim().is_empty()),
});
if has_blank_content {
return Err(crate::ZaiError::ApiError {
code: crate::client::error::codes::SDK_VALIDATION,
message: "message content must not be blank when present".to_owned(),
});
}
if let Some(request_id) = self.request_id.as_deref()
&& !(6..=64).contains(&request_id.chars().count())
{
return Err(crate::ZaiError::ApiError {
code: crate::client::error::codes::SDK_VALIDATION,
message: "request_id must contain between 6 and 64 characters".to_owned(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_default_and_model_ids_match_the_contract() {
assert_eq!(
serde_json::to_value(TokenizerModel::default()).unwrap(),
"glm-4.6v"
);
assert_eq!(
serde_json::to_value(TokenizerModel::Glm46).unwrap(),
"glm-4.6"
);
for (model, expected) in [
(TokenizerModel::Glm46V, "glm-4.6v"),
(TokenizerModel::Glm46, "glm-4.6"),
(TokenizerModel::Glm45, "glm-4.5"),
(TokenizerModel::Glm45Air, "glm-4.5-air"),
(TokenizerModel::Glm40520, "glm-4-0520"),
(TokenizerModel::Glm4Long, "glm-4-long"),
(TokenizerModel::Glm4Air, "glm-4-air"),
(TokenizerModel::Glm4Flash, "glm-4-flash"),
] {
assert_eq!(serde_json::to_value(model).unwrap(), expected);
}
}
#[test]
fn validation_rejects_blank_content_and_short_request_ids() {
let body = TokenizerBody::new(
TokenizerModel::default(),
vec![TokenizerMessage::User {
content: " ".into(),
}],
);
assert!(body.validate().is_err());
let body = TokenizerBody::new(
TokenizerModel::default(),
vec![TokenizerMessage::User {
content: "hello".into(),
}],
)
.with_request_id("short");
assert!(body.validate().is_err());
}
#[test]
fn debug_redacts_message_content_and_identifiers() {
let body = TokenizerBody::new(
TokenizerModel::default(),
vec![TokenizerMessage::User {
content: "private tokenizer input".to_owned(),
}],
)
.with_request_id("private-request")
.with_user_id("private-user");
let debug = format!("{body:?}");
for secret in ["private tokenizer input", "private-request", "private-user"] {
assert!(!debug.contains(secret));
}
assert!(debug.contains("messages_len: 1"));
}
}