Skip to main content

raqeem_core/
provider.rs

1//! Backend presets. Every supported transcription server speaks the same
2//! OpenAI-compatible multipart `/audio/transcriptions` shape, so a "provider"
3//! is just a label + the defaults ([`crate::Endpoint`] carries the actual
4//! URL / auth / model). Adding a backend = adding a variant here and a
5//! constructor on `Endpoint` — see `.claude/skills/add-endpoint-adapter`.
6
7/// A known transcription backend.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Provider {
10    /// Cohere's hosted API — `https://api.cohere.com/v2/audio/transcriptions`.
11    Cohere,
12    /// Any self-hosted OpenAI-compatible server (e.g. vLLM at
13    /// `/v1/audio/transcriptions`), reached via an explicit URL.
14    OpenAiCompatible,
15}
16
17impl Provider {
18    /// Stable machine-readable tag, recorded on every [`crate::Transcript`].
19    pub fn as_str(self) -> &'static str {
20        match self {
21            Provider::Cohere => "cohere",
22            Provider::OpenAiCompatible => "openai-compatible",
23        }
24    }
25
26    /// Every variant, so callers can enumerate the backends without matching by hand.
27    pub const ALL: &'static [Provider] = &[Provider::Cohere, Provider::OpenAiCompatible];
28
29    /// The spellings [`FromStr`](std::str::FromStr) accepts, as the CLI and the Python
30    /// API document them.
31    ///
32    /// Deliberately not derived from [`as_str`](Self::as_str): that returns the
33    /// provenance tag written onto a transcript, and for the self-hosted backend the tag
34    /// (`openai-compatible`) is not what a user types (`openai`). Error messages have to
35    /// come from here, or they tell people to pass a name the docs never mention.
36    pub const ACCEPTED_NAMES: &'static [&'static str] = &["cohere", "openai"];
37}
38
39impl std::fmt::Display for Provider {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.write_str(self.as_str())
42    }
43}
44
45/// The name a caller typed didn't match any backend.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct UnknownProvider(pub String);
48
49impl std::fmt::Display for UnknownProvider {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "unknown provider {:?} (expected one of: ", self.0)?;
52        for (i, name) in Provider::ACCEPTED_NAMES.iter().enumerate() {
53            if i > 0 {
54                f.write_str(", ")?;
55            }
56            write!(f, "{name}")?;
57        }
58        f.write_str(")")
59    }
60}
61
62impl std::error::Error for UnknownProvider {}
63
64impl std::str::FromStr for Provider {
65    type Err = UnknownProvider;
66
67    /// Parse a backend name. `"openai"` is accepted alongside the canonical
68    /// `"openai-compatible"` because that is the spelling the CLI and the Python
69    /// binding have always exposed to users.
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        match s {
72            "cohere" => Ok(Provider::Cohere),
73            "openai" | "openai-compatible" => Ok(Provider::OpenAiCompatible),
74            other => Err(UnknownProvider(other.to_string())),
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::Provider;
82
83    #[test]
84    fn every_tag_round_trips_through_from_str() {
85        for &p in Provider::ALL {
86            assert_eq!(
87                p.as_str().parse::<Provider>(),
88                Ok(p),
89                "{p} did not round-trip"
90            );
91        }
92    }
93
94    #[test]
95    fn the_user_facing_openai_spelling_is_accepted() {
96        assert_eq!("openai".parse::<Provider>(), Ok(Provider::OpenAiCompatible));
97    }
98
99    /// Every name we advertise must actually parse, or the error message sends people in
100    /// a circle.
101    #[test]
102    fn every_advertised_name_parses() {
103        for name in Provider::ACCEPTED_NAMES {
104            assert!(
105                name.parse::<Provider>().is_ok(),
106                "advertised {name:?} but it does not parse"
107            );
108        }
109    }
110
111    /// And the complement, which is the direction that fails silently: a new variant with
112    /// no entry in `ACCEPTED_NAMES` compiles, passes every other test, and is simply
113    /// never offered to a user who typos the provider name.
114    #[test]
115    fn every_variant_is_reachable_by_an_advertised_name() {
116        for &p in Provider::ALL {
117            let reachable = Provider::ACCEPTED_NAMES
118                .iter()
119                .any(|n| n.parse::<Provider>() == Ok(p));
120            assert!(
121                reachable,
122                "{p} has no entry in ACCEPTED_NAMES, so no documented spelling reaches it"
123            );
124        }
125    }
126
127    #[test]
128    fn an_unknown_name_lists_the_names_users_actually_type() {
129        let err = "nope".parse::<Provider>().unwrap_err();
130        let msg = err.to_string();
131        assert!(msg.contains("nope"), "{msg}");
132        assert!(msg.contains("cohere"), "{msg}");
133        // 'openai' is the documented spelling; 'openai-compatible' is the transcript tag
134        // and must not be what we tell a user to type.
135        assert!(msg.contains("openai"), "{msg}");
136        assert!(!msg.contains("openai-compatible"), "{msg}");
137    }
138}