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};
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 facts = target
245 .facts()
246 .unwrap_or_else(|| crate::catalog::ModelFacts::builtin());
247 let Some(spec) = facts.for_model(target.provider(), model) else {
248 return Vec::new();
249 };
250 let mut refused = Vec::new();
251 if self.shared.top_k.is_some() && spec.sampling == Some(Sampling::Never) {
252 refused.push(("top_k", "this model does not take `top_k`".to_owned()));
253 }
254 if self.shared.speed == Some(Speed::Fast) && !FAST_MODELS.contains(&spec.id.as_str()) {
255 refused.push((
256 "speed",
257 "fast mode runs on Claude Opus 5.5, Opus 5 and Opus 4.8 only".to_owned(),
258 ));
259 }
260 refused
261 }
262}
263
264#[non_exhaustive]
266#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
267#[serde(rename_all = "lowercase")]
268pub enum InferenceGeo {
269 Us,
271 Global,
273}
274
275#[non_exhaustive]
277#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
278#[serde(rename_all = "lowercase")]
279pub enum Speed {
280 Standard,
282 Fast,
284}
285
286#[non_exhaustive]
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub struct TaskBudget {
291 pub total: u32,
293 pub remaining: Option<u32>,
296}
297
298impl TaskBudget {
299 pub fn new(total: u32) -> Self {
301 Self {
302 total,
303 remaining: None,
304 }
305 }
306
307 pub fn remaining(mut self, remaining: u32) -> Self {
309 self.remaining = Some(remaining);
310 self
311 }
312}
313
314impl Serialize for TaskBudget {
315 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
316 let Self { total, remaining } = self;
317 let mut map = serializer.serialize_map(None)?;
318 map.serialize_entry("type", "tokens")?;
319 map.serialize_entry("total", total)?;
320 if let Some(remaining) = remaining {
321 map.serialize_entry("remaining", remaining)?;
322 }
323 map.end()
324 }
325}
326
327#[non_exhaustive]
329#[derive(Clone, Debug, PartialEq, Eq)]
330pub enum Fallbacks {
331 Default,
334 Models(Vec<String>),
337}
338
339impl Serialize for Fallbacks {
340 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
341 match self {
342 Self::Default => serializer.serialize_str("default"),
343 Self::Models(models) => {
344 serializer.collect_seq(models.iter().map(|model| Nested("model", model)))
345 }
346 }
347 }
348}
349
350#[non_exhaustive]
352#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
353#[serde(untagged)]
354pub enum ContainerParam {
355 Id(String),
357 Skills {
359 #[serde(skip_serializing_if = "Option::is_none")]
361 id: Option<String>,
362 skills: Vec<Skill>,
364 },
365}
366
367impl From<String> for ContainerParam {
368 fn from(id: String) -> Self {
369 Self::Id(id)
370 }
371}
372
373impl From<&str> for ContainerParam {
374 fn from(id: &str) -> Self {
375 Self::Id(id.to_owned())
376 }
377}
378
379#[non_exhaustive]
381#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
382pub struct Skill {
383 #[serde(rename = "type")]
385 pub kind: SkillKind,
386 pub skill_id: String,
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub version: Option<String>,
391}
392
393impl Skill {
394 pub fn new(kind: SkillKind, skill_id: impl Into<String>) -> Self {
396 Self {
397 kind,
398 skill_id: skill_id.into(),
399 version: None,
400 }
401 }
402
403 pub fn version(mut self, version: impl Into<String>) -> Self {
405 self.version = Some(version.into());
406 self
407 }
408}
409
410#[non_exhaustive]
412#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
413#[serde(rename_all = "lowercase")]
414pub enum SkillKind {
415 Anthropic,
417 Custom,
419}
420
421#[non_exhaustive]
424#[derive(Clone, Debug, Default, PartialEq, Serialize)]
425pub struct ContextManagement {
426 pub edits: Vec<Value>,
430}
431
432impl ContextManagement {
433 pub fn new(edits: Vec<Value>) -> Self {
435 Self { edits }
436 }
437}
438
439#[non_exhaustive]
441#[derive(Clone, Debug, PartialEq, Eq)]
442pub struct McpServer {
443 pub url: String,
445 pub name: String,
447 pub authorization_token: Option<String>,
449}
450
451impl McpServer {
452 pub fn new(url: impl Into<String>, name: impl Into<String>) -> Self {
454 Self {
455 url: url.into(),
456 name: name.into(),
457 authorization_token: None,
458 }
459 }
460
461 pub fn authorization_token(mut self, token: impl Into<String>) -> Self {
463 self.authorization_token = Some(token.into());
464 self
465 }
466}
467
468impl Serialize for McpServer {
469 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
470 let Self {
471 url,
472 name,
473 authorization_token,
474 } = self;
475 let mut map = serializer.serialize_map(None)?;
476 map.serialize_entry("type", "url")?;
477 map.serialize_entry("url", url)?;
478 map.serialize_entry("name", name)?;
479 if let Some(token) = authorization_token {
480 map.serialize_entry("authorization_token", token)?;
481 }
482 map.end()
483 }
484}
485
486#[non_exhaustive]
489#[derive(Clone, Debug, Default, PartialEq)]
490pub struct AnthropicExtras {
491 pub stop_reason: Option<String>,
493 pub stop_sequence: Option<String>,
495 pub stop_details: Option<StopDetails>,
497 pub cache_creation: Option<CacheCreation>,
499 pub service_tier: Option<String>,
501 pub inference_geo: Option<String>,
503 pub speed: Option<String>,
505 pub server_tool_use: Option<ServerToolUse>,
507 pub container: Option<Container>,
509 pub fallback_model: Option<String>,
512}
513
514#[non_exhaustive]
516#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
517pub struct StopDetails {
518 #[serde(rename = "type", default)]
520 pub kind: String,
521 #[serde(default)]
523 pub category: Option<String>,
524 #[serde(default)]
526 pub explanation: Option<String>,
527}
528
529#[non_exhaustive]
531#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
532pub struct CacheCreation {
533 #[serde(default)]
535 pub ephemeral_5m_input_tokens: u64,
536 #[serde(default)]
538 pub ephemeral_1h_input_tokens: u64,
539}
540
541#[non_exhaustive]
543#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
544pub struct ServerToolUse {
545 #[serde(default)]
547 pub web_search_requests: u64,
548 #[serde(default)]
550 pub web_fetch_requests: u64,
551}
552
553#[non_exhaustive]
555#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
556pub struct Container {
557 #[serde(default)]
559 pub id: String,
560 #[serde(default)]
562 pub expires_at: String,
563}
564
565#[derive(Deserialize)]
567struct Reply {
568 #[serde(default)]
569 stop_reason: Option<String>,
570 #[serde(default)]
571 stop_sequence: Option<String>,
572 #[serde(default)]
573 stop_details: Option<StopDetails>,
574 #[serde(default)]
575 usage: Option<ReplyUsage>,
576 #[serde(default)]
577 container: Option<Container>,
578 #[serde(default, rename = "content")]
580 _content: Vec<serde::de::IgnoredAny>,
581}
582
583#[derive(Default, Deserialize)]
584struct ReplyUsage {
585 #[serde(default)]
586 cache_creation: Option<CacheCreation>,
587 #[serde(default)]
588 service_tier: Option<String>,
589 #[serde(default)]
590 inference_geo: Option<String>,
591 #[serde(default)]
592 speed: Option<String>,
593 #[serde(default)]
594 server_tool_use: Option<ServerToolUse>,
595}
596
597impl ReplyExtras for AnthropicExtras {
598 fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
599 if api.as_str() != MESSAGES_API {
600 return Err(serde::de::Error::custom(format!(
601 "Anthropic extras read a Messages reply, not one from `{api}`"
602 )));
603 }
604 let Reply {
605 stop_reason,
606 stop_sequence,
607 stop_details,
608 usage,
609 container,
610 _content,
611 } = Reply::deserialize(raw)?;
612 let usage = usage.unwrap_or_default();
613 let fallback_model = raw
614 .pointer("/content/0")
615 .filter(|block| block.get("type").and_then(Value::as_str) == Some("fallback"))
616 .and_then(|block| block.pointer("/to/model"))
617 .and_then(Value::as_str)
618 .map(str::to_owned);
619 Ok(Self {
620 stop_reason,
621 stop_sequence,
622 stop_details,
623 cache_creation: usage.cache_creation,
624 service_tier: usage.service_tier,
625 inference_geo: usage.inference_geo,
626 speed: usage.speed,
627 server_tool_use: usage.server_tool_use,
628 container,
629 fallback_model,
630 })
631 }
632}
633
634#[derive(Deserialize)]
637pub(crate) struct MessagesStop {
638 #[serde(default)]
639 pub(crate) stop_reason: Option<String>,
640 #[serde(default)]
641 pub(crate) stop_sequence: Option<String>,
642}
643
644impl MessagesStop {
645 pub(crate) fn read(provider: &str, api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
651 if api.as_str() != MESSAGES_API {
652 return Err(serde::de::Error::custom(format!(
653 "{provider} extras read a Messages reply, not one from `{api}`"
654 )));
655 }
656 Self::deserialize(raw)
657 }
658}
659
660#[cfg(test)]
661mod tests;