返回 CodeWhale
ids.rs
根目录 / crates / config / src / 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
23 use std::fmt;
24
25 use 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).
32 pub const AUTO_SENTINEL: &str = "auto";
33
34 use crate::ProviderKind;
35
36 macro_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
77 string_newtype!(
78 /// A provider's canonical identifier (e.g. `"deepseek"`, `"openrouter"`).
79 ProviderId
80 );
81
82 string_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
90 string_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
98 string_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
107 impl 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")]
122 pub 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
135 impl 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 }
173
173 lines RUST