litellm-rs 0.6.0

A high-performance AI Gateway written in Rust, providing OpenAI-compatible APIs with intelligent routing, load balancing, and enterprise features
Documentation
//! Audio transcriptions endpoint

use crate::core::audio::types::TranscriptionRequest;
use crate::core::types::model::ProviderCapability;
use crate::server::state::AppState;
use actix_multipart::Multipart;
use actix_web::{HttpRequest, HttpResponse, Result as ActixResult, web};
use futures::StreamExt;
use tracing::{error, info};

use super::super::budgeted::{ApiKeyBudgetPolicy, run_unary};
use super::upload::{
    drain_field, parse_optional_f32_field, raw_response_format_error, read_audio_file,
    read_text_field, upload_error_response,
};
use crate::server::routes::ai::context::{
    enforce_api_key_model_and_token_limits, get_request_context,
};
use crate::server::routes::ai::openai_errors;

/// Audio transcriptions endpoint
///
/// OpenAI-compatible audio transcription API (Whisper).
/// Accepts multipart/form-data with audio file.
pub async fn audio_transcriptions(
    state: web::Data<AppState>,
    req: HttpRequest,
    mut payload: Multipart,
) -> ActixResult<HttpResponse> {
    info!("Audio transcriptions request");

    // Get request context (validates auth)
    let context = match get_request_context(&req) {
        Ok(ctx) => ctx,
        Err(_) => {
            return Ok(openai_errors::unauthorized_error("Unauthorized"));
        }
    };

    // Parse multipart form data
    let mut file_data: Option<Vec<u8>> = None;
    let mut filename = String::from("audio.mp3");
    let mut model = String::from("whisper-large-v3-turbo");
    let mut language: Option<String> = None;
    let mut prompt: Option<String> = None;
    let mut response_format: Option<String> = None;
    let mut temperature: Option<f32> = None;

    while let Some(item) = payload.next().await {
        let mut field = match item {
            Ok(f) => f,
            Err(e) => {
                error!("Error reading multipart field: {}", e);
                return Ok(openai_errors::validation_error(format!(
                    "Invalid multipart data: {}",
                    e
                )));
            }
        };

        let field_name = match field.name() {
            Some(name) => name.to_string(),
            None => {
                if let Err(e) = drain_field(&mut field).await {
                    return Ok(upload_error_response(e));
                }
                continue;
            }
        };

        match field_name.as_str() {
            "file" => {
                // Get filename from content disposition
                if let Some(cd) = field.content_disposition()
                    && let Some(fname) = cd.get_filename()
                {
                    filename = fname.to_string();
                }

                let data = match read_audio_file(&mut field).await {
                    Ok(data) => data,
                    Err(e) => return Ok(upload_error_response(e)),
                };
                file_data = Some(data);
            }
            "model" => match read_text_field(&mut field).await {
                Ok(value) if !value.is_empty() => model = value,
                Ok(_) => {}
                Err(e) => return Ok(upload_error_response(e)),
            },
            "language" => match read_text_field(&mut field).await {
                Ok(value) if !value.is_empty() => language = Some(value),
                Ok(_) => {}
                Err(e) => return Ok(upload_error_response(e)),
            },
            "prompt" => match read_text_field(&mut field).await {
                Ok(value) if !value.is_empty() => prompt = Some(value),
                Ok(_) => {}
                Err(e) => return Ok(upload_error_response(e)),
            },
            "response_format" => match read_text_field(&mut field).await {
                Ok(value) if !value.is_empty() => response_format = Some(value),
                Ok(_) => {}
                Err(e) => return Ok(upload_error_response(e)),
            },
            "temperature" => match read_text_field(&mut field).await {
                Ok(value) => match parse_optional_f32_field("temperature", &value) {
                    Ok(parsed) => temperature = parsed,
                    Err(response) => return Ok(response),
                },
                Err(e) => return Ok(upload_error_response(e)),
            },
            _ => {
                if let Err(e) = drain_field(&mut field).await {
                    return Ok(upload_error_response(e));
                }
            }
        }
    }

    // Validate file was provided
    let file = match file_data {
        Some(data) if !data.is_empty() => data,
        _ => {
            return Ok(openai_errors::validation_error("No audio file provided"));
        }
    };

    if let Some(error_response) = raw_response_format_error(response_format.as_deref()) {
        return Ok(error_response);
    }

    if let Err(error) = enforce_api_key_model_and_token_limits(&req, &model, None) {
        return Ok(openai_errors::gateway_error_response(&error));
    }

    // Create transcription request
    let transcription_request = TranscriptionRequest {
        file,
        filename,
        model: model.clone(),
        language,
        prompt,
        response_format,
        temperature,
        timestamp_granularities: None,
    };

    let requested_model = model;
    let context_for_execution = context.clone();
    let api_key_id = context.api_key_id();
    let api_key_budget_id = context.api_key_budget_id();
    let budgeted = state.budgeted.clone();
    let key_manager = budgeted.key_manager();
    let pricing_service = budgeted.pricing();
    let pricing_config = state.config().gateway.pricing.clone();

    match run_unary(
        &state.unified_router,
        &requested_model,
        ProviderCapability::AudioTranscription,
        move |provider, selected_model, _deployment_id| {
            let mut request = transcription_request.clone();
            let context = context_for_execution.clone();
            let budgeted = budgeted.clone();
            let key_manager = key_manager.clone();
            let pricing_service = pricing_service.clone();
            let pricing_config = pricing_config.clone();
            async move {
                let usage =
                    super::budgeting::audio_file_usage(&request.file, request.prompt.as_deref());
                let total_time_seconds =
                    super::budgeting::estimated_audio_file_seconds(&request.file);
                let budget_provider = provider.name().to_string();
                let (pricing_provider, pricing_model) =
                    super::super::spend::pricing_identity_for_provider(
                        pricing_service.as_ref(),
                        &provider,
                        &selected_model,
                    );
                request.model = selected_model.clone();
                let reserve_pricing_service = pricing_service.clone();
                let settle_pricing_service = pricing_service.clone();
                let reserve_pricing_config = pricing_config.clone();
                let settle_pricing_config = pricing_config;
                let reserve_pricing_provider = pricing_provider.clone();
                let reserve_pricing_model = pricing_model.clone();
                let settle_pricing_provider = pricing_provider;
                let settle_pricing_model = pricing_model;
                let reserve_usage = usage.clone();
                let settle_usage = usage;
                let settle_key_manager = key_manager.clone();
                budgeted
                    .for_selected_with_api_key_budget(
                        budget_provider.clone(),
                        selected_model.clone(),
                        api_key_budget_id,
                        ApiKeyBudgetPolicy::FromProviderReservation,
                    )
                    .reserve_call_settle(
                        |budget| {
                            super::budgeting::reserve_audio_provider_budget_with_pricing(
                                reserve_pricing_service.as_ref(),
                                &reserve_pricing_config,
                                budget.budget_limits(),
                                budget.provider(),
                                budget.model(),
                                &reserve_pricing_provider,
                                &reserve_pricing_model,
                                Some(total_time_seconds),
                                &reserve_usage,
                            )
                        },
                        || provider.audio_transcription(request, context),
                        |response, reservations, budget| {
                            let (budget_reservation, key_budget_reservation) =
                                reservations.into_parts();
                            async move {
                                let tokens_used = u64::from(settle_usage.total_tokens);
                                super::budgeting::record_audio_spend(
                                    settle_pricing_service.as_ref(),
                                    &settle_pricing_config,
                                    budget.budget_limits(),
                                    &settle_key_manager,
                                    api_key_id,
                                    budget.provider(),
                                    budget.model(),
                                    &settle_pricing_provider,
                                    &settle_pricing_model,
                                    Some(total_time_seconds),
                                    &settle_usage,
                                    budget_reservation,
                                    key_budget_reservation,
                                )
                                .await;
                                (response, tokens_used)
                            }
                        },
                    )
                    .await
            }
        },
    )
    .await
    {
        Ok(response) => Ok(HttpResponse::Ok().json(response)),
        Err(e) => {
            error!("Transcription error: {}", e);
            Ok(openai_errors::gateway_error_response(&e))
        }
    }
}