1use serde::ser::SerializeMap;
22use serde::{Deserialize, Serialize, Serializer};
23use serde_json::Value;
24
25use super::completion::{CLAUDE_OPUS_4_8, CLAUDE_OPUS_5, CLAUDE_OPUS_5_5, claude_spec};
26use super::wire::MESSAGES_API;
27use crate::catalog::Sampling;
28use crate::completion::{
29 CompletionRequest, ExtensionOptions, ProviderExtension, ReplayTarget, ReplyExtras,
30};
31use crate::message::Api;
32
33pub const FAST_MODE_BETA: &str = "fast-mode-2026-02-01";
35pub const TASK_BUDGETS_BETA: &str = "task-budgets-2026-03-13";
37pub const SERVER_SIDE_FALLBACK_BETA: &str = "server-side-fallback-2026-07-01";
39pub const SERVER_SIDE_FALLBACK_MODELS_BETA: &str = "server-side-fallback-2026-06-01";
41pub const CONTEXT_MANAGEMENT_BETA: &str = "context-management-2025-06-27";
43pub const MCP_CLIENT_BETA: &str = "mcp-client-2025-11-20";
45pub const CACHE_DIAGNOSIS_BETA: &str = "cache-diagnosis-2026-04-07";
47
48const FAST_MODELS: &[&str] = &[CLAUDE_OPUS_5_5, CLAUDE_OPUS_5, CLAUDE_OPUS_4_8];
50
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub struct AnthropicExt;
54
55impl ProviderExtension for AnthropicExt {
56 const PROVIDER: &'static str = super::PROVIDER_NAME;
57 type Options = AnthropicOptions;
58 type Extras = AnthropicExtras;
59}
60
61#[non_exhaustive]
64#[derive(Clone, Debug, Default, PartialEq, Serialize)]
65pub struct AnthropicOptions {
66 #[serde(rename = "*")]
68 pub shared: AnthropicShared,
69}
70
71#[non_exhaustive]
74#[derive(Clone, Debug, Default, PartialEq)]
75pub struct AnthropicShared {
76 pub top_k: Option<u32>,
79 pub metadata_user_id: Option<String>,
82 pub inference_geo: Option<InferenceGeo>,
84 pub speed: Option<Speed>,
87 pub task_budget: Option<TaskBudget>,
89 pub fallbacks: Option<Fallbacks>,
92 pub container: Option<ContainerParam>,
95 pub context_management: Option<ContextManagement>,
97 pub mcp_servers: Vec<McpServer>,
100 pub diagnostics_previous_message_id: Option<Option<String>>,
104}
105
106impl Serialize for AnthropicShared {
107 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
108 let Self {
109 top_k,
110 metadata_user_id,
111 inference_geo,
112 speed,
113 task_budget,
114 fallbacks,
115 container,
116 context_management,
117 mcp_servers,
118 diagnostics_previous_message_id,
119 } = self;
120 let mut map = serializer.serialize_map(None)?;
121 if let Some(top_k) = top_k {
122 map.serialize_entry("top_k", top_k)?;
123 }
124 if let Some(user_id) = metadata_user_id {
125 map.serialize_entry("metadata", &Nested("user_id", user_id))?;
126 }
127 if let Some(geo) = inference_geo {
128 map.serialize_entry("inference_geo", geo)?;
129 }
130 if let Some(speed) = speed {
131 map.serialize_entry("speed", speed)?;
132 }
133 if let Some(budget) = task_budget {
134 map.serialize_entry("output_config", &Nested("task_budget", budget))?;
135 }
136 if let Some(fallbacks) = fallbacks {
137 map.serialize_entry("fallbacks", fallbacks)?;
138 }
139 if let Some(container) = container {
140 map.serialize_entry("container", container)?;
141 }
142 if let Some(context_management) = context_management {
143 map.serialize_entry("context_management", context_management)?;
144 }
145 if !mcp_servers.is_empty() {
146 map.serialize_entry("mcp_servers", mcp_servers)?;
147 }
148 if let Some(previous) = diagnostics_previous_message_id {
149 map.serialize_entry("diagnostics", &Nested("previous_message_id", previous))?;
150 }
151 map.end()
152 }
153}
154
155pub(crate) struct Nested<'a, T>(pub(crate) &'static str, pub(crate) &'a T);
157
158impl<T: Serialize> Serialize for Nested<'_, T> {
159 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
160 let mut map = serializer.serialize_map(Some(1))?;
161 map.serialize_entry(self.0, self.1)?;
162 map.end()
163 }
164}
165
166impl AnthropicOptions {
167 pub fn top_k(mut self, top_k: u32) -> Self {
169 self.shared.top_k = Some(top_k);
170 self
171 }
172
173 pub fn metadata_user_id(mut self, user_id: impl Into<String>) -> Self {
175 self.shared.metadata_user_id = Some(user_id.into());
176 self
177 }
178
179 pub fn inference_geo(mut self, geo: InferenceGeo) -> Self {
181 self.shared.inference_geo = Some(geo);
182 self
183 }
184
185 pub fn speed(mut self, speed: Speed) -> Self {
187 self.shared.speed = Some(speed);
188 self
189 }
190
191 pub fn task_budget(mut self, budget: TaskBudget) -> Self {
193 self.shared.task_budget = Some(budget);
194 self
195 }
196
197 pub fn fallbacks(mut self, fallbacks: Fallbacks) -> Self {
199 self.shared.fallbacks = Some(fallbacks);
200 self
201 }
202
203 pub fn container(mut self, container: impl Into<ContainerParam>) -> Self {
205 self.shared.container = Some(container.into());
206 self
207 }
208
209 pub fn context_management(mut self, context_management: ContextManagement) -> Self {
211 self.shared.context_management = Some(context_management);
212 self
213 }
214
215 pub fn mcp_server(mut self, server: McpServer) -> Self {
217 self.shared.mcp_servers.push(server);
218 self
219 }
220
221 pub fn diagnostics_previous_message_id(mut self, previous: Option<String>) -> Self {
223 self.shared.diagnostics_previous_message_id = Some(previous);
224 self
225 }
226}
227
228impl ExtensionOptions for AnthropicOptions {
229 type Ext = AnthropicExt;
230
231 fn unsupported(
235 &self,
236 target: &dyn ReplayTarget,
237 request: &CompletionRequest,
238 ) -> Vec<(&'static str, String)> {
239 let model = request
240 .model
241 .as_deref()
242 .filter(|model| !model.is_empty())
243 .unwrap_or_else(|| target.model());
244 let Some(spec) = claude_spec(model) else {
245 return Vec::new();
246 };
247 let mut refused = Vec::new();
248 if self.shared.top_k.is_some() && spec.sampling == Some(Sampling::Never) {
249 refused.push(("top_k", "this model does not take `top_k`".to_owned()));
250 }
251 if self.shared.speed == Some(Speed::Fast) && !FAST_MODELS.contains(&spec.id.as_str()) {
252 refused.push((
253 "speed",
254 "fast mode runs on Claude Opus 5.5, Opus 5 and Opus 4.8 only".to_owned(),
255 ));
256 }
257 refused
258 }
259}
260
261#[non_exhaustive]
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
264#[serde(rename_all = "lowercase")]
265pub enum InferenceGeo {
266 Us,
268 Global,
270}
271
272#[non_exhaustive]
274#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
275#[serde(rename_all = "lowercase")]
276pub enum Speed {
277 Standard,
279 Fast,
281}
282
283#[non_exhaustive]
286#[derive(Clone, Copy, Debug, PartialEq, Eq)]
287pub struct TaskBudget {
288 pub total: u32,
290 pub remaining: Option<u32>,
293}
294
295impl TaskBudget {
296 pub fn new(total: u32) -> Self {
298 Self {
299 total,
300 remaining: None,
301 }
302 }
303
304 pub fn remaining(mut self, remaining: u32) -> Self {
306 self.remaining = Some(remaining);
307 self
308 }
309}
310
311impl Serialize for TaskBudget {
312 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
313 let Self { total, remaining } = self;
314 let mut map = serializer.serialize_map(None)?;
315 map.serialize_entry("type", "tokens")?;
316 map.serialize_entry("total", total)?;
317 if let Some(remaining) = remaining {
318 map.serialize_entry("remaining", remaining)?;
319 }
320 map.end()
321 }
322}
323
324#[non_exhaustive]
326#[derive(Clone, Debug, PartialEq, Eq)]
327pub enum Fallbacks {
328 Default,
331 Models(Vec<String>),
334}
335
336impl Serialize for Fallbacks {
337 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
338 match self {
339 Self::Default => serializer.serialize_str("default"),
340 Self::Models(models) => {
341 serializer.collect_seq(models.iter().map(|model| Nested("model", model)))
342 }
343 }
344 }
345}
346
347#[non_exhaustive]
349#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
350#[serde(untagged)]
351pub enum ContainerParam {
352 Id(String),
354 Skills {
356 #[serde(skip_serializing_if = "Option::is_none")]
358 id: Option<String>,
359 skills: Vec<Skill>,
361 },
362}
363
364impl From<String> for ContainerParam {
365 fn from(id: String) -> Self {
366 Self::Id(id)
367 }
368}
369
370impl From<&str> for ContainerParam {
371 fn from(id: &str) -> Self {
372 Self::Id(id.to_owned())
373 }
374}
375
376#[non_exhaustive]
378#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
379pub struct Skill {
380 #[serde(rename = "type")]
382 pub kind: SkillKind,
383 pub skill_id: String,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub version: Option<String>,
388}
389
390impl Skill {
391 pub fn new(kind: SkillKind, skill_id: impl Into<String>) -> Self {
393 Self {
394 kind,
395 skill_id: skill_id.into(),
396 version: None,
397 }
398 }
399
400 pub fn version(mut self, version: impl Into<String>) -> Self {
402 self.version = Some(version.into());
403 self
404 }
405}
406
407#[non_exhaustive]
409#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
410#[serde(rename_all = "lowercase")]
411pub enum SkillKind {
412 Anthropic,
414 Custom,
416}
417
418#[non_exhaustive]
421#[derive(Clone, Debug, Default, PartialEq, Serialize)]
422pub struct ContextManagement {
423 pub edits: Vec<Value>,
427}
428
429impl ContextManagement {
430 pub fn new(edits: Vec<Value>) -> Self {
432 Self { edits }
433 }
434}
435
436#[non_exhaustive]
438#[derive(Clone, Debug, PartialEq, Eq)]
439pub struct McpServer {
440 pub url: String,
442 pub name: String,
444 pub authorization_token: Option<String>,
446}
447
448impl McpServer {
449 pub fn new(url: impl Into<String>, name: impl Into<String>) -> Self {
451 Self {
452 url: url.into(),
453 name: name.into(),
454 authorization_token: None,
455 }
456 }
457
458 pub fn authorization_token(mut self, token: impl Into<String>) -> Self {
460 self.authorization_token = Some(token.into());
461 self
462 }
463}
464
465impl Serialize for McpServer {
466 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
467 let Self {
468 url,
469 name,
470 authorization_token,
471 } = self;
472 let mut map = serializer.serialize_map(None)?;
473 map.serialize_entry("type", "url")?;
474 map.serialize_entry("url", url)?;
475 map.serialize_entry("name", name)?;
476 if let Some(token) = authorization_token {
477 map.serialize_entry("authorization_token", token)?;
478 }
479 map.end()
480 }
481}
482
483#[non_exhaustive]
486#[derive(Clone, Debug, Default, PartialEq)]
487pub struct AnthropicExtras {
488 pub stop_reason: Option<String>,
490 pub stop_sequence: Option<String>,
492 pub stop_details: Option<StopDetails>,
494 pub cache_creation: Option<CacheCreation>,
496 pub service_tier: Option<String>,
498 pub inference_geo: Option<String>,
500 pub speed: Option<String>,
502 pub server_tool_use: Option<ServerToolUse>,
504 pub container: Option<Container>,
506 pub fallback_model: Option<String>,
509}
510
511#[non_exhaustive]
513#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
514pub struct StopDetails {
515 #[serde(rename = "type", default)]
517 pub kind: String,
518 #[serde(default)]
520 pub category: Option<String>,
521 #[serde(default)]
523 pub explanation: Option<String>,
524}
525
526#[non_exhaustive]
528#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
529pub struct CacheCreation {
530 #[serde(default)]
532 pub ephemeral_5m_input_tokens: u64,
533 #[serde(default)]
535 pub ephemeral_1h_input_tokens: u64,
536}
537
538#[non_exhaustive]
540#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
541pub struct ServerToolUse {
542 #[serde(default)]
544 pub web_search_requests: u64,
545 #[serde(default)]
547 pub web_fetch_requests: u64,
548}
549
550#[non_exhaustive]
552#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
553pub struct Container {
554 #[serde(default)]
556 pub id: String,
557 #[serde(default)]
559 pub expires_at: String,
560}
561
562#[derive(Deserialize)]
564struct Reply {
565 #[serde(default)]
566 stop_reason: Option<String>,
567 #[serde(default)]
568 stop_sequence: Option<String>,
569 #[serde(default)]
570 stop_details: Option<StopDetails>,
571 #[serde(default)]
572 usage: Option<ReplyUsage>,
573 #[serde(default)]
574 container: Option<Container>,
575 #[serde(default)]
576 content: Vec<Value>,
577}
578
579#[derive(Default, Deserialize)]
580struct ReplyUsage {
581 #[serde(default)]
582 cache_creation: Option<CacheCreation>,
583 #[serde(default)]
584 service_tier: Option<String>,
585 #[serde(default)]
586 inference_geo: Option<String>,
587 #[serde(default)]
588 speed: Option<String>,
589 #[serde(default)]
590 server_tool_use: Option<ServerToolUse>,
591}
592
593impl ReplyExtras for AnthropicExtras {
594 fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
595 if api.as_str() != MESSAGES_API {
596 return Err(serde::de::Error::custom(format!(
597 "Anthropic extras read a Messages reply, not one from `{api}`"
598 )));
599 }
600 let Reply {
601 stop_reason,
602 stop_sequence,
603 stop_details,
604 usage,
605 container,
606 content,
607 } = Reply::deserialize(raw)?;
608 let usage = usage.unwrap_or_default();
609 let fallback_model = content
610 .first()
611 .filter(|block| block.get("type").and_then(Value::as_str) == Some("fallback"))
612 .and_then(|block| block.pointer("/to/model"))
613 .and_then(Value::as_str)
614 .map(str::to_owned);
615 Ok(Self {
616 stop_reason,
617 stop_sequence,
618 stop_details,
619 cache_creation: usage.cache_creation,
620 service_tier: usage.service_tier,
621 inference_geo: usage.inference_geo,
622 speed: usage.speed,
623 server_tool_use: usage.server_tool_use,
624 container,
625 fallback_model,
626 })
627 }
628}
629
630#[derive(Deserialize)]
633pub(crate) struct MessagesStop {
634 #[serde(default)]
635 pub(crate) stop_reason: Option<String>,
636 #[serde(default)]
637 pub(crate) stop_sequence: Option<String>,
638}
639
640impl MessagesStop {
641 pub(crate) fn read(provider: &str, api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
647 if api.as_str() != MESSAGES_API {
648 return Err(serde::de::Error::custom(format!(
649 "{provider} extras read a Messages reply, not one from `{api}`"
650 )));
651 }
652 Self::deserialize(raw)
653 }
654}
655
656#[cfg(test)]
657mod tests;