Skip to main content

raqeem_core/
endpoint.rs

1//! The one adapter. Cohere-hosted and self-hosted vLLM differ only in URL,
2//! auth header, and model id — so a single `Endpoint` struct + two constructors
3//! covers both, and any future OpenAI-compatible backend.
4
5use crate::provider::Provider;
6
7/// Cohere's hosted transcription endpoint.
8pub const COHERE_URL: &str = "https://api.cohere.com/v2/audio/transcriptions";
9/// Default model id for the Arabic transcriber. Cohere requires a **dated** model
10/// id — undated aliases (`cohere-transcribe-arabic`) return HTTP 404 "model not
11/// found". Bump this when Cohere ships a newer dated Arabic transcription model.
12pub const DEFAULT_COHERE_MODEL: &str = "cohere-transcribe-arabic-07-2026";
13
14/// Where and how to reach a transcription server.
15///
16/// `Debug` is implemented by hand rather than derived, so that printing an `Endpoint`
17/// cannot disclose the key it holds — see the impl below.
18#[derive(Clone)]
19pub struct Endpoint {
20    /// Full URL to POST the multipart form to.
21    pub url: String,
22    /// Bearer token, if the backend requires auth.
23    pub api_key: Option<String>,
24    /// Model id sent as the `model` form field.
25    pub model: String,
26    /// Provenance tag recorded on the transcript.
27    pub provider: Provider,
28}
29
30/// Hand-written so the API key can never be printed.
31///
32/// A derived `Debug` renders `api_key: Some("sk-…")` in full, which means any downstream
33/// `dbg!(&endpoint)` or `tracing::debug!(?endpoint)` discloses the caller's credential.
34/// That is a footgun a published library shouldn't hand out, so the field is masked and
35/// only its presence is shown.
36impl std::fmt::Debug for Endpoint {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.debug_struct("Endpoint")
39            .field("url", &crate::error::redact_url(&self.url))
40            .field("api_key", &self.api_key.as_ref().map(|_| "***"))
41            .field("model", &self.model)
42            .field("provider", &self.provider)
43            .finish()
44    }
45}
46
47impl Endpoint {
48    /// Cohere's hosted API. `model` defaults to [`DEFAULT_COHERE_MODEL`].
49    pub fn cohere(api_key: impl Into<String>, model: Option<String>) -> Self {
50        Endpoint {
51            url: COHERE_URL.to_string(),
52            api_key: Some(api_key.into()),
53            model: model.unwrap_or_else(|| DEFAULT_COHERE_MODEL.to_string()),
54            provider: Provider::Cohere,
55        }
56    }
57
58    /// A self-hosted OpenAI-compatible endpoint — pass the full route URL
59    /// (e.g. `http://localhost:8000/v1/audio/transcriptions`).
60    ///
61    /// The URL is parsed here so a typo fails immediately and by name. Left unchecked it
62    /// surfaced much later as reqwest's `builder error`, which says nothing about which
63    /// input was wrong.
64    pub fn openai_compatible(
65        url: impl Into<String>,
66        model: impl Into<String>,
67        api_key: Option<String>,
68    ) -> crate::Result<Self> {
69        let url = url.into();
70        if url.trim().is_empty() {
71            return Err(crate::Error::InvalidEndpoint {
72                url,
73                reason: "it is empty".to_string(),
74            });
75        }
76        let parsed = url::Url::parse(&url).map_err(|e| crate::Error::InvalidEndpoint {
77            url: crate::error::redact_url(&url),
78            reason: e.to_string(),
79        })?;
80        if !matches!(parsed.scheme(), "http" | "https") {
81            return Err(crate::Error::InvalidEndpoint {
82                url: crate::error::redact_url(&url),
83                reason: format!("scheme {:?} is not http or https", parsed.scheme()),
84            });
85        }
86        Ok(Endpoint {
87            url,
88            api_key,
89            model: model.into(),
90            provider: Provider::OpenAiCompatible,
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::Endpoint;
98
99    #[test]
100    fn debug_never_prints_the_api_key() {
101        let ep = Endpoint::cohere("sk-SECRET-DO-NOT-PRINT", None);
102        let shown = format!("{ep:?}");
103        assert!(!shown.contains("sk-SECRET-DO-NOT-PRINT"), "{shown}");
104        assert!(
105            shown.contains("***"),
106            "presence should still be visible: {shown}"
107        );
108    }
109
110    #[test]
111    fn debug_never_prints_url_credentials() {
112        let ep = Endpoint::openai_compatible("https://bob:hunter2@host/v1", "m", None).unwrap();
113        let shown = format!("{ep:?}");
114        assert!(!shown.contains("hunter2"), "{shown}");
115    }
116
117    #[test]
118    fn a_valid_url_is_accepted() {
119        let ep = Endpoint::openai_compatible("http://localhost:8000/v1", "m", None).unwrap();
120        assert_eq!(ep.url, "http://localhost:8000/v1");
121    }
122
123    #[test]
124    fn a_bad_url_is_rejected_by_name_not_by_builder_error() {
125        for bad in ["", "   ", "not-a-url", "ftp://host/v1", "/just/a/path"] {
126            let err = Endpoint::openai_compatible(bad, "m", None)
127                .expect_err("{bad:?} should be rejected");
128            assert!(
129                err.to_string().contains("endpoint"),
130                "error should name the endpoint for {bad:?}: {err}"
131            );
132        }
133    }
134}