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