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_PER_MESSAGE_EFFORT_PLATFORMS: &[&str] = &["anthropic", "vertex_ai"];
139
140const OPENAI_FIRST_PER_MESSAGE_EFFORT_GENERATION: u32 = 6;
143
144#[must_use]
147pub fn takes_per_message_effort(provider: &str, canonical_id: &str) -> bool {
148 if ANTHROPIC_PER_MESSAGE_EFFORT_PLATFORMS.contains(&provider) {
149 let model = canonical_id
151 .split_once('@')
152 .map_or(canonical_id, |(model, _)| model);
153 return ANTHROPIC_PER_MESSAGE_EFFORT_MODELS.contains(&model);
154 }
155 if provider == "openai" {
156 return gpt_generation(canonical_id)
157 .is_some_and(|generation| generation >= OPENAI_FIRST_PER_MESSAGE_EFFORT_GENERATION);
158 }
159 false
160}
161
162fn gpt_generation(model: &str) -> Option<u32> {
164 let rest = model.strip_prefix("gpt-")?;
165 let digits = rest
166 .find(|c: char| !c.is_ascii_digit())
167 .unwrap_or(rest.len());
168 rest.get(..digits)?.parse().ok()
169}
170
171#[must_use]
173pub fn parse_provider(provider_str: &str) -> String {
174 let mapped = match provider_str {
175 "openai" | "text-completion-openai" | "chatgpt" => "openai",
176 "anthropic" => "anthropic",
177 "gemini" | "palm" => "gemini",
178 "deepseek" => "deepseek",
179 "xai" => "xai",
180 "mistral" | "codestral" | "text-completion-codestral" => "mistral",
181 "meta_llama" => "meta",
182 s if s.starts_with("bedrock") => "bedrock",
183 "azure" | "azure_ai" | "azure_text" => "azure",
184 s if s.starts_with("vertex_ai") => "vertex_ai",
185 "groq" => "groq",
186 "together_ai" => "together_ai",
187 s if s.starts_with("fireworks_ai") => "fireworks_ai",
188 "replicate" => "replicate",
189 "cohere" | "cohere_chat" => "cohere",
190 "perplexity" => "perplexity",
191 "github_copilot" => "github_copilot",
192 "dashscope" => "alibaba",
193 other => return other.to_string(),
194 };
195 mapped.to_string()
196}
197
198#[must_use]
200pub fn parse_mode(mode_str: &str) -> Option<&'static str> {
201 match mode_str {
202 "chat" | "responses" => Some("chat"),
203 "embedding" => Some("embedding"),
204 "image_generation" | "image_edit" => Some("image_generation"),
205 "audio_transcription" => Some("audio_transcription"),
206 "audio_speech" => Some("audio_speech"),
207 "rerank" => Some("rerank"),
208 "moderation" => Some("moderation"),
209 "completion" => Some("completion"),
210 "video_generation" => Some("video_generation"),
211 "search" | "vector_store" => Some("search"),
212 "ocr" => Some("ocr"),
213 _ => None,
214 }
215}
216
217#[derive(Debug, Clone)]
219pub struct ConvertedEntry {
220 pub litellm_id: String,
222 pub canonical_id: String,
224 pub provider: String,
226 pub mode: String,
228 pub max_input_tokens: Option<u32>,
230 pub max_output_tokens: Option<u32>,
232 pub input_cost_per_token: f64,
234 pub output_cost_per_token: f64,
236 pub cache_read_per_token: Option<f64>,
238 pub cache_write_per_token: Option<f64>,
240 pub reasoning_per_token: Option<f64>,
242 pub image_per_token: Option<f64>,
244 pub abilities: Vec<&'static str>,
246 pub deprecation_date: Option<String>,
248}
249
250#[must_use]
252pub fn convert_litellm_entry(key: &str, raw: &RawLiteLLMEntry) -> Option<ConvertedEntry> {
253 let provider_str = raw.litellm_provider.as_deref()?;
254 let mode_str = raw.mode.as_deref()?;
255 let mode = parse_mode(mode_str)?;
256 let provider = parse_provider(provider_str);
257 let canonical_id = strip_provider_prefix(key).to_string();
258
259 let max_input = raw.max_input_tokens;
260 let max_output = raw.max_output_tokens.or(raw.max_tokens);
261
262 let mut abilities = Vec::new();
263 if raw.supports_function_calling == Some(true) {
264 abilities.push("ToolUse");
265 }
266 if raw.supports_vision == Some(true) {
267 abilities.push("Vision");
268 }
269 if raw.supports_audio_input == Some(true) {
270 abilities.push("Audio");
271 }
272 if raw.supports_audio_output == Some(true) {
273 abilities.push("AudioOutput");
274 }
275 if raw.supports_video_input == Some(true) {
276 abilities.push("Video");
277 }
278 if raw.supports_web_search == Some(true) {
279 abilities.push("WebSearch");
280 }
281 if raw.supports_pdf_input == Some(true) {
282 abilities.push("Pdf");
283 }
284 if raw.supports_reasoning == Some(true) {
285 abilities.push("Reasoning");
286 }
287 if raw.supports_computer_use == Some(true) {
288 abilities.push("ComputerUse");
289 }
290 if raw.supports_prompt_caching == Some(true) {
291 abilities.push("PromptCaching");
292 }
293 if raw.supports_assistant_prefill == Some(true) {
294 abilities.push("AssistantPrefill");
295 }
296 if mode == "image_generation" {
297 abilities.push("ImageGeneration");
298 }
299 if takes_per_message_effort(&provider, &canonical_id) {
300 abilities.push("PerMessageEffort");
301 }
302
303 Some(ConvertedEntry {
304 litellm_id: key.to_string(),
305 canonical_id,
306 provider,
307 mode: mode.to_string(),
308 max_input_tokens: max_input,
309 max_output_tokens: max_output,
310 input_cost_per_token: raw.input_cost_per_token.unwrap_or(0.0),
311 output_cost_per_token: raw.output_cost_per_token.unwrap_or(0.0),
312 cache_read_per_token: raw.cache_read_input_token_cost,
313 cache_write_per_token: raw.cache_creation_input_token_cost,
314 reasoning_per_token: raw.output_cost_per_reasoning_token,
315 image_per_token: raw.input_cost_per_audio_token,
316 abilities,
317 deprecation_date: raw.deprecation_date.clone(),
318 })
319}
320
321#[must_use]
328pub fn parse_litellm_json(json_bytes: &[u8]) -> Vec<ConvertedEntry> {
329 let map: serde_json::Map<String, serde_json::Value> =
330 serde_json::from_slice(json_bytes).expect("invalid LiteLLM JSON");
331
332 let mut entries = Vec::new();
333 for (key, value) in &map {
334 if key == "sample_spec" {
335 continue;
336 }
337 let Ok(raw) = serde_json::from_value::<RawLiteLLMEntry>(value.clone()) else {
338 continue;
339 };
340 if let Some(entry) = convert_litellm_entry(key, &raw) {
341 entries.push(entry);
342 }
343 }
344 entries
345}