Skip to main content

codewhale_config/route/
ids.rs

1//! Transparent string newtypes for provider/model/route identities.
2//!
3//! These types make the distinct *meanings* of route strings unmistakable at
4//! the type level so callers can no longer mix:
5//!
6//! - [`ProviderId`] — a provider's canonical id (e.g. `"deepseek"`).
7//! - [`ModelId`] — a canonical, provider-agnostic logical model id.
8//! - [`WireModelId`] — a provider-owned wire id sent on the request
9//!   (e.g. `"deepseek-ai/DeepSeek-V4-Pro"` on Together).
10//! - [`LogicalModelRef`] — a user/selector reference to a model, which may be
11//!   `"auto"`, a bare model, or an aggregator-prefixed string.
12//!
13//! [`ModelId`] and [`WireModelId`] are deliberately DISTINCT types and are
14//! never interchangeable: a canonical model identity is not the same thing as
15//! the provider-specific string put on the wire.
16//!
17//! INVARIANT (load-bearing for #2608): a namespace prefix can NEVER become a
18//! provider. There is intentionally NO `From`/`Into` conversion from
19//! [`LogicalModelRef`] or [`NamespaceHint`] to [`ProviderId`]. A prefix like
20//! `deepseek-ai/` is a catalog/namespace hint only; it is not proof of
21//! provider ownership. Do not add such a conversion.
22
23use std::fmt;
24
25use serde::{Deserialize, Serialize};
26
27use crate::ProviderKind;
28
29macro_rules! string_newtype {
30    ($(#[$meta:meta])* $name:ident) => {
31        $(#[$meta])*
32        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33        #[serde(transparent)]
34        pub struct $name(String);
35
36        impl $name {
37            /// Borrow the inner string slice.
38            #[must_use]
39            pub fn as_str(&self) -> &str {
40                &self.0
41            }
42        }
43
44        impl From<&str> for $name {
45            fn from(value: &str) -> Self {
46                Self(value.to_string())
47            }
48        }
49
50        impl From<String> for $name {
51            fn from(value: String) -> Self {
52                Self(value)
53            }
54        }
55
56        impl AsRef<str> for $name {
57            fn as_ref(&self) -> &str {
58                &self.0
59            }
60        }
61
62        impl fmt::Display for $name {
63            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64                f.write_str(&self.0)
65            }
66        }
67    };
68}
69
70string_newtype!(
71    /// A provider's canonical identifier (e.g. `"deepseek"`, `"openrouter"`).
72    ProviderId
73);
74
75string_newtype!(
76    /// A canonical, provider-agnostic logical model identity.
77    ///
78    /// Distinct from [`WireModelId`]: this is "what the model is", not "what
79    /// string a provider expects on the wire".
80    ModelId
81);
82
83string_newtype!(
84    /// A provider-owned wire model id sent verbatim on the request.
85    ///
86    /// Distinct from [`ModelId`]: aggregator-prefixed strings such as
87    /// `"deepseek-ai/DeepSeek-V4-Pro"` are wire ids, not canonical identities.
88    WireModelId
89);
90
91string_newtype!(
92    /// A user/selector reference to a model.
93    ///
94    /// May be the `"auto"` sentinel, a bare model name, or an
95    /// aggregator-prefixed string. A [`LogicalModelRef`] carries no provider
96    /// authority by itself; see [`Self::namespace_hint`].
97    LogicalModelRef
98);
99
100impl ProviderId {
101    /// Build a [`ProviderId`] from a [`ProviderKind`] using its canonical id.
102    #[must_use]
103    pub fn from_kind(kind: ProviderKind) -> Self {
104        Self(kind.as_str().to_string())
105    }
106}
107
108/// A leading namespace/organization prefix carried by a [`LogicalModelRef`].
109///
110/// A namespace hint is a *catalog* hint only. It is NEVER convertible to a
111/// [`ProviderId`]; an aggregator may serve `deepseek-ai/...` without being
112/// DeepSeek, and a custom endpoint may legitimately use a look-alike string.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
114#[serde(rename_all = "kebab-case")]
115pub enum NamespaceHint {
116    /// `deepseek-ai/` prefix.
117    DeepseekAi,
118    /// `deepseek/` prefix.
119    Deepseek,
120    /// `anthropic/` prefix.
121    Anthropic,
122    /// `openai/` prefix.
123    Openai,
124    /// `qwen/` prefix.
125    Qwen,
126}
127
128impl LogicalModelRef {
129    /// Borrow the raw selector string.
130    #[must_use]
131    pub fn raw(&self) -> &str {
132        self.as_str()
133    }
134
135    /// Whether this selector is the explicit `auto` router sentinel.
136    ///
137    /// `auto` is an opt-in router sentinel, never a literal model id.
138    #[must_use]
139    pub fn is_auto(&self) -> bool {
140        self.raw() == "auto"
141    }
142
143    /// Parse the leading namespace prefix, if any.
144    ///
145    /// Returns `Some` only for the curated aggregator/organization prefixes.
146    /// This is a hint about catalog namespace and does NOT identify a provider.
147    #[must_use]
148    pub fn namespace_hint(&self) -> Option<NamespaceHint> {
149        let raw = self.raw();
150        // Order matters: `deepseek-ai/` must be matched before `deepseek/`.
151        if raw.starts_with("deepseek-ai/") {
152            Some(NamespaceHint::DeepseekAi)
153        } else if raw.starts_with("deepseek/") {
154            Some(NamespaceHint::Deepseek)
155        } else if raw.starts_with("anthropic/") {
156            Some(NamespaceHint::Anthropic)
157        } else if raw.starts_with("openai/") {
158            Some(NamespaceHint::Openai)
159        } else if raw.starts_with("qwen/") {
160            Some(NamespaceHint::Qwen)
161        } else {
162            None
163        }
164    }
165}