use secrecy::{ExposeSecret, SecretString};
#[derive(Clone)]
#[non_exhaustive]
pub struct MiniMaxConfig {
pub(crate) api_key: SecretString,
pub(crate) endpoint: String,
pub(crate) model: String,
pub(crate) timeout_secs: u64,
}
impl std::fmt::Debug for MiniMaxConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MiniMaxConfig")
.field("api_key", &"<redacted SecretString>")
.field("endpoint", &self.endpoint)
.field("model", &self.model)
.field("timeout_secs", &self.timeout_secs)
.finish_non_exhaustive()
}
}
impl MiniMaxConfig {
pub fn new<S: Into<String>>(api_key: S) -> Result<Self, MiniMaxConfigError> {
let key = api_key.into();
if key.trim().is_empty() {
return Err(MiniMaxConfigError::EmptyApiKey);
}
Ok(Self {
api_key: SecretString::new(key.into_boxed_str()),
endpoint: crate::client::DEFAULT_ENDPOINT.to_string(),
model: crate::client::DEFAULT_MODEL.to_string(),
timeout_secs: crate::client::DEFAULT_TIMEOUT_SECS,
})
}
#[must_use]
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = endpoint.into();
self
}
#[must_use]
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = model.into();
self
}
#[must_use]
pub fn with_timeout_secs(mut self, timeout_secs: u64) -> Self {
self.timeout_secs = timeout_secs;
self
}
pub fn expose_api_key(&self) -> &str {
self.api_key.expose_secret()
}
pub fn endpoint(&self) -> &str {
&self.endpoint
}
pub fn model(&self) -> &str {
&self.model
}
pub fn timeout_secs(&self) -> u64 {
self.timeout_secs
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MiniMaxConfigError {
#[error("MINIMAX_API_KEY must be non-empty")]
EmptyApiKey,
}
impl From<MiniMaxConfigError> for voxora_traits::AsrError {
fn from(err: MiniMaxConfigError) -> Self {
match err {
MiniMaxConfigError::EmptyApiKey => {
voxora_traits::AsrError::Config("MINIMAX_API_KEY must be non-empty".into())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_accepts_non_empty_token() {
let cfg = MiniMaxConfig::new("sk-test").expect("non-empty token");
assert_eq!(cfg.expose_api_key(), "sk-test");
assert_eq!(cfg.endpoint(), crate::client::DEFAULT_ENDPOINT);
assert_eq!(cfg.model(), crate::client::DEFAULT_MODEL);
assert_eq!(cfg.timeout_secs(), crate::client::DEFAULT_TIMEOUT_SECS);
}
#[test]
fn new_rejects_empty_token() {
let err = MiniMaxConfig::new("").expect_err("empty token rejected");
assert!(matches!(err, MiniMaxConfigError::EmptyApiKey));
}
#[test]
fn new_rejects_whitespace_token() {
let err = MiniMaxConfig::new(" ").expect_err("whitespace token rejected");
assert!(matches!(err, MiniMaxConfigError::EmptyApiKey));
}
#[test]
fn debug_redacts_api_key() {
let cfg = MiniMaxConfig::new("sk-supersecret").unwrap();
let rendered = format!("{cfg:?}");
assert!(
!rendered.contains("sk-supersecret"),
"Debug must redact the API key: {rendered}"
);
assert!(
rendered.contains("redacted"),
"Debug should mention redaction: {rendered}"
);
}
#[test]
fn builders_override_defaults() {
let cfg = MiniMaxConfig::new("sk-test")
.unwrap()
.with_endpoint("http://localhost:9999")
.with_model("asr-2.0")
.with_timeout_secs(120);
assert_eq!(cfg.endpoint(), "http://localhost:9999");
assert_eq!(cfg.model(), "asr-2.0");
assert_eq!(cfg.timeout_secs(), 120);
assert_eq!(cfg.expose_api_key(), "sk-test");
}
#[test]
fn config_is_clone() {
let cfg = MiniMaxConfig::new("sk-test").unwrap();
let cfg2 = cfg.clone();
assert_eq!(cfg.expose_api_key(), cfg2.expose_api_key());
assert_eq!(cfg.endpoint(), cfg2.endpoint());
}
}