1use crate::types::{Api, InputModality, ModelCost, ProviderId, ThinkingLevelMap};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
14#[serde(default, rename_all = "camelCase")]
15pub struct AnthropicMessagesCompat {
16 pub supports_eager_tool_input_streaming: Option<bool>,
18 pub supports_long_cache_retention: Option<bool>,
20 pub send_session_affinity_headers: Option<bool>,
22 pub supports_cache_control_on_tools: Option<bool>,
24 pub supports_temperature: Option<bool>,
26 pub force_adaptive_thinking: Option<bool>,
28 pub allow_empty_signature: Option<bool>,
30 pub supports_strict_tools: Option<bool>,
32 pub supports_tool_references: Option<bool>,
34}
35
36impl AnthropicMessagesCompat {
37 pub fn eager_tool_input_streaming(&self) -> bool {
39 self.supports_eager_tool_input_streaming.unwrap_or(true)
40 }
41 pub fn long_cache_retention(&self) -> bool {
42 self.supports_long_cache_retention.unwrap_or(true)
43 }
44 pub fn cache_control_on_tools(&self) -> bool {
45 self.supports_cache_control_on_tools.unwrap_or(true)
46 }
47 pub fn temperature(&self) -> bool {
48 self.supports_temperature.unwrap_or(true)
49 }
50 pub fn adaptive_thinking(&self) -> bool {
51 self.force_adaptive_thinking.unwrap_or(false)
52 }
53 pub fn empty_signature(&self) -> bool {
54 self.allow_empty_signature.unwrap_or(false)
55 }
56 pub fn strict_tools(&self) -> bool {
57 self.supports_strict_tools.unwrap_or(false)
58 }
59 pub fn tool_references(&self) -> bool {
60 self.supports_tool_references.unwrap_or(true)
64 }
65}
66
67pub fn prior_reasoning_is_replayed(model: &Model) -> bool {
81 match model.api {
82 Api::OpenaiCompletions => matches!(
83 model.compat.as_ref(),
84 Some(StreamingProtocolCompat::OpenaiCompletions(compat))
85 if compat.requires_thinking_as_text == Some(true)
86 || compat.requires_reasoning_content == Some(true)
87 ),
88 Api::OpenaiResponses | Api::AzureOpenaiResponses | Api::OpenaiCodexResponses => true,
89 Api::AnthropicMessages => true,
90 _ => false,
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
98#[serde(default, rename_all = "camelCase")]
99pub struct OpenaiCompletionsCompat {
100 pub supports_store: Option<bool>,
101 pub supports_developer_role: Option<bool>,
102 pub supports_reasoning_effort: Option<bool>,
103 pub supports_usage_in_streaming: Option<bool>,
104 pub supports_finish_reason: Option<bool>,
105 pub max_tokens_field: Option<MaxTokensField>,
106 pub requires_tool_result_name: Option<bool>,
107 pub requires_assistant_after_tool_result: Option<bool>,
108 pub requires_thinking_as_text: Option<bool>,
109 pub requires_reasoning_content: Option<bool>,
112 pub supports_strict_mode: Option<bool>,
113 pub send_session_affinity_headers: Option<bool>,
114 pub supports_long_cache_retention: Option<bool>,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum MaxTokensField {
120 MaxCompletionTokens,
121 MaxTokens,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
126#[serde(default, rename_all = "camelCase")]
127pub struct OpenaiResponsesCompat {
128 pub supports_developer_role: Option<bool>,
129 pub supports_long_cache_retention: Option<bool>,
130 pub supports_strict_mode: Option<bool>,
131 #[serde(rename = "supportsOpenAIGrammarTools")]
135 pub supports_openai_grammar_tools: Option<bool>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
140#[serde(default, rename_all = "camelCase")]
141pub struct BedrockCompat {
142 pub supports_strict_mode: Option<bool>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub enum StreamingProtocolCompat {
149 AnthropicMessages(AnthropicMessagesCompat),
150 OpenaiCompletions(OpenaiCompletionsCompat),
151 OpenaiResponses(OpenaiResponsesCompat),
152 Bedrock(BedrockCompat),
153 Other(serde_json::Value),
155 None,
157}
158
159impl Default for StreamingProtocolCompat {
160 fn default() -> Self {
161 StreamingProtocolCompat::None
162 }
163}
164
165impl StreamingProtocolCompat {
166 pub fn as_anthropic(&self) -> Option<&AnthropicMessagesCompat> {
167 match self {
168 StreamingProtocolCompat::AnthropicMessages(c) => Some(c),
169 _ => None,
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct Model {
179 pub id: String,
180 pub name: String,
181 pub api: Api,
182 pub provider: ProviderId,
183 pub base_url: String,
184 pub reasoning: bool,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub thinking_level_map: Option<ThinkingLevelMap>,
187 pub input: Vec<InputModality>,
188 pub cost: ModelCost,
189 #[serde(default)]
191 pub cost_configured: bool,
192 pub context_window: u64,
193 pub max_tokens: u64,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub sampling_params: Option<serde_json::Map<String, serde_json::Value>>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub headers: Option<std::collections::BTreeMap<String, String>>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub compat: Option<StreamingProtocolCompat>,
200}
201
202impl Model {
203 pub fn new(
205 id: impl Into<String>,
206 name: impl Into<String>,
207 api: Api,
208 provider: impl Into<String>,
209 base_url: impl Into<String>,
210 ) -> Self {
211 Self {
212 id: id.into(),
213 name: name.into(),
214 api,
215 provider: provider.into(),
216 base_url: base_url.into(),
217 reasoning: false,
218 thinking_level_map: None,
219 input: vec![InputModality::Text],
220 cost: ModelCost::default(),
221 cost_configured: false,
222 context_window: 0,
223 max_tokens: 0,
224 sampling_params: None,
225 headers: None,
226 compat: None,
227 }
228 }
229
230 pub fn has_pricing(&self) -> bool {
232 self.cost_configured || self.cost != ModelCost::default()
233 }
234
235 pub fn supported_thinking_levels(&self) -> Vec<crate::types::ThinkingLevel> {
239 use crate::types::ThinkingLevel::*;
240 let map = self.thinking_level_map.as_ref();
241 let supported = |lvl: crate::types::ThinkingLevel| -> bool {
242 match map.and_then(|m| m.get(&lvl)) {
243 Some(None) => false, Some(Some(_)) => true, None => true, }
247 };
248 let mut levels = vec![Off, Minimal, Low, Medium, High];
249 if matches!(map.and_then(|m| m.get(&Xhigh)), Some(Some(_))) {
251 levels.push(Xhigh);
252 }
253 if matches!(map.and_then(|m| m.get(&Max)), Some(Some(_))) {
254 levels.push(Max);
255 }
256 levels.retain(|&lvl| supported(lvl));
257 levels
258 }
259
260 pub fn clamp_thinking_level(
264 &self,
265 requested: crate::types::ThinkingLevel,
266 ) -> crate::types::ThinkingLevel {
267 use crate::types::ThinkingLevel::*;
268 let order = [Off, Minimal, Low, Medium, High, Xhigh, Max];
269 let supported: std::collections::HashSet<_> =
270 self.supported_thinking_levels().into_iter().collect();
271 let req_idx = order
272 .iter()
273 .position(|l| *l == requested)
274 .unwrap_or(order.len() - 1);
275 for i in (0..=req_idx).rev() {
277 if supported.contains(&order[i]) {
278 return order[i];
279 }
280 }
281 Off
282 }
283}