use crate::provider::Provider;
pub const COHERE_URL: &str = "https://api.cohere.com/v2/audio/transcriptions";
pub const DEFAULT_COHERE_MODEL: &str = "cohere-transcribe-arabic-07-2026";
#[derive(Clone)]
pub struct Endpoint {
pub url: String,
pub api_key: Option<String>,
pub model: String,
pub provider: Provider,
}
impl std::fmt::Debug for Endpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Endpoint")
.field("url", &crate::error::redact_url(&self.url))
.field("api_key", &self.api_key.as_ref().map(|_| "***"))
.field("model", &self.model)
.field("provider", &self.provider)
.finish()
}
}
impl Endpoint {
pub fn cohere(api_key: impl Into<String>, model: Option<String>) -> Self {
Endpoint {
url: COHERE_URL.to_string(),
api_key: Some(api_key.into()),
model: model.unwrap_or_else(|| DEFAULT_COHERE_MODEL.to_string()),
provider: Provider::Cohere,
}
}
pub fn openai_compatible(
url: impl Into<String>,
model: impl Into<String>,
api_key: Option<String>,
) -> crate::Result<Self> {
let url = url.into();
if url.trim().is_empty() {
return Err(crate::Error::InvalidEndpoint {
url,
reason: "it is empty".to_string(),
});
}
let parsed = url::Url::parse(&url).map_err(|e| crate::Error::InvalidEndpoint {
url: crate::error::redact_url(&url),
reason: e.to_string(),
})?;
if !matches!(parsed.scheme(), "http" | "https") {
return Err(crate::Error::InvalidEndpoint {
url: crate::error::redact_url(&url),
reason: format!("scheme {:?} is not http or https", parsed.scheme()),
});
}
Ok(Endpoint {
url,
api_key,
model: model.into(),
provider: Provider::OpenAiCompatible,
})
}
}
#[cfg(test)]
mod tests {
use super::Endpoint;
#[test]
fn debug_never_prints_the_api_key() {
let ep = Endpoint::cohere("sk-SECRET-DO-NOT-PRINT", None);
let shown = format!("{ep:?}");
assert!(!shown.contains("sk-SECRET-DO-NOT-PRINT"), "{shown}");
assert!(
shown.contains("***"),
"presence should still be visible: {shown}"
);
}
#[test]
fn debug_never_prints_url_credentials() {
let ep = Endpoint::openai_compatible("https://bob:hunter2@host/v1", "m", None).unwrap();
let shown = format!("{ep:?}");
assert!(!shown.contains("hunter2"), "{shown}");
}
#[test]
fn a_valid_url_is_accepted() {
let ep = Endpoint::openai_compatible("http://localhost:8000/v1", "m", None).unwrap();
assert_eq!(ep.url, "http://localhost:8000/v1");
}
#[test]
fn a_bad_url_is_rejected_by_name_not_by_builder_error() {
for bad in ["", " ", "not-a-url", "ftp://host/v1", "/just/a/path"] {
let err = Endpoint::openai_compatible(bad, "m", None)
.expect_err("{bad:?} should be rejected");
assert!(
err.to_string().contains("endpoint"),
"error should name the endpoint for {bad:?}: {err}"
);
}
}
}