Skip to main content

switchyard_protocol/
model_id.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Model identifier used by Switchyard. See ModelId docs for details.
5
6use std::borrow::Borrow;
7use std::fmt;
8use std::ops::Deref;
9
10use serde::{Deserialize, Serialize};
11
12/// A model name used in a request or routing decision.
13///
14/// It can name a provider model, such as `"openai/gpt-oss-20b"`. It can also name a
15/// Switchyard route, such as `"switchyard/random"`.
16///
17/// A target name from server config, such as `"capable"`, is not a model ID. The server
18/// resolves target names to model IDs before routing.
19///
20/// This type acts like a string. It can be printed, compared, and saved as a JSON string.
21#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct ModelId(String);
24
25/// Forwards to the wrapped string rather than deriving, so `{:?}` renders `"gpt-4"`
26/// instead of `ModelId("gpt-4")`.
27impl fmt::Debug for ModelId {
28    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29        fmt::Debug::fmt(&self.0, formatter)
30    }
31}
32
33impl ModelId {
34    /// Wraps a model identifier.
35    pub fn new(id: impl Into<String>) -> Self {
36        Self(id.into())
37    }
38
39    /// The identifier as a string slice.
40    pub fn as_str(&self) -> &str {
41        &self.0
42    }
43
44    /// Unwraps to the owned identifier.
45    pub fn into_string(self) -> String {
46        self.0
47    }
48}
49
50impl fmt::Display for ModelId {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        formatter.write_str(&self.0)
53    }
54}
55
56impl Deref for ModelId {
57    type Target = str;
58
59    fn deref(&self) -> &str {
60        &self.0
61    }
62}
63
64impl AsRef<str> for ModelId {
65    fn as_ref(&self) -> &str {
66        &self.0
67    }
68}
69
70/// Lets a `HashMap<ModelId, _>` or `HashSet<ModelId>` be looked up by `&str`, so
71/// callers holding a borrowed id do not have to allocate one to query.
72impl Borrow<str> for ModelId {
73    fn borrow(&self) -> &str {
74        &self.0
75    }
76}
77
78impl From<String> for ModelId {
79    fn from(id: String) -> Self {
80        Self(id)
81    }
82}
83
84impl From<&str> for ModelId {
85    fn from(id: &str) -> Self {
86        Self(id.to_string())
87    }
88}
89
90/// Mirrors `From<&str> for String`, so a borrowed id reaches an owning
91/// `impl Into<ModelId>` parameter without an explicit clone.
92impl From<&ModelId> for ModelId {
93    fn from(id: &ModelId) -> Self {
94        id.clone()
95    }
96}
97
98impl From<ModelId> for String {
99    fn from(id: ModelId) -> Self {
100        id.0
101    }
102}
103
104impl From<&ModelId> for String {
105    fn from(id: &ModelId) -> Self {
106        id.0.clone()
107    }
108}
109
110// Comparison against bare strings, in both directions, so an id can be checked
111// against a literal or a configured name without wrapping either side.
112// This is likely excessive. We can reduce when things settle.
113
114impl PartialEq<str> for ModelId {
115    fn eq(&self, other: &str) -> bool {
116        self.0 == other
117    }
118}
119
120impl PartialEq<&str> for ModelId {
121    fn eq(&self, other: &&str) -> bool {
122        self.0 == *other
123    }
124}
125
126impl PartialEq<String> for ModelId {
127    fn eq(&self, other: &String) -> bool {
128        &self.0 == other
129    }
130}
131
132impl PartialEq<ModelId> for str {
133    fn eq(&self, other: &ModelId) -> bool {
134        self == other.0
135    }
136}
137
138impl PartialEq<ModelId> for &str {
139    fn eq(&self, other: &ModelId) -> bool {
140        *self == other.0
141    }
142}
143
144impl PartialEq<ModelId> for String {
145    fn eq(&self, other: &ModelId) -> bool {
146        self == &other.0
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use std::collections::HashMap;
153
154    use super::*;
155
156    #[test]
157    fn it_behaves_like_the_string_it_wraps() {
158        let id = ModelId::new("openai/gpt-oss-20b");
159
160        assert_eq!(id.to_string(), "openai/gpt-oss-20b");
161        assert_eq!(id, "openai/gpt-oss-20b");
162        assert_eq!("openai/gpt-oss-20b", id);
163        assert!(id.starts_with("openai/"));
164        assert_eq!(takes_str(&id), "openai/gpt-oss-20b");
165    }
166
167    /// Deref coercion means an `&ModelId` reaches a `&str` parameter unchanged.
168    fn takes_str(model: &str) -> &str {
169        model
170    }
171
172    #[test]
173    fn a_map_of_ids_is_queryable_by_str() {
174        let by_model = HashMap::from([(ModelId::new("aws/anthropic/claude-opus-4-5"), 1)]);
175
176        assert_eq!(by_model.get("aws/anthropic/claude-opus-4-5"), Some(&1));
177    }
178
179    #[test]
180    fn it_serializes_as_a_bare_string() -> serde_json::Result<()> {
181        let id = ModelId::new("openai/gpt-oss-20b");
182
183        let json = serde_json::to_string(&id)?;
184        assert_eq!(json, "\"openai/gpt-oss-20b\"");
185        assert_eq!(serde_json::from_str::<ModelId>(&json)?, id);
186        Ok(())
187    }
188}