systemprompt_wire/anthropic/
beta.rs1use std::collections::BTreeSet;
22use std::fmt;
23
24use serde::{Deserialize, Serialize};
25use serde_json::{Map, Value};
27
28pub const ANTHROPIC_BETA_HEADER: &str = "anthropic-beta";
29
30#[derive(
31 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
32)]
33#[serde(transparent)]
34pub struct AnthropicBeta(String);
35
36impl AnthropicBeta {
37 #[must_use]
38 pub fn new(flag: impl Into<String>) -> Self {
39 Self(flag.into())
40 }
41
42 #[must_use]
43 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46}
47
48impl fmt::Display for AnthropicBeta {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.write_str(&self.0)
51 }
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
56pub struct BetaHeader(Vec<AnthropicBeta>);
57
58impl BetaHeader {
59 #[must_use]
60 pub fn parse(value: &str) -> Self {
61 let mut betas: Vec<AnthropicBeta> = Vec::new();
62 for flag in value.split(',').map(str::trim).filter(|f| !f.is_empty()) {
63 if !betas.iter().any(|b| b.as_str() == flag) {
64 betas.push(AnthropicBeta::new(flag));
65 }
66 }
67 Self(betas)
68 }
69
70 #[must_use]
71 pub fn admitted_by(self, policy: &BetaPolicy) -> Self {
72 Self(self.0.into_iter().filter(|b| policy.admits(b)).collect())
73 }
74
75 #[must_use]
76 pub fn refused_by(self, policy: &BetaPolicy) -> Self {
77 Self(self.0.into_iter().filter(|b| !policy.admits(b)).collect())
78 }
79
80 pub fn extend(&mut self, other: Self) {
81 for beta in other.0 {
82 if !self.0.iter().any(|b| b == &beta) {
83 self.0.push(beta);
84 }
85 }
86 }
87
88 #[must_use]
89 pub fn contains(&self, flag: &str) -> bool {
90 self.0.iter().any(|b| b.as_str() == flag)
91 }
92
93 #[must_use]
94 pub fn opens(&self, gate: &BetaGatedField) -> bool {
95 self.0
96 .iter()
97 .any(|b| b.as_str().starts_with(gate.beta_prefix))
98 }
99
100 #[must_use]
101 pub const fn is_empty(&self) -> bool {
102 self.0.is_empty()
103 }
104
105 #[must_use]
106 pub fn betas(&self) -> &[AnthropicBeta] {
107 &self.0
108 }
109
110 #[must_use]
111 pub fn render(&self) -> Option<String> {
112 (!self.0.is_empty()).then(|| {
113 self.0
114 .iter()
115 .map(AnthropicBeta::as_str)
116 .collect::<Vec<_>>()
117 .join(",")
118 })
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum BetaPolicy {
125 ForwardAll,
126 Only(BTreeSet<AnthropicBeta>),
127}
128
129impl BetaPolicy {
130 #[must_use]
131 pub fn admits(&self, beta: &AnthropicBeta) -> bool {
132 match self {
133 Self::ForwardAll => true,
134 Self::Only(accepted) => accepted.contains(beta),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct BetaGatedField {
144 pub field: &'static str,
145 pub beta_prefix: &'static str,
146}
147
148pub const BETA_GATED_FIELDS: &[BetaGatedField] = &[
149 BetaGatedField {
150 field: "context_management",
151 beta_prefix: "context-management-",
152 },
153 BetaGatedField {
154 field: "mcp_servers",
155 beta_prefix: "mcp-client-",
156 },
157 BetaGatedField {
158 field: "container",
159 beta_prefix: "code-execution-",
160 },
161];
162
163pub fn strip_fields_gated_by(
165 body: &mut Map<String, Value>,
166 dropped: &BetaHeader,
167) -> Vec<&'static str> {
168 BETA_GATED_FIELDS
169 .iter()
170 .filter(|gate| dropped.opens(gate) && body.remove(gate.field).is_some())
171 .map(|gate| gate.field)
172 .collect()
173}