Skip to main content

claude_api/
beta.rs

1//! Typed `anthropic-beta` header values.
2//!
3//! [`BetaHeader`] enumerates the canonical beta version strings
4//! published in Anthropic's API reference. It's an open-string enum:
5//! known values map to typed variants for autocompletion and
6//! refactor safety, and unknown values fall through to
7//! [`BetaHeader::Other`] so a brand-new beta header can still be
8//! passed by string without a crate update.
9//!
10//! Pass [`BetaHeader`] values directly to
11//! [`ClientBuilder::beta`](crate::ClientBuilder::beta) -- the
12//! `impl Into<String>` bound is satisfied via [`From<BetaHeader> for
13//! String`].
14//!
15//! ```ignore
16//! use claude_api::{Client, BetaHeader};
17//!
18//! let client = Client::builder()
19//!     .api_key("sk-ant-...")
20//!     .beta(BetaHeader::Skills)
21//!     .beta(BetaHeader::UserProfiles)
22//!     .build()?;
23//! # Ok::<(), claude_api::Error>(())
24//! ```
25
26use serde::{Deserialize, Serialize};
27
28/// Canonical Anthropic beta header values.
29///
30/// This list is derived from the public API reference and may grow
31/// over time. New values added by the API will deserialize into
32/// [`Self::Other`] until this enum is updated.
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub enum BetaHeader {
35    /// `message-batches-2024-09-24`
36    MessageBatches,
37    /// `prompt-caching-2024-07-31`
38    PromptCaching,
39    /// `computer-use-2024-10-22`
40    ComputerUse20241022,
41    /// `computer-use-2025-01-24`
42    ComputerUse20250124,
43    /// `pdfs-2024-09-25`
44    Pdfs,
45    /// `token-counting-2024-11-01`
46    TokenCounting,
47    /// `token-efficient-tools-2025-02-19`
48    TokenEfficientTools,
49    /// `output-128k-2025-02-19`
50    Output128k,
51    /// `files-api-2025-04-14`
52    FilesApi,
53    /// `mcp-client-2025-04-04`
54    McpClient20250404,
55    /// `mcp-client-2025-11-20`
56    McpClient20251120,
57    /// `dev-full-thinking-2025-05-14`
58    DevFullThinking,
59    /// `interleaved-thinking-2025-05-14`
60    InterleavedThinking,
61    /// `code-execution-2025-05-22`
62    CodeExecution,
63    /// `extended-cache-ttl-2025-04-11`
64    ExtendedCacheTtl,
65    /// `context-1m-2025-08-07`
66    Context1m,
67    /// `context-management-2025-06-27`
68    ContextManagement,
69    /// `model-context-window-exceeded-2025-08-26`
70    ModelContextWindowExceeded,
71    /// `skills-2025-10-02`
72    Skills,
73    /// `fast-mode-2026-02-01`
74    FastMode,
75    /// `output-300k-2026-03-24`
76    Output300k,
77    /// `user-profiles-2026-03-24`
78    UserProfiles,
79    /// `advisor-tool-2026-03-01`
80    AdvisorTool,
81    /// Forward-compat fallback for beta headers added by Anthropic
82    /// after this enum was last updated. Round-trips byte-for-byte.
83    Other(String),
84}
85
86impl BetaHeader {
87    /// Wire-format string sent in the `anthropic-beta` HTTP header.
88    #[must_use]
89    pub fn as_str(&self) -> &str {
90        match self {
91            Self::MessageBatches => "message-batches-2024-09-24",
92            Self::PromptCaching => "prompt-caching-2024-07-31",
93            Self::ComputerUse20241022 => "computer-use-2024-10-22",
94            Self::ComputerUse20250124 => "computer-use-2025-01-24",
95            Self::Pdfs => "pdfs-2024-09-25",
96            Self::TokenCounting => "token-counting-2024-11-01",
97            Self::TokenEfficientTools => "token-efficient-tools-2025-02-19",
98            Self::Output128k => "output-128k-2025-02-19",
99            Self::FilesApi => "files-api-2025-04-14",
100            Self::McpClient20250404 => "mcp-client-2025-04-04",
101            Self::McpClient20251120 => "mcp-client-2025-11-20",
102            Self::DevFullThinking => "dev-full-thinking-2025-05-14",
103            Self::InterleavedThinking => "interleaved-thinking-2025-05-14",
104            Self::CodeExecution => "code-execution-2025-05-22",
105            Self::ExtendedCacheTtl => "extended-cache-ttl-2025-04-11",
106            Self::Context1m => "context-1m-2025-08-07",
107            Self::ContextManagement => "context-management-2025-06-27",
108            Self::ModelContextWindowExceeded => "model-context-window-exceeded-2025-08-26",
109            Self::Skills => "skills-2025-10-02",
110            Self::FastMode => "fast-mode-2026-02-01",
111            Self::Output300k => "output-300k-2026-03-24",
112            Self::UserProfiles => "user-profiles-2026-03-24",
113            Self::AdvisorTool => "advisor-tool-2026-03-01",
114            Self::Other(v) => v,
115        }
116    }
117
118    /// Whether this is a recognized known variant (not [`Self::Other`]).
119    #[must_use]
120    pub fn is_known(&self) -> bool {
121        !matches!(self, Self::Other(_))
122    }
123}
124
125impl std::fmt::Display for BetaHeader {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.write_str(self.as_str())
128    }
129}
130
131impl From<BetaHeader> for String {
132    fn from(b: BetaHeader) -> Self {
133        b.as_str().to_owned()
134    }
135}
136
137impl From<&BetaHeader> for String {
138    fn from(b: &BetaHeader) -> Self {
139        b.as_str().to_owned()
140    }
141}
142
143impl From<String> for BetaHeader {
144    fn from(s: String) -> Self {
145        Self::from_wire(&s).unwrap_or(Self::Other(s))
146    }
147}
148
149impl From<&str> for BetaHeader {
150    fn from(s: &str) -> Self {
151        Self::from_wire(s).unwrap_or_else(|| Self::Other(s.to_owned()))
152    }
153}
154
155impl BetaHeader {
156    /// Parse a wire-format string into a known variant. Returns `None`
157    /// for unknown values; callers wrap into [`Self::Other`].
158    fn from_wire(s: &str) -> Option<Self> {
159        Some(match s {
160            "message-batches-2024-09-24" => Self::MessageBatches,
161            "prompt-caching-2024-07-31" => Self::PromptCaching,
162            "computer-use-2024-10-22" => Self::ComputerUse20241022,
163            "computer-use-2025-01-24" => Self::ComputerUse20250124,
164            "pdfs-2024-09-25" => Self::Pdfs,
165            "token-counting-2024-11-01" => Self::TokenCounting,
166            "token-efficient-tools-2025-02-19" => Self::TokenEfficientTools,
167            "output-128k-2025-02-19" => Self::Output128k,
168            "files-api-2025-04-14" => Self::FilesApi,
169            "mcp-client-2025-04-04" => Self::McpClient20250404,
170            "mcp-client-2025-11-20" => Self::McpClient20251120,
171            "dev-full-thinking-2025-05-14" => Self::DevFullThinking,
172            "interleaved-thinking-2025-05-14" => Self::InterleavedThinking,
173            "code-execution-2025-05-22" => Self::CodeExecution,
174            "extended-cache-ttl-2025-04-11" => Self::ExtendedCacheTtl,
175            "context-1m-2025-08-07" => Self::Context1m,
176            "context-management-2025-06-27" => Self::ContextManagement,
177            "model-context-window-exceeded-2025-08-26" => Self::ModelContextWindowExceeded,
178            "skills-2025-10-02" => Self::Skills,
179            "fast-mode-2026-02-01" => Self::FastMode,
180            "output-300k-2026-03-24" => Self::Output300k,
181            "user-profiles-2026-03-24" => Self::UserProfiles,
182            "advisor-tool-2026-03-01" => Self::AdvisorTool,
183            _ => return None,
184        })
185    }
186}
187
188impl Serialize for BetaHeader {
189    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
190        s.serialize_str(self.as_str())
191    }
192}
193
194impl<'de> Deserialize<'de> for BetaHeader {
195    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
196        let s = String::deserialize(d)?;
197        Ok(Self::from(s))
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use pretty_assertions::assert_eq;
205
206    /// All 23 canonical wire strings, in the order they appear on the
207    /// API reference. Pin against drift.
208    const ALL_WIRE: &[&str] = &[
209        "message-batches-2024-09-24",
210        "prompt-caching-2024-07-31",
211        "computer-use-2024-10-22",
212        "computer-use-2025-01-24",
213        "pdfs-2024-09-25",
214        "token-counting-2024-11-01",
215        "token-efficient-tools-2025-02-19",
216        "output-128k-2025-02-19",
217        "files-api-2025-04-14",
218        "mcp-client-2025-04-04",
219        "mcp-client-2025-11-20",
220        "dev-full-thinking-2025-05-14",
221        "interleaved-thinking-2025-05-14",
222        "code-execution-2025-05-22",
223        "extended-cache-ttl-2025-04-11",
224        "context-1m-2025-08-07",
225        "context-management-2025-06-27",
226        "model-context-window-exceeded-2025-08-26",
227        "skills-2025-10-02",
228        "fast-mode-2026-02-01",
229        "output-300k-2026-03-24",
230        "user-profiles-2026-03-24",
231        "advisor-tool-2026-03-01",
232    ];
233
234    #[test]
235    fn every_known_wire_value_round_trips_through_from_and_as_str() {
236        for &wire in ALL_WIRE {
237            let parsed = BetaHeader::from(wire);
238            assert!(
239                parsed.is_known(),
240                "expected {wire} to parse to a known variant, got {parsed:?}"
241            );
242            assert_eq!(parsed.as_str(), wire, "round-trip mismatch on {wire}");
243        }
244    }
245
246    #[test]
247    fn unknown_values_fall_through_to_other_and_round_trip() {
248        let unknown = BetaHeader::from("brand-new-beta-2099-12-31");
249        assert!(!unknown.is_known());
250        assert_eq!(unknown.as_str(), "brand-new-beta-2099-12-31");
251        let s: String = unknown.clone().into();
252        assert_eq!(s, "brand-new-beta-2099-12-31");
253    }
254
255    #[test]
256    fn serde_round_trips_known_and_unknown_values() {
257        let known = BetaHeader::Skills;
258        let json = serde_json::to_string(&known).unwrap();
259        assert_eq!(json, r#""skills-2025-10-02""#);
260        let back: BetaHeader = serde_json::from_str(&json).unwrap();
261        assert_eq!(back, BetaHeader::Skills);
262
263        let unknown = BetaHeader::from("custom-x");
264        let json = serde_json::to_string(&unknown).unwrap();
265        assert_eq!(json, r#""custom-x""#);
266        let back: BetaHeader = serde_json::from_str(&json).unwrap();
267        assert_eq!(back, BetaHeader::Other("custom-x".into()));
268    }
269
270    #[test]
271    fn display_matches_wire_format() {
272        assert_eq!(format!("{}", BetaHeader::Skills), "skills-2025-10-02");
273        assert_eq!(
274            format!("{}", BetaHeader::UserProfiles),
275            "user-profiles-2026-03-24"
276        );
277    }
278
279    #[test]
280    fn into_string_works_for_client_builder_beta() {
281        // Simulates what `client.beta(BetaHeader::Skills)` does internally
282        // via `impl Into<String>`.
283        let v: String = BetaHeader::Skills.into();
284        assert_eq!(v, "skills-2025-10-02");
285        let v: String = (&BetaHeader::UserProfiles).into();
286        assert_eq!(v, "user-profiles-2026-03-24");
287    }
288}