| @@ -2,13 +2,13 @@ use std::io; · 1 unmodified line |
| 2 | use std::str::FromStr; | 2 | use std::str::FromStr; |
| 3 | use std::time::Duration; | 3 | use std::time::Duration; |
| 4 | | 4 | |
| 5 | use anyhow::{Context as _, Result, anyhow}; | 5 | use anyhow::{Context as _, Result}; |
| 6 | use chrono::{DateTime, Utc}; | 6 | use chrono::{DateTime, Utc}; |
| 7 | use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; | 7 | use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; |
| 8 | use http_client::http::{self, HeaderMap, HeaderValue}; | 8 | use http_client::http::{self, HeaderMap, HeaderValue}; |
| 9 | use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode}; | 9 | use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode}; |
| 10 | use serde::{Deserialize, Serialize}; | 10 | use serde::{Deserialize, Serialize}; |
| 11 | use strum::{EnumIter, EnumString}; | 11 | use strum::EnumString; |
| 12 | use thiserror::Error; | 12 | use thiserror::Error; |
| 13 | | 13 | |
| 14 | pub mod batches; | 14 | pub mod batches; |
| @@ -16,14 +16,6 @@ pub mod completion; · 1 unmodified line |
| 16 | | 16 | |
| 17 | pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com"; | 17 | pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com"; |
| 18 | | 18 | |
| 19 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | | |
| 20 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] | | |
| 21 | pub struct AnthropicModelCacheConfiguration { | | |
| 22 | pub min_total_token: u64, | | |
| 23 | pub should_speculate: bool, | | |
| 24 | pub max_cache_anchors: usize, | | |
| 25 | } | | |
| 26 | | | |
| 27 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | 19 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
| 28 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] | 20 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] |
| 29 | pub enum AnthropicModelMode { | 21 | pub enum AnthropicModelMode { |
| @@ -35,343 +27,152 @@ pub enum AnthropicModelMode { · 5 unmodified lines |
| 35 | AdaptiveThinking, | 27 | AdaptiveThinking, |
| 36 | } | 28 | } |
| 37 | | 29 | |
| 38 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | 30 | /// Capabilities reported by the Anthropic models endpoint for a given model. |
| 39 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] | 31 | #[derive(Clone, Debug, Default, Deserialize)] |
| 40 | pub enum Model { | 32 | pub struct ModelCapabilities { |
| 41 | #[serde( | 33 | #[serde(default)] |
| 42 | rename = "claude-opus-4", | 34 | pub thinking: Option<ThinkingCapability>, |
| 43 | alias = "claude-opus-4-latest", | 35 | #[serde(default)] |
| 44 | alias = "claude-opus-4-thinking", | 36 | pub image_input: Option<SupportedCapability>, |
| 45 | alias = "claude-opus-4-thinking-latest" | 37 | #[serde(default)] |
| 46 | )] | 38 | pub effort: Option<EffortCapability>, |
| 47 | ClaudeOpus4, | | |
| 48 | #[serde( | | |
| 49 | rename = "claude-opus-4-1", | | |
| 50 | alias = "claude-opus-4-1-latest", | | |
| 51 | alias = "claude-opus-4-1-thinking", | | |
| 52 | alias = "claude-opus-4-1-thinking-latest" | | |
| 53 | )] | | |
| 54 | ClaudeOpus4_1, | | |
| 55 | #[serde( | | |
| 56 | rename = "claude-opus-4-5", | | |
| 57 | alias = "claude-opus-4-5-latest", | | |
| 58 | alias = "claude-opus-4-5-thinking", | | |
| 59 | alias = "claude-opus-4-5-thinking-latest" | | |
| 60 | )] | | |
| 61 | ClaudeOpus4_5, | | |
| 62 | #[serde( | | |
| 63 | rename = "claude-opus-4-6", | | |
| 64 | alias = "claude-opus-4-6-latest", | | |
| 65 | alias = "claude-opus-4-6-1m-context", | | |
| 66 | alias = "claude-opus-4-6-1m-context-latest", | | |
| 67 | alias = "claude-opus-4-6-thinking", | | |
| 68 | alias = "claude-opus-4-6-thinking-latest", | | |
| 69 | alias = "claude-opus-4-6-1m-context-thinking", | | |
| 70 | alias = "claude-opus-4-6-1m-context-thinking-latest" | | |
| 71 | )] | | |
| 72 | ClaudeOpus4_6, | | |
| 73 | #[serde( | | |
| 74 | rename = "claude-opus-4-7", | | |
| 75 | alias = "claude-opus-4-7-latest", | | |
| 76 | alias = "claude-opus-4-7-1m-context", | | |
| 77 | alias = "claude-opus-4-7-1m-context-latest", | | |
| 78 | alias = "claude-opus-4-7-thinking", | | |
| 79 | alias = "claude-opus-4-7-thinking-latest", | | |
| 80 | alias = "claude-opus-4-7-1m-context-thinking", | | |
| 81 | alias = "claude-opus-4-7-1m-context-thinking-latest" | | |
| 82 | )] | | |
| 83 | ClaudeOpus4_7, | | |
| 84 | #[serde( | | |
| 85 | rename = "claude-sonnet-4", | | |
| 86 | alias = "claude-sonnet-4-latest", | | |
| 87 | alias = "claude-sonnet-4-thinking", | | |
| 88 | alias = "claude-sonnet-4-thinking-latest" | | |
| 89 | )] | | |
| 90 | ClaudeSonnet4, | | |
| 91 | #[serde( | | |
| 92 | rename = "claude-sonnet-4-5", | | |
| 93 | alias = "claude-sonnet-4-5-latest", | | |
| 94 | alias = "claude-sonnet-4-5-thinking", | | |
| 95 | alias = "claude-sonnet-4-5-thinking-latest" | | |
| 96 | )] | | |
| 97 | ClaudeSonnet4_5, | | |
| 98 | #[default] | | |
| 99 | #[serde( | | |
| 100 | rename = "claude-sonnet-4-6", | | |
| 101 | alias = "claude-sonnet-4-6-latest", | | |
| 102 | alias = "claude-sonnet-4-6-1m-context", | | |
| 103 | alias = "claude-sonnet-4-6-1m-context-latest", | | |
| 104 | alias = "claude-sonnet-4-6-thinking", | | |
| 105 | alias = "claude-sonnet-4-6-thinking-latest", | | |
| 106 | alias = "claude-sonnet-4-6-1m-context-thinking", | | |
| 107 | alias = "claude-sonnet-4-6-1m-context-thinking-latest" | | |
| 108 | )] | | |
| 109 | ClaudeSonnet4_6, | | |
| 110 | #[serde( | | |
| 111 | rename = "claude-haiku-4-5", | | |
| 112 | alias = "claude-haiku-4-5-latest", | | |
| 113 | alias = "claude-haiku-4-5-thinking", | | |
| 114 | alias = "claude-haiku-4-5-thinking-latest" | | |
| 115 | )] | | |
| 116 | ClaudeHaiku4_5, | | |
| 117 | #[serde(rename = "claude-3-haiku", alias = "claude-3-haiku-latest")] | | |
| 118 | Claude3Haiku, | | |
| 119 | #[serde(rename = "custom")] | | |
| 120 | Custom { | | |
| 121 | name: String, | | |
| 122 | max_tokens: u64, | | |
| 123 | /// The name displayed in the UI, such as in the agent panel model dropdown menu. | | |
| 124 | display_name: Option<String>, | | |
| 125 | /// Override this model with a different Anthropic model for tool calls. | | |
| 126 | tool_override: Option<String>, | | |
| 127 | /// Indicates whether this custom model supports caching. | | |
| 128 | cache_configuration: Option<AnthropicModelCacheConfiguration>, | | |
| 129 | max_output_tokens: Option<u64>, | | |
| 130 | default_temperature: Option<f32>, | | |
| 131 | #[serde(default)] | | |
| 132 | extra_beta_headers: Vec<String>, | | |
| 133 | #[serde(default)] | | |
| 134 | mode: AnthropicModelMode, | | |
| 135 | }, | | |
| 136 | } | 39 | } |
| 137 | | 40 | |
| 138 | impl Model { | 41 | #[derive(Clone, Debug, Default, Deserialize)] |
| 139 | pub fn default_fast() -> Self { | 42 | pub struct SupportedCapability { |
| 140 | Self::ClaudeHaiku4_5 | 43 | #[serde(default)] |
| 141 | } | 44 | pub supported: bool, |
| 142 | | 45 | } |
| 143 | pub fn from_id(id: &str) -> Result<Self> { | | |
| 144 | if id.starts_with("claude-opus-4-7") { | | |
| 145 | return Ok(Self::ClaudeOpus4_7); | | |
| 146 | } | | |
| 147 | | | |
| 148 | if id.starts_with("claude-opus-4-6") { | | |
| 149 | return Ok(Self::ClaudeOpus4_6); | | |
| 150 | } | | |
| 151 | | | |
| 152 | if id.starts_with("claude-opus-4-5") { | | |
| 153 | return Ok(Self::ClaudeOpus4_5); | | |
| 154 | } | | |
| 155 | | | |
| 156 | if id.starts_with("claude-opus-4-1") { | | |
| 157 | return Ok(Self::ClaudeOpus4_1); | | |
| 158 | } | | |
| 159 | | | |
| 160 | if id.starts_with("claude-opus-4") { | | |
| 161 | return Ok(Self::ClaudeOpus4); | | |
| 162 | } | | |
| 163 | | | |
| 164 | if id.starts_with("claude-sonnet-4-6") { | | |
| 165 | return Ok(Self::ClaudeSonnet4_6); | | |
| 166 | } | | |
| 167 | | | |
| 168 | if id.starts_with("claude-sonnet-4-5") { | | |
| 169 | return Ok(Self::ClaudeSonnet4_5); | | |
| 170 | } | | |
| 171 | | | |
| 172 | if id.starts_with("claude-sonnet-4") { | | |
| 173 | return Ok(Self::ClaudeSonnet4); | | |
| 174 | } | | |
| 175 | | | |
| 176 | if id.starts_with("claude-haiku-4-5") { | | |
| 177 | return Ok(Self::ClaudeHaiku4_5); | | |
| 178 | } | | |
| 179 | | | |
| 180 | if id.starts_with("claude-3-haiku") { | | |
| 181 | return Ok(Self::Claude3Haiku); | | |
| 182 | } | | |
| 183 | | | |
| 184 | Err(anyhow!("invalid model ID: {id}")) | | |
| 185 | } | | |
| 186 | | | |
| 187 | pub fn id(&self) -> &str { | | |
| 188 | match self { | | |
| 189 | Self::ClaudeOpus4 => "claude-opus-4-latest", | | |
| 190 | Self::ClaudeOpus4_1 => "claude-opus-4-1-latest", | | |
| 191 | Self::ClaudeOpus4_5 => "claude-opus-4-5-latest", | | |
| 192 | Self::ClaudeOpus4_6 => "claude-opus-4-6-latest", | | |
| 193 | Self::ClaudeOpus4_7 => "claude-opus-4-7-latest", | | |
| 194 | Self::ClaudeSonnet4 => "claude-sonnet-4-latest", | | |
| 195 | Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest", | | |
| 196 | Self::ClaudeSonnet4_6 => "claude-sonnet-4-6-latest", | | |
| 197 | Self::ClaudeHaiku4_5 => "claude-haiku-4-5-latest", | | |
| 198 | Self::Claude3Haiku => "claude-3-haiku-20240307", | | |
| 199 | Self::Custom { name, .. } => name, | | |
| 200 | } | | |
| 201 | } | | |
| 202 | | | |
| 203 | /// The id of the model that should be used for making API requests | | |
| 204 | pub fn request_id(&self) -> &str { | | |
| 205 | match self { | | |
| 206 | Self::ClaudeOpus4 => "claude-opus-4-20250514", | | |
| 207 | Self::ClaudeOpus4_1 => "claude-opus-4-1-20250805", | | |
| 208 | Self::ClaudeOpus4_5 => "claude-opus-4-5-20251101", | | |
| 209 | Self::ClaudeOpus4_6 => "claude-opus-4-6", | | |
| 210 | Self::ClaudeOpus4_7 => "claude-opus-4-7", | | |
| 211 | Self::ClaudeSonnet4 => "claude-sonnet-4-20250514", | | |
| 212 | Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-20250929", | | |
| 213 | Self::ClaudeSonnet4_6 => "claude-sonnet-4-6", | | |
| 214 | Self::ClaudeHaiku4_5 => "claude-haiku-4-5-20251001", | | |
| 215 | Self::Claude3Haiku => "claude-3-haiku-20240307", | | |
| 216 | Self::Custom { name, .. } => name, | | |
| 217 | } | | |
| 218 | } | | |
| 219 | | 46 | |
| 220 | pub fn display_name(&self) -> &str { | 47 | #[derive(Clone, Debug, Default, Deserialize)] |
| 221 | match self { | 48 | pub struct ThinkingCapability { |
| 222 | Self::ClaudeOpus4 => "Claude Opus 4", | 49 | #[serde(default)] |
| 223 | Self::ClaudeOpus4_1 => "Claude Opus 4.1", | 50 | pub supported: bool, |
| 224 | Self::ClaudeOpus4_5 => "Claude Opus 4.5", | 51 | #[serde(default)] |
| 225 | Self::ClaudeOpus4_6 => "Claude Opus 4.6", | 52 | pub types: Option<ThinkingTypes>, |
| 226 | Self::ClaudeOpus4_7 => "Claude Opus 4.7", | 53 | } |
| 227 | Self::ClaudeSonnet4 => "Claude Sonnet 4", | | |
| 228 | Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5", | | |
| 229 | Self::ClaudeSonnet4_6 => "Claude Sonnet 4.6", | | |
| 230 | Self::ClaudeHaiku4_5 => "Claude Haiku 4.5", | | |
| 231 | Self::Claude3Haiku => "Claude 3 Haiku", | | |
| 232 | Self::Custom { | | |
| 233 | name, display_name, .. | | |
| 234 | } => display_name.as_ref().unwrap_or(name), | | |
| 235 | } | | |
| 236 | } | | |
| 237 | | 54 | |
| 238 | pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> { | 55 | #[derive(Clone, Debug, Default, Deserialize)] |
| 239 | match self { | 56 | pub struct ThinkingTypes { |
| 240 | Self::ClaudeOpus4 | 57 | #[serde(default)] |
| 241 | | Self::ClaudeOpus4_1 | 58 | pub adaptive: SupportedCapability, |
| 242 | | Self::ClaudeOpus4_5 | 59 | #[serde(default)] |
| 243 | | Self::ClaudeOpus4_6 | 60 | pub enabled: SupportedCapability, |
| 244 | | Self::ClaudeOpus4_7 | 61 | } |
| 245 | | Self::ClaudeSonnet4 | | |
| 246 | | Self::ClaudeSonnet4_5 | | |
| 247 | | Self::ClaudeSonnet4_6 | | |
| 248 | | Self::ClaudeHaiku4_5 | | |
| 249 | | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration { | | |
| 250 | min_total_token: 2_048, | | |
| 251 | should_speculate: true, | | |
| 252 | max_cache_anchors: 4, | | |
| 253 | }), | | |
| 254 | Self::Custom { | | |
| 255 | cache_configuration, | | |
| 256 | .. | | |
| 257 | } => cache_configuration.clone(), | | |
| 258 | } | | |
| 259 | } | | |
| 260 | | 62 | |
| 261 | pub fn max_token_count(&self) -> u64 { | 63 | #[derive(Clone, Debug, Default, Deserialize)] |
| 262 | match self { | 64 | pub struct EffortCapability { |
| 263 | Self::ClaudeOpus4 | 65 | #[serde(default)] |
| 264 | | Self::ClaudeOpus4_1 | 66 | pub supported: bool, |
| 265 | | Self::ClaudeOpus4_5 | 67 | #[serde(default)] |
| 266 | | Self::ClaudeSonnet4 | 68 | pub low: Option<SupportedCapability>, |
| 267 | | Self::ClaudeSonnet4_5 | 69 | #[serde(default)] |
| 268 | | Self::ClaudeHaiku4_5 | 70 | pub medium: Option<SupportedCapability>, |
| 269 | | Self::Claude3Haiku => 200_000, | 71 | #[serde(default)] |
| 270 | Self::ClaudeOpus4_6 | Self::ClaudeOpus4_7 | Self::ClaudeSonnet4_6 => 1_000_000, | 72 | pub high: Option<SupportedCapability>, |
| 271 | Self::Custom { max_tokens, .. } => *max_tokens, | 73 | #[serde(default)] |
| 272 | } | 74 | pub max: Option<SupportedCapability>, |
| 273 | } | 75 | #[serde(default)] |
| | 76 | pub xhigh: Option<SupportedCapability>, |
| | 77 | } |
| 274 | | 78 | |
| 275 | pub fn max_output_tokens(&self) -> u64 { | 79 | #[derive(Clone, Debug, PartialEq)] |
| 276 | match self { | 80 | pub struct Model { |
| 277 | Self::ClaudeOpus4 | Self::ClaudeOpus4_1 => 32_000, | 81 | pub id: String, |
| 278 | Self::ClaudeOpus4_5 | 82 | pub display_name: String, |
| 279 | | Self::ClaudeSonnet4 | 83 | pub max_input_tokens: u64, |
| 280 | | Self::ClaudeSonnet4_5 | 84 | pub max_output_tokens: u64, |
| 281 | | Self::ClaudeSonnet4_6 | 85 | pub default_temperature: f32, |
| 282 | | Self::ClaudeHaiku4_5 => 64_000, | 86 | pub mode: AnthropicModelMode, |
| 283 | Self::ClaudeOpus4_6 | Self::ClaudeOpus4_7 => 128_000, | 87 | pub supports_thinking: bool, |
| 284 | Self::Claude3Haiku => 4_096, | 88 | pub supports_adaptive_thinking: bool, |
| 285 | Self::Custom { | 89 | pub supports_images: bool, |
| 286 | max_output_tokens, .. | 90 | pub supports_speed: bool, |
| 287 | } => max_output_tokens.unwrap_or(4_096), | 91 | pub supported_effort_levels: Vec<Effort>, |
| 288 | } | 92 | /// A model id to substitute when invoking tools, used for models that |
| 289 | } | 93 | /// don't support tool calling natively. |
| | 94 | pub tool_override: Option<String>, |
| | 95 | /// Extra `Anthropic-Beta` header values to send with each request. |
| | 96 | pub extra_beta_headers: Vec<String>, |
| | 97 | } |
| 290 | | 98 | |
| 291 | pub fn default_temperature(&self) -> f32 { | 99 | impl Model { |
| 292 | match self { | 100 | /// Construct a `Model` from an entry returned by the `/v1/models` listing endpoint. |
| 293 | Self::ClaudeOpus4 | 101 | pub fn from_listed(entry: ListModelEntry) -> Self { |
| 294 | | Self::ClaudeOpus4_1 | 102 | let supports_thinking = entry |
| 295 | | Self::ClaudeOpus4_5 | 103 | .capabilities |
| 296 | | Self::ClaudeOpus4_6 | 104 | .as_ref() |
| 297 | | Self::ClaudeOpus4_7 | 105 | .and_then(|t| t.thinking.as_ref()) |
| 298 | | Self::ClaudeSonnet4 | 106 | .map(|t| t.supported) |
| 299 | | Self::ClaudeSonnet4_5 | 107 | .unwrap_or(false); |
| 300 | | Self::ClaudeSonnet4_6 | 108 | let supports_adaptive_thinking = entry |
| 301 | | Self::ClaudeHaiku4_5 | 109 | .capabilities |
| 302 | | Self::Claude3Haiku => 1.0, | 110 | .as_ref() |
| 303 | Self::Custom { | 111 | .and_then(|t| t.thinking.as_ref()) |
| 304 | default_temperature, | 112 | .and_then(|t| t.types.as_ref()) |
| 305 | .. | 113 | .map(|types| types.adaptive.supported) |
| 306 | } => default_temperature.unwrap_or(1.0), | 114 | .unwrap_or(false); |
| | 115 | let supports_images = entry |
| | 116 | .capabilities |
| | 117 | .as_ref() |
| | 118 | .and_then(|c| c.image_input.as_ref()) |
| | 119 | .map(|c| c.supported) |
| | 120 | .unwrap_or(false); |
| | 121 | |
| | 122 | let mut supported_effort_levels = Vec::new(); |
| | 123 | if let Some(effort) = entry.capabilities.as_ref().and_then(|e| e.effort.as_ref()) { |
| | 124 | // The `xhigh` effort level reported by the API has no |
| | 125 | // corresponding `Effort` variant in the request enum, so it is |
| | 126 | // intentionally dropped here. |
| | 127 | for (level, supported) in [ |
| | 128 | (Effort::Low, effort.low.as_ref()), |
| | 129 | (Effort::Medium, effort.medium.as_ref()), |
| | 130 | (Effort::High, effort.high.as_ref()), |
| | 131 | (Effort::XHigh, effort.xhigh.as_ref()), |
| | 132 | (Effort::Max, effort.max.as_ref()), |
| | 133 | ] { |
| | 134 | if supported.map(|c| c.supported).unwrap_or(false) { |
| | 135 | supported_effort_levels.push(level); |
| | 136 | } |
| | 137 | } |
| 307 | } | 138 | } |
| 308 | } | | |
| 309 | | 139 | |
| 310 | pub fn mode(&self) -> AnthropicModelMode { | 140 | let mode = if supports_adaptive_thinking { |
| 311 | match self { | 141 | AnthropicModelMode::AdaptiveThinking |
| 312 | Self::Custom { mode, .. } => mode.clone(), | 142 | } else if supports_thinking { |
| 313 | _ if self.supports_adaptive_thinking() => AnthropicModelMode::AdaptiveThinking, | 143 | AnthropicModelMode::Thinking { |
| 314 | _ if self.supports_thinking() => AnthropicModelMode::Thinking { | | |
| 315 | budget_tokens: Some(4_096), | 144 | budget_tokens: Some(4_096), |
| 316 | }, | | |
| 317 | _ => AnthropicModelMode::Default, | | |
| 318 | } | | |
| 319 | } | | |
| 320 | | | |
| 321 | pub fn supports_thinking(&self) -> bool { | | |
| 322 | match self { | | |
| 323 | Self::Custom { mode, .. } => { | | |
| 324 | matches!( | | |
| 325 | mode, | | |
| 326 | AnthropicModelMode::Thinking { .. } | AnthropicModelMode::AdaptiveThinking | | |
| 327 | ) | | |
| 328 | } | 145 | } |
| 329 | _ => matches!( | 146 | } else { |
| 330 | self, | 147 | AnthropicModelMode::Default |
| 331 | Self::ClaudeOpus4 | 148 | }; |
| 332 | | Self::ClaudeOpus4_1 | | |
| 333 | | Self::ClaudeOpus4_5 | | |
| 334 | | Self::ClaudeOpus4_6 | | |
| 335 | | Self::ClaudeOpus4_7 | | |
| 336 | | Self::ClaudeSonnet4 | | |
| 337 | | Self::ClaudeSonnet4_5 | | |
| 338 | | Self::ClaudeSonnet4_6 | | |
| 339 | | Self::ClaudeHaiku4_5 | | |
| 340 | ), | | |
| 341 | } | | |
| 342 | } | | |
| 343 | | 149 | |
| 344 | pub fn supports_speed(&self) -> bool { | 150 | let supports_speed = entry.id == "claude-opus-4-6"; |
| 345 | matches!(self, Self::ClaudeOpus4_6 | Self::ClaudeSonnet4_6) | | |
| 346 | } | | |
| 347 | | 151 | |
| 348 | pub fn supports_adaptive_thinking(&self) -> bool { | 152 | Self { |
| 349 | match self { | 153 | display_name: entry.display_name, |
| 350 | Self::Custom { mode, .. } => matches!(mode, AnthropicModelMode::AdaptiveThinking), | 154 | id: entry.id, |
| 351 | _ => matches!( | 155 | max_input_tokens: entry.max_input_tokens, |
| 352 | self, | 156 | max_output_tokens: entry.max_tokens, |
| 353 | Self::ClaudeOpus4_6 | Self::ClaudeOpus4_7 | Self::ClaudeSonnet4_6 | 157 | default_temperature: 1.0, |
| 354 | ), | 158 | mode, |
| | 159 | supports_thinking, |
| | 160 | supports_adaptive_thinking, |
| | 161 | supports_images, |
| | 162 | supports_speed, |
| | 163 | supported_effort_levels, |
| | 164 | tool_override: None, |
| | 165 | extra_beta_headers: Vec::new(), |
| 355 | } | 166 | } |
| 356 | } | 167 | } |
| 357 | | 168 | |
| 358 | pub fn beta_headers(&self) -> Option<String> { | 169 | pub fn beta_headers(&self) -> Option<String> { |
| 359 | let mut headers = vec![]; | 170 | let headers: Vec<&str> = self |
| 360 | | 171 | .extra_beta_headers |
| 361 | match self { | 172 | .iter() |
| 362 | Self::Custom { | 173 | .map(|h| h.trim()) |
| 363 | extra_beta_headers, .. | 174 | .filter(|h| !h.is_empty()) |
| 364 | } => { | 175 | .collect(); |
| 365 | headers.extend( | | |
| 366 | extra_beta_headers | | |
| 367 | .iter() | | |
| 368 | .filter(|header| !header.trim().is_empty()) | | |
| 369 | .cloned(), | | |
| 370 | ); | | |
| 371 | } | | |
| 372 | _ => {} | | |
| 373 | } | | |
| 374 | | | |
| 375 | if headers.is_empty() { | 176 | if headers.is_empty() { |
| 376 | None | 177 | None |
| 377 | } else { | 178 | } else { |
| @@ -379,15 +180,11 @@ impl Model { · 1 unmodified line |
| 379 | } | 180 | } |
| 380 | } | 181 | } |
| 381 | | 182 | |
| 382 | pub fn tool_model_id(&self) -> &str { | 183 | pub fn request_id(&self, has_tools: bool) -> &str { |
| 383 | if let Self::Custom { | 184 | if has_tools { |
| 384 | tool_override: Some(tool_override), | 185 | self.tool_override.as_deref().unwrap_or(&self.id) |
| 385 | .. | | |
| 386 | } = self | | |
| 387 | { | | |
| 388 | tool_override | | |
| 389 | } else { | 186 | } else { |
| 390 | self.request_id() | 187 | &self.id |
| 391 | } | 188 | } |
| 392 | } | 189 | } |
| 393 | } | 190 | } |
| @@ -405,6 +202,73 @@ pub async fn stream_completion( · 11 unmodified lines |
| 405 | .map(|output| output.0) | 202 | .map(|output| output.0) |
| 406 | } | 203 | } |
| 407 | | 204 | |
| | 205 | /// A raw model entry returned by the Anthropic models listing endpoint. |
| | 206 | #[derive(Clone, Debug, Deserialize)] |
| | 207 | pub struct ListModelEntry { |
| | 208 | pub id: String, |
| | 209 | pub display_name: String, |
| | 210 | pub max_input_tokens: u64, |
| | 211 | pub max_tokens: u64, |
| | 212 | #[serde(default)] |
| | 213 | pub capabilities: Option<ModelCapabilities>, |
| | 214 | } |
| | 215 | |
| | 216 | #[derive(Debug, Deserialize)] |
| | 217 | struct ListModelsResponse { |
| | 218 | data: Vec<ListModelEntry>, |
| | 219 | } |
| | 220 | |
| | 221 | /// Fetch the list of models available to the current API key. The returned |
| | 222 | /// models are constructed by feeding each raw entry through |
| | 223 | /// [`Model::from_listed`]. |
| | 224 | /// |
| | 225 | /// See https://docs.claude.com/en/api/models-list. |
| | 226 | pub async fn list_models( |
| | 227 | client: &dyn HttpClient, |
| | 228 | api_url: &str, |
| | 229 | api_key: &str, |
| | 230 | ) -> Result<Vec<Model>> { |
| | 231 | let uri = format!("{api_url}/v1/models?limit=1000"); |
| | 232 | |
| | 233 | let request = HttpRequest::builder() |
| | 234 | .method(Method::GET) |
| | 235 | .uri(uri) |
| | 236 | .header("Anthropic-Version", "2023-06-01") |
| | 237 | .header("X-Api-Key", api_key.trim()) |
| | 238 | .header("Accept", "application/json") |
| | 239 | .body(AsyncBody::default()) |
| | 240 | .context("failed to build Anthropic models list request")?; |
| | 241 | |
| | 242 | let mut response = client |
| | 243 | .send(request) |
| | 244 | .await |
| | 245 | .context("failed to send Anthropic models list request")?; |
| | 246 | |
| | 247 | let mut body = String::new(); |
| | 248 | response |
| | 249 | .body_mut() |
| | 250 | .read_to_string(&mut body) |
| | 251 | .await |
| | 252 | .context("failed to read Anthropic models list response")?; |
| | 253 | |
| | 254 | anyhow::ensure!( |
| | 255 | response.status().is_success(), |
| | 256 | "failed to list Anthropic models: {} {}", |
| | 257 | response.status(), |
| | 258 | body, |
| | 259 | ); |
| | 260 | |
| | 261 | let parsed: ListModelsResponse = |
| | 262 | serde_json::from_str(&body).context("failed to parse Anthropic models list response")?; |
| | 263 | |
| | 264 | let models = parsed |
| | 265 | .data |
| | 266 | .into_iter() |
| | 267 | .map(Model::from_listed) |
| | 268 | .collect::<Vec<_>>(); |
| | 269 | Ok(models) |
| | 270 | } |
| | 271 | |
| 408 | /// Generate completion without streaming. | 272 | /// Generate completion without streaming. |
| 409 | pub async fn non_streaming_completion( | 273 | pub async fn non_streaming_completion( |
| 410 | client: &dyn HttpClient, | 274 | client: &dyn HttpClient, |
| @@ -780,13 +644,14 @@ pub enum AdaptiveThinkingDisplay { · 369 unmodified lines |
| 780 | Summarized, | 644 | Summarized, |
| 781 | } | 645 | } |
| 782 | | 646 | |
| 783 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, EnumString)] | 647 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString)] |
| 784 | #[serde(rename_all = "snake_case")] | 648 | #[serde(rename_all = "snake_case")] |
| 785 | #[strum(serialize_all = "snake_case")] | 649 | #[strum(serialize_all = "snake_case")] |
| 786 | pub enum Effort { | 650 | pub enum Effort { |
| 787 | Low, | 651 | Low, |
| 788 | Medium, | 652 | Medium, |
| 789 | High, | 653 | High, |
| | 654 | XHigh, |
| 790 | Max, | 655 | Max, |
| 791 | } | 656 | } |
| 792 | | 657 | |
| @@ -1095,63 +960,93 @@ impl From<ApiError> for language_model_core::LanguageModelCompletionError { · 302 unmodified lines |
| 1095 | } | 960 | } |
| 1096 | } | 961 | } |
| 1097 | | 962 | |
| 1098 | #[test] | 963 | #[cfg(test)] |
| 1099 | fn custom_mode_thinking_is_preserved() { | 964 | mod tests { |
| 1100 | let model = Model::Custom { | 965 | use super::*; |
| 1101 | name: "my-custom-model".to_string(), | 966 | |
| 1102 | max_tokens: 8192, | 967 | fn listed_entry(id: &str, capabilities: ModelCapabilities) -> ListModelEntry { |
| 1103 | display_name: None, | 968 | ListModelEntry { |
| 1104 | tool_override: None, | 969 | id: id.to_string(), |
| 1105 | cache_configuration: None, | 970 | display_name: id.to_string(), |
| 1106 | max_output_tokens: None, | 971 | max_input_tokens: 200_000, |
| 1107 | default_temperature: None, | 972 | max_tokens: 64_000, |
| 1108 | extra_beta_headers: vec![], | 973 | capabilities: Some(capabilities), |
| 1109 | mode: AnthropicModelMode::Thinking { | | |
| 1110 | budget_tokens: Some(2048), | | |
| 1111 | }, | | |
| 1112 | }; | | |
| 1113 | assert_eq!( | | |
| 1114 | model.mode(), | | |
| 1115 | AnthropicModelMode::Thinking { | | |
| 1116 | budget_tokens: Some(2048) | | |
| 1117 | } | 974 | } |
| 1118 | ); | 975 | } |
| 1119 | assert!(model.supports_thinking()); | | |
| 1120 | } | | |
| 1121 | | 976 | |
| 1122 | #[test] | 977 | #[test] |
| 1123 | fn custom_mode_adaptive_is_preserved() { | 978 | fn from_listed_picks_adaptive_thinking_mode() { |
| 1124 | let model = Model::Custom { | 979 | let entry = listed_entry( |
| 1125 | name: "my-custom-model".to_string(), | 980 | "claude-test-adaptive", |
| 1126 | max_tokens: 8192, | 981 | ModelCapabilities { |
| 1127 | display_name: None, | 982 | thinking: Some(ThinkingCapability { |
| 1128 | tool_override: None, | 983 | supported: true, |
| 1129 | cache_configuration: None, | 984 | types: Some(ThinkingTypes { |
| 1130 | max_output_tokens: None, | 985 | adaptive: SupportedCapability { supported: true }, |
| 1131 | default_temperature: None, | 986 | enabled: SupportedCapability { supported: true }, |
| 1132 | extra_beta_headers: vec![], | 987 | }), |
| 1133 | mode: AnthropicModelMode::AdaptiveThinking, | 988 | }), |
| 1134 | }; | 989 | ..Default::default() |
| 1135 | assert_eq!(model.mode(), AnthropicModelMode::AdaptiveThinking); | 990 | }, |
| 1136 | assert!(model.supports_adaptive_thinking()); | 991 | ); |
| 1137 | assert!(model.supports_thinking()); | 992 | let model = Model::from_listed(entry); |
| 1138 | } | 993 | assert!(model.supports_thinking); |
| | 994 | assert!(model.supports_adaptive_thinking); |
| | 995 | assert_eq!(model.mode, AnthropicModelMode::AdaptiveThinking); |
| | 996 | } |
| 1139 | | 997 | |
| 1140 | #[test] | 998 | #[test] |
| 1141 | fn custom_mode_default_disables_thinking() { | 999 | fn from_listed_picks_thinking_mode_when_only_enabled_supported() { |
| 1142 | let model = Model::Custom { | 1000 | let entry = listed_entry( |
| 1143 | name: "my-custom-model".to_string(), | 1001 | "claude-test-thinking", |
| 1144 | max_tokens: 8192, | 1002 | ModelCapabilities { |
| 1145 | display_name: None, | 1003 | thinking: Some(ThinkingCapability { |
| 1146 | tool_override: None, | 1004 | supported: true, |
| 1147 | cache_configuration: None, | 1005 | types: Some(ThinkingTypes { |
| 1148 | max_output_tokens: None, | 1006 | adaptive: SupportedCapability { supported: false }, |
| 1149 | default_temperature: None, | 1007 | enabled: SupportedCapability { supported: true }, |
| 1150 | extra_beta_headers: vec![], | 1008 | }), |
| 1151 | mode: AnthropicModelMode::Default, | 1009 | }), |
| 1152 | }; | 1010 | ..Default::default() |
| 1153 | assert!(!model.supports_thinking()); | 1011 | }, |
| 1154 | assert!(!model.supports_adaptive_thinking()); | 1012 | ); |
| | 1013 | let model = Model::from_listed(entry); |
| | 1014 | assert!(model.supports_thinking); |
| | 1015 | assert!(!model.supports_adaptive_thinking); |
| | 1016 | assert!(matches!(model.mode, AnthropicModelMode::Thinking { .. })); |
| | 1017 | } |
| | 1018 | |
| | 1019 | #[test] |
| | 1020 | fn from_listed_default_mode_when_no_thinking() { |
| | 1021 | let entry = listed_entry("claude-test-default", ModelCapabilities::default()); |
| | 1022 | let model = Model::from_listed(entry); |
| | 1023 | assert!(!model.supports_thinking); |
| | 1024 | assert!(!model.supports_adaptive_thinking); |
| | 1025 | assert_eq!(model.mode, AnthropicModelMode::Default); |
| | 1026 | } |
| | 1027 | |
| | 1028 | #[test] |
| | 1029 | fn from_listed_collects_supported_effort_levels() { |
| | 1030 | let entry = listed_entry( |
| | 1031 | "claude-test-effort", |
| | 1032 | ModelCapabilities { |
| | 1033 | effort: Some(EffortCapability { |
| | 1034 | supported: true, |
| | 1035 | low: Some(SupportedCapability { supported: true }), |
| | 1036 | medium: Some(SupportedCapability { supported: false }), |
| | 1037 | high: Some(SupportedCapability { supported: true }), |
| | 1038 | max: Some(SupportedCapability { supported: true }), |
| | 1039 | xhigh: Some(SupportedCapability { supported: true }), |
| | 1040 | }), |
| | 1041 | ..Default::default() |
| | 1042 | }, |
| | 1043 | ); |
| | 1044 | let model = Model::from_listed(entry); |
| | 1045 | assert_eq!( |
| | 1046 | &model.supported_effort_levels, |
| | 1047 | &[Effort::Low, Effort::High, Effort::XHigh, Effort::Max] |
| | 1048 | ); |
| | 1049 | } |
| 1155 | } | 1050 | } |
| 1156 | | 1051 | |
| 1157 | #[test] | 1052 | #[test] |