Skip to main content

claude_api/
types.rs

1//! Foundational shared types: [`ModelId`], [`Role`], [`Usage`], [`StopReason`].
2
3use std::borrow::Cow;
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8/// Identifier for a Claude model.
9///
10/// Stored as a string rather than an enum so a new model release does not
11/// require an SDK bump. Common models are exposed as associated constants
12/// for ergonomics; arbitrary values can be constructed with [`ModelId::custom`].
13///
14/// ```
15/// use claude_api::types::ModelId;
16///
17/// let known = ModelId::SONNET_4_6;
18/// let custom = ModelId::custom("claude-some-future-model");
19/// assert_eq!(known.as_str(), "claude-sonnet-4-6");
20/// assert_eq!(custom.as_str(), "claude-some-future-model");
21/// ```
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct ModelId(Cow<'static, str>);
24
25impl ModelId {
26    /// Claude Opus 4.7.
27    pub const OPUS_4_7: ModelId = ModelId(Cow::Borrowed("claude-opus-4-7"));
28    /// Claude Sonnet 4.6.
29    pub const SONNET_4_6: ModelId = ModelId(Cow::Borrowed("claude-sonnet-4-6"));
30    /// Claude Haiku 4.5 (dated snapshot).
31    pub const HAIKU_4_5: ModelId = ModelId(Cow::Borrowed("claude-haiku-4-5-20251001"));
32
33    /// Construct a [`ModelId`] from an arbitrary string.
34    pub fn custom(s: impl Into<String>) -> Self {
35        Self(Cow::Owned(s.into()))
36    }
37
38    /// Borrow the underlying string.
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44impl fmt::Display for ModelId {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(&self.0)
47    }
48}
49
50impl AsRef<str> for ModelId {
51    fn as_ref(&self) -> &str {
52        &self.0
53    }
54}
55
56impl From<&'static str> for ModelId {
57    fn from(s: &'static str) -> Self {
58        Self(Cow::Borrowed(s))
59    }
60}
61
62impl From<String> for ModelId {
63    fn from(s: String) -> Self {
64        Self(Cow::Owned(s))
65    }
66}
67
68impl Serialize for ModelId {
69    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
70        s.serialize_str(&self.0)
71    }
72}
73
74impl<'de> Deserialize<'de> for ModelId {
75    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
76        String::deserialize(d).map(Self::from)
77    }
78}
79
80/// Conversation role for a message.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[serde(rename_all = "lowercase")]
83pub enum Role {
84    /// A user-authored turn.
85    User,
86    /// A model-authored turn.
87    Assistant,
88}
89
90/// Why the model stopped producing output.
91///
92/// New variants may appear over time; unknown values deserialize to
93/// [`StopReason::Other`]. The original wire string is not preserved.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum StopReason {
97    /// Natural end of turn.
98    EndTurn,
99    /// Hit the request's `max_tokens`.
100    MaxTokens,
101    /// Hit a configured stop sequence.
102    StopSequence,
103    /// Stopped to emit a tool use; caller should run the tool and continue.
104    ToolUse,
105    /// Paused mid-turn (e.g. for a server-side tool call to complete).
106    PauseTurn,
107    /// The model refused to answer.
108    Refusal,
109    /// An unrecognized stop reason; the SDK is older than the API.
110    #[serde(other)]
111    Other,
112}
113
114/// Service tier reported on a response.
115///
116/// Mirrors Anthropic's tiered routing. New tiers may appear; unknowns
117/// deserialize to [`ServiceTier::Other`].
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum ServiceTier {
121    /// Standard service tier.
122    Standard,
123    /// Priority service tier.
124    Priority,
125    /// Batch service tier.
126    Batch,
127    /// An unrecognized service tier.
128    #[serde(other)]
129    Other,
130}
131
132/// Token usage and related counters returned on every response.
133///
134/// `#[non_exhaustive]` because Anthropic adds fields here regularly
135/// (`cache_creation`, `server_tool_use`, `service_tier` are recent additions).
136#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
137#[non_exhaustive]
138pub struct Usage {
139    /// Number of input tokens billed.
140    pub input_tokens: u32,
141    /// Number of output tokens billed.
142    pub output_tokens: u32,
143    /// Tokens written to the prompt cache on this request.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub cache_creation_input_tokens: Option<u32>,
146    /// Tokens read from the prompt cache on this request.
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub cache_read_input_tokens: Option<u32>,
149    /// Per-TTL breakdown of cache writes (5-minute vs 1-hour).
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub cache_creation: Option<CacheCreationBreakdown>,
152    /// Counters for server-side tool usage (e.g. web search).
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub server_tool_use: Option<ServerToolUseUsage>,
155    /// Service tier the request actually ran on.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub service_tier: Option<ServiceTier>,
158    /// Inference geography the request was routed to (e.g.
159    /// `"not_available"`, region codes when reported). Open string for
160    /// forward-compat.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub inference_geo: Option<String>,
163}
164
165/// Per-TTL breakdown of cache-creation tokens.
166#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
167#[non_exhaustive]
168pub struct CacheCreationBreakdown {
169    /// Tokens written to a 5-minute-TTL cache entry.
170    #[serde(default)]
171    pub ephemeral_5m_input_tokens: u32,
172    /// Tokens written to a 1-hour-TTL cache entry.
173    #[serde(default)]
174    pub ephemeral_1h_input_tokens: u32,
175}
176
177/// Counters for server-side tool invocations billed on this request.
178#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
179#[non_exhaustive]
180pub struct ServerToolUseUsage {
181    /// Number of web-search requests issued.
182    #[serde(default)]
183    pub web_search_requests: u32,
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use pretty_assertions::assert_eq;
190    use serde::de::DeserializeOwned;
191
192    fn round_trip<T>(value: &T, expected_json: &str)
193    where
194        T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug,
195    {
196        let json = serde_json::to_string(value).expect("serialize");
197        assert_eq!(json, expected_json, "serialized form mismatch");
198        let parsed: T = serde_json::from_str(&json).expect("deserialize");
199        assert_eq!(&parsed, value, "round-trip mismatch");
200    }
201
202    #[test]
203    fn model_id_serializes_as_string() {
204        round_trip(&ModelId::OPUS_4_7, "\"claude-opus-4-7\"");
205        round_trip(&ModelId::SONNET_4_6, "\"claude-sonnet-4-6\"");
206        round_trip(&ModelId::HAIKU_4_5, "\"claude-haiku-4-5-20251001\"");
207        round_trip(
208            &ModelId::custom("claude-future-foo"),
209            "\"claude-future-foo\"",
210        );
211    }
212
213    #[test]
214    fn model_id_const_equals_custom() {
215        assert_eq!(ModelId::OPUS_4_7, ModelId::custom("claude-opus-4-7"));
216    }
217
218    #[test]
219    fn model_id_display_and_as_ref() {
220        assert_eq!(ModelId::SONNET_4_6.to_string(), "claude-sonnet-4-6");
221        assert_eq!(
222            <ModelId as AsRef<str>>::as_ref(&ModelId::SONNET_4_6),
223            "claude-sonnet-4-6"
224        );
225    }
226
227    #[test]
228    fn role_serializes_lowercase() {
229        round_trip(&Role::User, "\"user\"");
230        round_trip(&Role::Assistant, "\"assistant\"");
231    }
232
233    #[test]
234    fn stop_reason_round_trips_known_variants() {
235        round_trip(&StopReason::EndTurn, "\"end_turn\"");
236        round_trip(&StopReason::MaxTokens, "\"max_tokens\"");
237        round_trip(&StopReason::StopSequence, "\"stop_sequence\"");
238        round_trip(&StopReason::ToolUse, "\"tool_use\"");
239        round_trip(&StopReason::PauseTurn, "\"pause_turn\"");
240        round_trip(&StopReason::Refusal, "\"refusal\"");
241    }
242
243    #[test]
244    fn stop_reason_unknown_falls_back_to_other() {
245        let parsed: StopReason = serde_json::from_str("\"some_new_reason\"").expect("deserialize");
246        assert_eq!(parsed, StopReason::Other);
247    }
248
249    #[test]
250    fn service_tier_unknown_falls_back_to_other() {
251        let parsed: ServiceTier = serde_json::from_str("\"enterprise\"").expect("deserialize");
252        assert_eq!(parsed, ServiceTier::Other);
253        round_trip(&ServiceTier::Standard, "\"standard\"");
254        round_trip(&ServiceTier::Priority, "\"priority\"");
255        round_trip(&ServiceTier::Batch, "\"batch\"");
256    }
257
258    #[test]
259    fn usage_minimal_payload_round_trips() {
260        let u = Usage {
261            input_tokens: 12,
262            output_tokens: 34,
263            ..Usage::default()
264        };
265        round_trip(&u, r#"{"input_tokens":12,"output_tokens":34}"#);
266    }
267
268    #[test]
269    fn usage_full_payload_round_trips() {
270        let u = Usage {
271            input_tokens: 100,
272            output_tokens: 50,
273            cache_creation_input_tokens: Some(20),
274            cache_read_input_tokens: Some(80),
275            cache_creation: Some(CacheCreationBreakdown {
276                ephemeral_5m_input_tokens: 10,
277                ephemeral_1h_input_tokens: 10,
278            }),
279            server_tool_use: Some(ServerToolUseUsage {
280                web_search_requests: 3,
281            }),
282            service_tier: Some(ServiceTier::Standard),
283            inference_geo: Some("us-east-1".into()),
284        };
285        let json = serde_json::to_string(&u).expect("serialize");
286        let parsed: Usage = serde_json::from_str(&json).expect("deserialize");
287        assert_eq!(parsed, u);
288    }
289
290    #[test]
291    fn usage_tolerates_unknown_fields() {
292        let json = r#"{
293            "input_tokens": 5,
294            "output_tokens": 7,
295            "future_field": "ignored"
296        }"#;
297        let parsed: Usage = serde_json::from_str(json).expect("deserialize");
298        assert_eq!(parsed.input_tokens, 5);
299        assert_eq!(parsed.output_tokens, 7);
300    }
301}