1use crate::config::ProviderConfig;
2use crate::error::{LiteLLMError, Result};
3use crate::http::send_json;
4use crate::providers::{merge_extra_body, resolve_api_key};
5use crate::stream::{parse_sse_stream, ChatStream};
6use crate::types::{
7 ChatRequest, ChatResponse, EmbeddingRequest, EmbeddingResponse, ImageData, ImageRequest,
8 ImageResponse, Reasoning, Usage, VideoRequest, VideoResponse,
9};
10use base64::{engine::general_purpose, Engine as _};
11use reqwest::multipart::Form;
12use reqwest::Client;
13use serde::Deserialize;
14use serde_json::Value;
15use std::time::Duration;
16use tokio::time::sleep;
17
18pub const DEFAULT_VIDEO_MAX_POLL_ATTEMPTS: u32 = 120;
20pub const DEFAULT_VIDEO_POLL_INTERVAL_SECS: u64 = 5;
22
23#[derive(Debug, Deserialize)]
24struct OpenAIChatResponse {
25 id: Option<String>,
26 choices: Vec<OpenAIChoice>,
27 usage: Option<OpenAIUsage>,
28}
29
30#[derive(Debug, Deserialize)]
31struct OpenAIChoice {
32 message: OpenAIMessage,
33}
34
35#[derive(Debug, Deserialize)]
36struct OpenAIMessage {
37 content: Option<Value>,
38 reasoning_content: Option<String>,
39 reasoning: Option<String>,
40 reasoning_details: Option<Value>,
41}
42
43#[derive(Debug, Deserialize)]
44struct OpenAIUsage {
45 prompt_tokens: Option<u64>,
46 completion_tokens: Option<u64>,
47 total_tokens: Option<u64>,
48 cost: Option<Value>,
49 completion_tokens_details: Option<CompletionTokensDetails>,
50}
51
52#[derive(Debug, Deserialize)]
53struct CompletionTokensDetails {
54 reasoning_tokens: Option<u64>,
55}
56
57#[derive(Debug, Deserialize)]
58struct OpenAIEmbeddingResponse {
59 data: Vec<OpenAIEmbeddingItem>,
60 usage: Option<OpenAIUsage>,
61}
62
63#[derive(Debug, Deserialize)]
64struct OpenAIEmbeddingItem {
65 embedding: Vec<f32>,
66}
67
68fn build_chat_body(req: &ChatRequest, stream: bool) -> Result<Value> {
72 let mut body = serde_json::json!({
73 "model": req.model,
74 "messages": req.messages,
75 });
76
77 if stream {
78 body["stream"] = serde_json::json!(true);
79 }
80
81 if let Some(temp) = req.temperature {
82 body["temperature"] = serde_json::json!(temp);
83 }
84 if let Some(max_tokens) = req.max_tokens {
85 body["max_tokens"] = serde_json::json!(max_tokens);
86 }
87 if let Some(ref fmt) = req.response_format {
88 body["response_format"] = fmt.clone();
89 }
90 if let Some(max_completion_tokens) = req.max_completion_tokens {
91 body["max_completion_tokens"] = serde_json::json!(max_completion_tokens);
92 }
93 if let Some(ref tools) = req.tools {
94 body["tools"] = tools.clone();
95 }
96 if let Some(ref tool_choice) = req.tool_choice {
97 body["tool_choice"] = tool_choice.clone();
98 }
99 if let Some(parallel) = req.parallel_tool_calls {
100 body["parallel_tool_calls"] = serde_json::json!(parallel);
101 }
102 if let Some(ref stop) = req.stop {
103 body["stop"] = stop.clone();
104 }
105 if let Some(top_p) = req.top_p {
106 body["top_p"] = serde_json::json!(top_p);
107 }
108 if let Some(presence) = req.presence_penalty {
109 body["presence_penalty"] = serde_json::json!(presence);
110 }
111 if let Some(frequency) = req.frequency_penalty {
112 body["frequency_penalty"] = serde_json::json!(frequency);
113 }
114 if let Some(seed) = req.seed {
115 body["seed"] = serde_json::json!(seed);
116 }
117 if let Some(ref user) = req.user {
118 body["user"] = serde_json::json!(user);
119 }
120 if let Some(ref metadata) = req.metadata {
121 body["metadata"] = metadata.clone();
122 }
123 if let Some(ref reasoning_effort) = req.reasoning_effort {
124 body["reasoning_effort"] = reasoning_effort.clone();
125 }
126 if let Some(ref thinking) = req.thinking {
127 body["thinking"] = thinking.clone();
128 }
129
130 merge_extra_body(
131 &mut body,
132 req.extra_body.as_ref(),
133 &[
134 "model",
135 "messages",
136 "stream",
137 "temperature",
138 "max_tokens",
139 "response_format",
140 "max_completion_tokens",
141 "tools",
142 "tool_choice",
143 "parallel_tool_calls",
144 "stop",
145 "top_p",
146 "presence_penalty",
147 "frequency_penalty",
148 "seed",
149 "user",
150 "metadata",
151 "reasoning_effort",
152 "thinking",
153 ],
154 )?;
155
156 Ok(body)
157}
158
159pub async fn chat(client: &Client, cfg: &ProviderConfig, req: ChatRequest) -> Result<ChatResponse> {
160 let base = cfg
161 .base_url
162 .clone()
163 .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
164 let url = format!("{}/chat/completions", base.trim_end_matches('/'));
165 let key = resolve_api_key(cfg)?;
166
167 let body = build_chat_body(&req, false)?;
168
169 let mut builder = client.post(url).json(&body);
170 if let Some(key) = key {
171 builder = builder.bearer_auth(key);
172 }
173 for (k, v) in &cfg.extra_headers {
174 builder = builder.header(k, v);
175 }
176
177 let (parsed, headers) = send_json::<OpenAIChatResponse>(builder).await?;
178 let first_message = parsed.choices.first().map(|c| &c.message);
179 let content = first_message
180 .and_then(|message| extract_text_value(message.content.as_ref()))
181 .unwrap_or_default();
182 let reasoning = first_message.and_then(extract_reasoning);
183 let header_cost = headers
184 .get("x-litellm-response-cost")
185 .and_then(|v| v.to_str().ok())
186 .and_then(|v| v.parse::<f64>().ok());
187 let mut usage = map_usage(parsed.usage);
188 if usage.cost_usd.is_none() {
189 usage.cost_usd = header_cost;
190 }
191
192 Ok(ChatResponse {
193 content,
194 reasoning,
195 usage,
196 response_id: parsed.id,
197 header_cost,
198 raw: None,
199 })
200}
201
202fn extract_reasoning(message: &OpenAIMessage) -> Option<Reasoning> {
203 message
204 .reasoning_details
205 .as_ref()
206 .and_then(|details| details.as_array().cloned())
207 .and_then(Reasoning::from_details)
208 .or_else(|| {
209 message
210 .reasoning_content
211 .as_ref()
212 .or(message.reasoning.as_ref())
213 .cloned()
214 .and_then(Reasoning::from_text)
215 })
216}
217
218fn extract_text_value(value: Option<&Value>) -> Option<String> {
219 let mut out = String::new();
220 collect_text_fragments(value?, &mut out);
221 (!out.trim().is_empty()).then_some(out)
222}
223
224fn collect_text_fragments(value: &Value, out: &mut String) {
225 match value {
226 Value::String(text) => out.push_str(text),
227 Value::Array(items) => {
228 for item in items {
229 collect_text_fragments(item, out);
230 }
231 }
232 Value::Object(map) => {
233 if let Some(text) = map.get("text") {
234 collect_text_fragments(text, out);
235 } else if let Some(content) = map.get("content") {
236 collect_text_fragments(content, out);
237 } else if let Some(value) = map.get("value") {
238 collect_text_fragments(value, out);
239 }
240 }
241 _ => {}
242 }
243}
244
245pub async fn chat_stream(
246 client: &Client,
247 cfg: &ProviderConfig,
248 req: ChatRequest,
249) -> Result<ChatStream> {
250 let base = cfg
251 .base_url
252 .clone()
253 .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
254 let url = format!("{}/chat/completions", base.trim_end_matches('/'));
255 let key = resolve_api_key(cfg)?;
256
257 let body = build_chat_body(&req, true)?;
258
259 let mut builder = client.post(url).json(&body);
260 if let Some(key) = key {
261 builder = builder.bearer_auth(key);
262 }
263 for (k, v) in &cfg.extra_headers {
264 builder = builder.header(k, v);
265 }
266
267 let resp = builder.send().await.map_err(LiteLLMError::from)?;
268 let status = resp.status();
269 if !status.is_success() {
270 let text = resp.text().await.map_err(LiteLLMError::from)?;
271 return Err(LiteLLMError::http(format!(
272 "http {}: {}",
273 status.as_u16(),
274 text
275 )));
276 }
277
278 Ok(parse_sse_stream(resp.bytes_stream()))
279}
280
281pub async fn embeddings(
282 client: &Client,
283 cfg: &ProviderConfig,
284 req: EmbeddingRequest,
285) -> Result<EmbeddingResponse> {
286 let base = cfg
287 .base_url
288 .clone()
289 .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
290 let url = format!("{}/embeddings", base.trim_end_matches('/'));
291 let key = resolve_api_key(cfg)?;
292
293 let mut body = serde_json::json!({
294 "model": req.model,
295 "input": req.input,
296 });
297 merge_extra_body(&mut body, req.extra_body.as_ref(), &["model", "input"])?;
298
299 let mut builder = client.post(url).json(&body);
300 if let Some(key) = key {
301 builder = builder.bearer_auth(key);
302 }
303 for (k, v) in &cfg.extra_headers {
304 builder = builder.header(k, v);
305 }
306
307 let (parsed, _headers) = send_json::<OpenAIEmbeddingResponse>(builder).await?;
308 let vectors = parsed.data.into_iter().map(|d| d.embedding).collect();
309
310 Ok(EmbeddingResponse {
311 vectors,
312 usage: map_usage(parsed.usage),
313 raw: None,
314 })
315}
316
317pub async fn image_generation(
318 client: &Client,
319 cfg: &ProviderConfig,
320 req: ImageRequest,
321) -> Result<ImageResponse> {
322 let base = cfg
323 .base_url
324 .clone()
325 .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
326 let url = format!("{}/images/generations", base.trim_end_matches('/'));
327 let key = resolve_api_key(cfg)?;
328
329 let mut body = serde_json::json!({
330 "model": req.model,
331 "prompt": req.prompt,
332 });
333 if let Some(n) = req.n {
334 body["n"] = serde_json::json!(n);
335 }
336 if let Some(ref size) = req.size {
337 body["size"] = serde_json::json!(size);
338 }
339 if let Some(ref quality) = req.quality {
340 body["quality"] = serde_json::json!(quality);
341 }
342 if let Some(ref background) = req.background {
343 body["background"] = serde_json::json!(background);
344 }
345 merge_extra_body(&mut body, req.extra_body.as_ref(), &["model", "prompt"])?;
346
347 let mut builder = client.post(url).json(&body);
348 if let Some(key) = key {
349 builder = builder.bearer_auth(key);
350 }
351 for (k, v) in &cfg.extra_headers {
352 builder = builder.header(k, v);
353 }
354
355 let (parsed, _headers) = send_json::<Value>(builder).await?;
356 let images = parsed
357 .get("data")
358 .and_then(|v| v.as_array())
359 .map(|arr| {
360 arr.iter()
361 .map(|item| ImageData {
362 b64_json: item
363 .get("b64_json")
364 .and_then(|v| v.as_str())
365 .map(|s| s.to_string()),
366 url: item
367 .get("url")
368 .and_then(|v| v.as_str())
369 .map(|s| s.to_string()),
370 revised_prompt: item
371 .get("revised_prompt")
372 .and_then(|v| v.as_str())
373 .map(|s| s.to_string()),
374 mime_type: None,
375 })
376 .collect::<Vec<_>>()
377 })
378 .unwrap_or_default();
379
380 Ok(ImageResponse {
381 images,
382 usage: Usage::default(),
383 raw: None,
384 })
385}
386
387#[derive(Debug, Clone)]
389pub struct VideoGenerationOptions {
390 pub max_poll_attempts: u32,
392 pub poll_interval_secs: u64,
394}
395
396impl Default for VideoGenerationOptions {
397 fn default() -> Self {
398 Self {
399 max_poll_attempts: DEFAULT_VIDEO_MAX_POLL_ATTEMPTS,
400 poll_interval_secs: DEFAULT_VIDEO_POLL_INTERVAL_SECS,
401 }
402 }
403}
404
405pub async fn video_generation(
406 client: &Client,
407 cfg: &ProviderConfig,
408 req: VideoRequest,
409) -> Result<VideoResponse> {
410 video_generation_with_options(client, cfg, req, VideoGenerationOptions::default()).await
411}
412
413pub async fn video_generation_with_options(
414 client: &Client,
415 cfg: &ProviderConfig,
416 req: VideoRequest,
417 options: VideoGenerationOptions,
418) -> Result<VideoResponse> {
419 let base = cfg
420 .base_url
421 .clone()
422 .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
423 let url = format!("{}/videos", base.trim_end_matches('/'));
424 let key = resolve_api_key(cfg)?;
425
426 let mut form = Form::new()
427 .text("model", req.model)
428 .text("prompt", req.prompt);
429 if let Some(seconds) = req.seconds {
430 form = form.text("seconds", seconds.to_string());
431 }
432 if let Some(size) = req.size {
433 form = form.text("size", size);
434 }
435
436 let mut builder = client.post(url).multipart(form);
437 if let Some(ref key) = key {
438 builder = builder.bearer_auth(key.clone());
439 }
440 for (k, v) in &cfg.extra_headers {
441 builder = builder.header(k, v);
442 }
443
444 let (parsed, _headers) = send_json::<Value>(builder).await?;
445 let video_id = parsed
446 .get("id")
447 .and_then(|v| v.as_str())
448 .ok_or_else(|| LiteLLMError::Parse("missing video id".into()))?;
449
450 let status_url = format!("{}/videos/{}", base.trim_end_matches('/'), video_id);
451 let poll_interval = Duration::from_secs(options.poll_interval_secs);
452
453 for attempt in 0..options.max_poll_attempts {
454 let mut status_builder = client.get(&status_url);
455 if let Some(ref key) = key {
456 status_builder = status_builder.bearer_auth(key.clone());
457 }
458 let (status_resp, _headers) = send_json::<Value>(status_builder).await?;
459 let status = status_resp
460 .get("status")
461 .and_then(|v| v.as_str())
462 .unwrap_or("unknown");
463
464 match status {
465 "completed" => {
466 return fetch_video_content(client, &base, video_id, key.as_deref()).await;
467 }
468 "failed" => {
469 let msg = status_resp
470 .get("error")
471 .and_then(|v| v.as_str())
472 .unwrap_or("video generation failed");
473 return Err(LiteLLMError::http(msg.to_string()));
474 }
475 _ => {
476 if attempt + 1 >= options.max_poll_attempts {
477 return Err(LiteLLMError::http(format!(
478 "video generation timed out after {} attempts",
479 options.max_poll_attempts
480 )));
481 }
482 sleep(poll_interval).await;
483 }
484 }
485 }
486
487 Err(LiteLLMError::http("video generation timed out"))
488}
489
490async fn fetch_video_content(
491 client: &Client,
492 base: &str,
493 video_id: &str,
494 key: Option<&str>,
495) -> Result<VideoResponse> {
496 let content_url = format!("{}/videos/{}/content", base.trim_end_matches('/'), video_id);
497 let mut content_builder = client.get(&content_url);
498 if let Some(key) = key {
499 content_builder = content_builder.bearer_auth(key);
500 }
501
502 let bytes = content_builder
503 .send()
504 .await
505 .map_err(LiteLLMError::from)?
506 .bytes()
507 .await
508 .map_err(LiteLLMError::from)?;
509 let b64 = general_purpose::STANDARD.encode(bytes);
510
511 Ok(VideoResponse {
512 video_url: Some(format!("data:video/mp4;base64,{b64}")),
513 raw: None,
514 })
515}
516
517fn map_usage(usage: Option<OpenAIUsage>) -> Usage {
518 usage.map_or_else(Usage::default, |u| Usage {
519 prompt_tokens: u.prompt_tokens,
520 completion_tokens: u.completion_tokens,
521 thoughts_tokens: u.completion_tokens_details.and_then(|d| d.reasoning_tokens),
522 total_tokens: u.total_tokens,
523 cost_usd: parse_cost(u.cost.as_ref()),
524 })
525}
526
527fn parse_cost(value: Option<&Value>) -> Option<f64> {
528 let v = value?;
529 if let Some(n) = v.as_f64() {
530 return Some(n);
531 }
532 if let Some(s) = v.as_str() {
533 return s.parse::<f64>().ok();
534 }
535 None
536}