1use serde::Deserialize;
8
9#[derive(Debug, Deserialize)]
11pub struct RawLiteLLMEntry {
12 #[serde(default)]
14 pub litellm_provider: Option<String>,
15 #[serde(default)]
17 pub mode: Option<String>,
18 #[serde(default)]
20 pub max_input_tokens: Option<u32>,
21 #[serde(default)]
23 pub max_output_tokens: Option<u32>,
24 #[serde(default)]
26 pub max_tokens: Option<u32>,
27 #[serde(default)]
29 pub input_cost_per_token: Option<f64>,
30 #[serde(default)]
32 pub output_cost_per_token: Option<f64>,
33 #[serde(default)]
35 pub cache_read_input_token_cost: Option<f64>,
36 #[serde(default)]
38 pub cache_creation_input_token_cost: Option<f64>,
39 #[serde(default)]
41 pub output_cost_per_reasoning_token: Option<f64>,
42 #[serde(default)]
44 pub input_cost_per_audio_token: Option<f64>,
45 #[serde(default)]
47 pub deprecation_date: Option<String>,
48 #[serde(default)]
51 pub supports_function_calling: Option<bool>,
52 #[serde(default)]
54 pub supports_vision: Option<bool>,
55 #[serde(default)]
57 pub supports_audio_input: Option<bool>,
58 #[serde(default)]
60 pub supports_audio_output: Option<bool>,
61 #[serde(default)]
63 pub supports_video_input: Option<bool>,
64 #[serde(default)]
66 pub supports_web_search: Option<bool>,
67 #[serde(default)]
69 pub supports_pdf_input: Option<bool>,
70 #[serde(default)]
72 pub supports_reasoning: Option<bool>,
73 #[serde(default)]
75 pub supports_computer_use: Option<bool>,
76 #[serde(default)]
78 pub supports_prompt_caching: Option<bool>,
79 #[serde(default)]
81 pub supports_assistant_prefill: Option<bool>,
82}
83
84const PROVIDER_PREFIXES: &[(&str, &str)] = &[
86 ("openai/", "openai"),
87 ("anthropic/", "anthropic"),
88 ("gemini/", "gemini"),
89 ("deepseek/", "deepseek"),
90 ("xai/", "xai"),
91 ("mistral/", "mistral"),
92 ("meta-llama/", "meta"),
93 ("bedrock/", "bedrock"),
94 ("azure/", "azure"),
95 ("azure_ai/", "azure"),
96 ("vertex_ai/", "vertex_ai"),
97 ("groq/", "groq"),
98 ("together_ai/", "together_ai"),
99 ("fireworks_ai/", "fireworks_ai"),
100 ("replicate/", "replicate"),
101 ("cohere/", "cohere"),
102 ("cohere_chat/", "cohere"),
103 ("perplexity/", "perplexity"),
104 ("github_copilot/", "github_copilot"),
105 ("ollama/", "ollama"),
106 ("nvidia_nim/", "nvidia_nim"),
107 ("sambanova/", "sambanova"),
108 ("cerebras/", "cerebras"),
109 ("dashscope/", "alibaba"),
110];
111
112#[must_use]
114pub fn strip_provider_prefix(litellm_key: &str) -> &str {
115 for &(prefix, _) in PROVIDER_PREFIXES {
116 if let Some(rest) = litellm_key.strip_prefix(prefix) {
117 return rest;
118 }
119 }
120 litellm_key
121}
122
123const ANTHROPIC_PER_MESSAGE_EFFORT_MODELS: &[&str] = &[
128 "claude-fable-5-1",
129 "claude-mythos-5-1",
130 "claude-opus-5-5",
131 "claude-opus-5",
132 "claude-sonnet-5-5",
133 "claude-haiku-5-5",
134];
135
136const ANTHROPIC_MID_CONVERSATION_SYSTEM_MODELS: &[&str] = &[
139 "claude-opus-4-8",
140 "claude-fable-5",
141 "claude-fable-5-1",
142 "claude-mythos-5",
143 "claude-mythos-5-1",
144 "claude-opus-5",
145 "claude-opus-5-5",
146 "claude-sonnet-5-5",
147 "claude-haiku-5-5",
148];
149
150#[must_use]
153pub fn takes_mid_conversation_system(provider: &str, canonical_id: &str) -> bool {
154 provider == "anthropic" && ANTHROPIC_MID_CONVERSATION_SYSTEM_MODELS.contains(&canonical_id)
155}
156
157const ANTHROPIC_PER_MESSAGE_EFFORT_PLATFORMS: &[&str] = &["anthropic", "vertex_ai"];
160
161const OPENAI_FIRST_PER_MESSAGE_EFFORT_GENERATION: u32 = 6;
164
165#[must_use]
168pub fn takes_per_message_effort(provider: &str, canonical_id: &str) -> bool {
169 if ANTHROPIC_PER_MESSAGE_EFFORT_PLATFORMS.contains(&provider) {
170 let model = canonical_id
172 .split_once('@')
173 .map_or(canonical_id, |(model, _)| model);
174 return ANTHROPIC_PER_MESSAGE_EFFORT_MODELS.contains(&model);
175 }
176 if provider == "openai" {
177 return gpt_generation(canonical_id)
178 .is_some_and(|generation| generation >= OPENAI_FIRST_PER_MESSAGE_EFFORT_GENERATION);
179 }
180 false
181}
182
183fn gpt_generation(model: &str) -> Option<u32> {
185 let rest = model.strip_prefix("gpt-")?;
186 let digits = rest
187 .find(|c: char| !c.is_ascii_digit())
188 .unwrap_or(rest.len());
189 rest.get(..digits)?.parse().ok()
190}
191
192#[must_use]
194pub fn parse_provider(provider_str: &str) -> String {
195 let mapped = match provider_str {
196 "openai" | "text-completion-openai" | "chatgpt" => "openai",
197 "anthropic" => "anthropic",
198 "gemini" | "palm" => "gemini",
199 "deepseek" => "deepseek",
200 "xai" => "xai",
201 "mistral" | "codestral" | "text-completion-codestral" => "mistral",
202 "meta_llama" => "meta",
203 s if s.starts_with("bedrock") => "bedrock",
204 "azure" | "azure_ai" | "azure_text" => "azure",
205 s if s.starts_with("vertex_ai") => "vertex_ai",
206 "groq" => "groq",
207 "together_ai" => "together_ai",
208 s if s.starts_with("fireworks_ai") => "fireworks_ai",
209 "replicate" => "replicate",
210 "cohere" | "cohere_chat" => "cohere",
211 "perplexity" => "perplexity",
212 "github_copilot" => "github_copilot",
213 "dashscope" => "alibaba",
214 other => return other.to_string(),
215 };
216 mapped.to_string()
217}
218
219#[must_use]
221pub fn parse_mode(mode_str: &str) -> Option<&'static str> {
222 match mode_str {
223 "chat" | "responses" => Some("chat"),
224 "embedding" => Some("embedding"),
225 "image_generation" | "image_edit" => Some("image_generation"),
226 "audio_transcription" => Some("audio_transcription"),
227 "audio_speech" => Some("audio_speech"),
228 "rerank" => Some("rerank"),
229 "moderation" => Some("moderation"),
230 "completion" => Some("completion"),
231 "video_generation" => Some("video_generation"),
232 "search" | "vector_store" => Some("search"),
233 "ocr" => Some("ocr"),
234 _ => None,
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct ConvertedEntry {
241 pub litellm_id: String,
243 pub canonical_id: String,
245 pub provider: String,
247 pub mode: String,
249 pub max_input_tokens: Option<u32>,
251 pub max_output_tokens: Option<u32>,
253 pub input_cost_per_token: f64,
255 pub output_cost_per_token: f64,
257 pub cache_read_per_token: Option<f64>,
259 pub cache_write_per_token: Option<f64>,
261 pub reasoning_per_token: Option<f64>,
263 pub image_per_token: Option<f64>,
265 pub abilities: Vec<&'static str>,
267 pub deprecation_date: Option<String>,
269}
270
271#[must_use]
273pub fn convert_litellm_entry(key: &str, raw: &RawLiteLLMEntry) -> Option<ConvertedEntry> {
274 let provider_str = raw.litellm_provider.as_deref()?;
275 let mode_str = raw.mode.as_deref()?;
276 let mode = parse_mode(mode_str)?;
277 let provider = parse_provider(provider_str);
278 let canonical_id = strip_provider_prefix(key).to_string();
279
280 let max_input = raw.max_input_tokens;
281 let max_output = raw.max_output_tokens.or(raw.max_tokens);
282
283 let mut abilities = Vec::new();
284 if raw.supports_function_calling == Some(true) {
285 abilities.push("ToolUse");
286 }
287 if raw.supports_vision == Some(true) {
288 abilities.push("Vision");
289 }
290 if raw.supports_audio_input == Some(true) {
291 abilities.push("Audio");
292 }
293 if raw.supports_audio_output == Some(true) {
294 abilities.push("AudioOutput");
295 }
296 if raw.supports_video_input == Some(true) {
297 abilities.push("Video");
298 }
299 if raw.supports_web_search == Some(true) {
300 abilities.push("WebSearch");
301 }
302 if raw.supports_pdf_input == Some(true) {
303 abilities.push("Pdf");
304 }
305 if raw.supports_reasoning == Some(true) {
306 abilities.push("Reasoning");
307 }
308 if raw.supports_computer_use == Some(true) {
309 abilities.push("ComputerUse");
310 }
311 if raw.supports_prompt_caching == Some(true) {
312 abilities.push("PromptCaching");
313 }
314 if raw.supports_assistant_prefill == Some(true) {
315 abilities.push("AssistantPrefill");
316 }
317 if mode == "image_generation" {
318 abilities.push("ImageGeneration");
319 }
320 if takes_per_message_effort(&provider, &canonical_id) {
321 abilities.push("PerMessageEffort");
322 }
323 if takes_mid_conversation_system(&provider, &canonical_id) {
324 abilities.push("MidConversationSystem");
325 }
326
327 Some(ConvertedEntry {
328 litellm_id: key.to_string(),
329 canonical_id,
330 provider,
331 mode: mode.to_string(),
332 max_input_tokens: max_input,
333 max_output_tokens: max_output,
334 input_cost_per_token: raw.input_cost_per_token.unwrap_or(0.0),
335 output_cost_per_token: raw.output_cost_per_token.unwrap_or(0.0),
336 cache_read_per_token: raw.cache_read_input_token_cost,
337 cache_write_per_token: raw.cache_creation_input_token_cost,
338 reasoning_per_token: raw.output_cost_per_reasoning_token,
339 image_per_token: raw.input_cost_per_audio_token,
340 abilities,
341 deprecation_date: raw.deprecation_date.clone(),
342 })
343}
344
345#[must_use]
352pub fn parse_litellm_json(json_bytes: &[u8]) -> Vec<ConvertedEntry> {
353 let map: serde_json::Map<String, serde_json::Value> =
354 serde_json::from_slice(json_bytes).expect("invalid LiteLLM JSON");
355
356 let mut entries = Vec::new();
357 for (key, value) in &map {
358 if key == "sample_spec" {
359 continue;
360 }
361 let Ok(raw) = serde_json::from_value::<RawLiteLLMEntry>(value.clone()) else {
362 continue;
363 };
364 if let Some(entry) = convert_litellm_entry(key, &raw) {
365 entries.push(entry);
366 }
367 }
368 entries
369}