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
27/// The `"auto"` router sentinel for [`LogicalModelRef`].
28///
29/// `auto` is an opt-in router sentinel — it never refers to a literal model
30/// named "auto". Centralized here so every comparison site uses the same
31/// spelling (#4158).
32pub const AUTO_SENTINEL: &str = "auto";
33
34use crate::ProviderKind;
35
36macro_rules! string_newtype {
37    ($(#[$meta:meta])* $name:ident) => {
38        $(#[$meta])*
39        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
40        #[serde(transparent)]
41        pub struct $name(String);
42
43        impl $name {
44            /// Borrow the inner string slice.
45            #[must_use]
46            pub fn as_str(&self) -> &str {
47                &self.0
48            }
49        }
50
51        impl From<&str> for $name {
52            fn from(value: &str) -> Self {
53                Self(value.to_string())
54            }
55        }
56
57        impl From<String> for $name {
58            fn from(value: String) -> Self {
59                Self(value)
60            }
61        }
62
63        impl AsRef<str> for $name {
64            fn as_ref(&self) -> &str {
65                &self.0
66            }
67        }
68
69        impl fmt::Display for $name {
70            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71                f.write_str(&self.0)
72            }
73        }
74    };
75}
76
77string_newtype!(
78    /// A provider's canonical identifier (e.g. `"deepseek"`, `"openrouter"`).
79    ProviderId
80);
81
82string_newtype!(
83    /// A canonical, provider-agnostic logical model identity.
84    ///
85    /// Distinct from [`WireModelId`]: this is "what the model is", not "what
86    /// string a provider expects on the wire".
87    ModelId
88);
89
90string_newtype!(
91    /// A provider-owned wire model id sent verbatim on the request.
92    ///
93    /// Distinct from [`ModelId`]: aggregator-prefixed strings such as
94    /// `"deepseek-ai/DeepSeek-V4-Pro"` are wire ids, not canonical identities.
95    WireModelId
96);
97
98string_newtype!(
99    /// A user/selector reference to a model.
100    ///
101    /// May be the `"auto"` sentinel, a bare model name, or an
102    /// aggregator-prefixed string. A [`LogicalModelRef`] carries no provider
103    /// authority by itself; see [`Self::namespace_hint`].
104    LogicalModelRef
105);
106
107impl ProviderId {
108    /// Build a [`ProviderId`] from a [`ProviderKind`] using its canonical id.
109    #[must_use]
110    pub fn from_kind(kind: ProviderKind) -> Self {
111        Self(kind.as_str().to_string())
112    }
113}
114
115/// A leading namespace/organization prefix carried by a [`LogicalModelRef`].
116///
117/// A namespace hint is a *catalog* hint only. It is NEVER convertible to a
118/// [`ProviderId`]; an aggregator may serve `deepseek-ai/...` without being
119/// DeepSeek, and a custom endpoint may legitimately use a look-alike string.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
121#[serde(rename_all = "kebab-case")]
122pub enum NamespaceHint {
123    /// `deepseek-ai/` prefix.
124    DeepseekAi,
125    /// `deepseek/` prefix.
126    Deepseek,
127    /// `anthropic/` prefix.
128    Anthropic,
129    /// `openai/` prefix.
130    Openai,
131    /// `qwen/` prefix.
132    Qwen,
133}
134
135impl LogicalModelRef {
136    /// Borrow the raw selector string.
137    #[must_use]
138    pub fn raw(&self) -> &str {
139        self.as_str()
140    }
141
142    /// Whether this selector is the explicit `auto` router sentinel.
143    ///
144    /// `auto` is an opt-in router sentinel, never a literal model id.
145    #[must_use]
146    pub fn is_auto(&self) -> bool {
147        self.raw() == AUTO_SENTINEL
148    }
149
150    /// Parse the leading namespace prefix, if any.
151    ///
152    /// Returns `Some` only for the curated aggregator/organization prefixes.
153    /// This is a hint about catalog namespace and does NOT identify a provider.
154    #[must_use]
155    pub fn namespace_hint(&self) -> Option<NamespaceHint> {
156        let raw = self.raw();
157        // Order matters: `deepseek-ai/` must be matched before `deepseek/`.
158        if raw.starts_with("deepseek-ai/") {
159            Some(NamespaceHint::DeepseekAi)
160        } else if raw.starts_with("deepseek/") {
161            Some(NamespaceHint::Deepseek)
162        } else if raw.starts_with("anthropic/") {
163            Some(NamespaceHint::Anthropic)
164        } else if raw.starts_with("openai/") {
165            Some(NamespaceHint::Openai)
166        } else if raw.starts_with("qwen/") {
167            Some(NamespaceHint::Qwen)
168        } else {
169            None
170        }
171    }
172}