use crate::types::{Api, InputModality, ModelCost, ProviderId, ThinkingLevelMap};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct AnthropicMessagesCompat {
pub supports_eager_tool_input_streaming: Option<bool>,
pub supports_long_cache_retention: Option<bool>,
pub send_session_affinity_headers: Option<bool>,
pub supports_cache_control_on_tools: Option<bool>,
pub supports_temperature: Option<bool>,
pub force_adaptive_thinking: Option<bool>,
pub allow_empty_signature: Option<bool>,
pub supports_strict_tools: Option<bool>,
pub supports_tool_references: Option<bool>,
}
impl AnthropicMessagesCompat {
pub fn eager_tool_input_streaming(&self) -> bool {
self.supports_eager_tool_input_streaming.unwrap_or(true)
}
pub fn long_cache_retention(&self) -> bool {
self.supports_long_cache_retention.unwrap_or(true)
}
pub fn cache_control_on_tools(&self) -> bool {
self.supports_cache_control_on_tools.unwrap_or(true)
}
pub fn temperature(&self) -> bool {
self.supports_temperature.unwrap_or(true)
}
pub fn adaptive_thinking(&self) -> bool {
self.force_adaptive_thinking.unwrap_or(false)
}
pub fn empty_signature(&self) -> bool {
self.allow_empty_signature.unwrap_or(false)
}
pub fn strict_tools(&self) -> bool {
self.supports_strict_tools.unwrap_or(false)
}
pub fn tool_references(&self) -> bool {
self.supports_tool_references.unwrap_or(true)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct OpenaiCompletionsCompat {
pub supports_store: Option<bool>,
pub supports_developer_role: Option<bool>,
pub supports_reasoning_effort: Option<bool>,
pub supports_usage_in_streaming: Option<bool>,
pub supports_finish_reason: Option<bool>,
pub max_tokens_field: Option<MaxTokensField>,
pub requires_tool_result_name: Option<bool>,
pub requires_assistant_after_tool_result: Option<bool>,
pub requires_thinking_as_text: Option<bool>,
pub supports_strict_mode: Option<bool>,
pub send_session_affinity_headers: Option<bool>,
pub supports_long_cache_retention: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MaxTokensField {
MaxCompletionTokens,
MaxTokens,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct OpenaiResponsesCompat {
pub supports_developer_role: Option<bool>,
pub supports_long_cache_retention: Option<bool>,
pub supports_strict_mode: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct BedrockCompat {
pub supports_strict_mode: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StreamingProtocolCompat {
AnthropicMessages(AnthropicMessagesCompat),
OpenaiCompletions(OpenaiCompletionsCompat),
OpenaiResponses(OpenaiResponsesCompat),
Bedrock(BedrockCompat),
Other(serde_json::Value),
None,
}
impl Default for StreamingProtocolCompat {
fn default() -> Self {
StreamingProtocolCompat::None
}
}
impl StreamingProtocolCompat {
pub fn as_anthropic(&self) -> Option<&AnthropicMessagesCompat> {
match self {
StreamingProtocolCompat::AnthropicMessages(c) => Some(c),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Model {
pub id: String,
pub name: String,
pub api: Api,
pub provider: ProviderId,
pub base_url: String,
pub reasoning: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_level_map: Option<ThinkingLevelMap>,
pub input: Vec<InputModality>,
pub cost: ModelCost,
pub context_window: u64,
pub max_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sampling_params: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub headers: Option<std::collections::BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compat: Option<StreamingProtocolCompat>,
}
impl Model {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
api: Api,
provider: impl Into<String>,
base_url: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
api,
provider: provider.into(),
base_url: base_url.into(),
reasoning: false,
thinking_level_map: None,
input: vec![InputModality::Text],
cost: ModelCost::default(),
context_window: 0,
max_tokens: 0,
sampling_params: None,
headers: None,
compat: None,
}
}
pub fn supported_thinking_levels(&self) -> Vec<crate::types::ThinkingLevel> {
use crate::types::ThinkingLevel::*;
let map = self.thinking_level_map.as_ref();
let supported = |lvl: crate::types::ThinkingLevel| -> bool {
match map.and_then(|m| m.get(&lvl)) {
Some(None) => false, Some(Some(_)) => true, None => true, }
};
let mut levels = vec![Off, Minimal, Low, Medium, High];
if matches!(map.and_then(|m| m.get(&Xhigh)), Some(Some(_))) {
levels.push(Xhigh);
}
if matches!(map.and_then(|m| m.get(&Max)), Some(Some(_))) {
levels.push(Max);
}
levels.retain(|&lvl| supported(lvl));
levels
}
pub fn clamp_thinking_level(
&self,
requested: crate::types::ThinkingLevel,
) -> crate::types::ThinkingLevel {
use crate::types::ThinkingLevel::*;
let order = [Off, Minimal, Low, Medium, High, Xhigh, Max];
let supported: std::collections::HashSet<_> =
self.supported_thinking_levels().into_iter().collect();
let req_idx = order
.iter()
.position(|l| *l == requested)
.unwrap_or(order.len() - 1);
for i in (0..=req_idx).rev() {
if supported.contains(&order[i]) {
return order[i];
}
}
Off
}
}