Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/openai_files_endpoints/files_endpoints.py: 34%
520 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1######################################################################
3# /v1/files Endpoints
5# Equivalent of https://platform.openai.com/docs/api-reference/files
6######################################################################
8import asyncio
9import traceback
10from collections.abc import Mapping, Sequence
11from typing import Any, BinaryIO, Final, TypedDict, cast, get_args
13import httpx
14from fastapi import (
15 APIRouter,
16 Depends,
17 File,
18 Form,
19 HTTPException,
20 Request,
21 Response,
22 UploadFile,
23 status,
24)
25from pydantic import TypeAdapter
26from typing_extensions import ReadOnly
28import litellm
29from litellm import CreateFileRequest, get_secret_str
30from litellm._logging import verbose_proxy_logger
31from litellm.litellm_core_utils.cloud_storage_security import (
32 is_managed_cloud_storage_uri,
33)
34from litellm.litellm_core_utils.core_helpers import get_or_create_metadata_bucket
35from litellm.llms.base_llm.files.litellm_db_storage_backend import LITELLM_DB_STORAGE_BACKEND_NAME
36from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
37from litellm.llms.base_llm.managed_resources.isolation import build_list_page
38from litellm.proxy._types import *
39from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
40from litellm.proxy.batches_endpoints.litellm_executed_batches import (
41 litellm_executed_provider_of,
42 resolve_litellm_executed_provider,
43)
44from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
45from litellm.proxy.common_utils.http_parsing_utils import (
46 _read_request_body,
47 extract_nested_form_metadata,
48)
49from litellm.proxy.common_utils.openai_endpoint_utils import (
50 get_custom_llm_provider_from_request_body,
51 get_custom_llm_provider_from_request_headers,
52 get_custom_llm_provider_from_request_query,
53)
54from litellm.proxy.common_utils.openai_error_payload import (
55 error_status_code,
56 openai_error_param,
57 openai_error_type,
58)
59from litellm.proxy.openai_files_endpoints.batch_file_validation import (
60 BATCH_LINE_SHAPE,
61 PASSTHROUGH_BATCH_LINE_SHAPE,
62 check_batch_file_upload,
63 raise_batch_file_validation_failure,
64)
65from litellm.proxy.openai_files_endpoints.batch_guardrails import (
66 EMPTY_MAPPING,
67 BatchScanResult,
68 raise_nothing_to_submit,
69 raise_public,
70 rewrite_batch_input_file,
71 scan_batch_input_file,
72)
73from litellm.proxy.openai_files_endpoints.common_utils import (
74 _is_base64_encoded_unified_file_id,
75 add_internal_model_credentials,
76 apply_team_provider_credentials,
77 authorize_model_for_key,
78 encode_file_id_with_model,
79 extract_file_creation_params,
80 get_authorized_credentials_for_model,
81 handle_model_based_routing,
82 prepare_data_with_credentials,
83 validate_file_list_limit,
84 validate_managed_files_requirement,
85 validate_managed_id_requirement,
86)
87from litellm.proxy.openai_files_endpoints.general_upload_validation import (
88 MB,
89 check_allowed_extension,
90 check_blocked_extension,
91 check_unsafe_filename,
92 check_upload_file_size,
93 coerce_optional_int_setting,
94 coerce_optional_str_list_setting,
95 raise_upload_validation_failure,
96)
97from litellm.proxy.utils import PrismaClient, ProxyLogging, is_known_model
98from litellm.repositories.table_repositories import ManagedFileRepository
99from litellm.router import Router
100from litellm.types.llms.openai import (
101 CREATE_FILE_REQUESTS_PURPOSE,
102 FileExpiresAfter,
103 FileListPage,
104 OpenAIFileObject,
105 OpenAIFilesPurpose,
106)
108router: Final = APIRouter()
111def _names_a_litellm_executed_provider(llm_router: Router, candidate: str, team_id: str | None) -> bool:
112 credentials: Final = llm_router.get_deployment_credentials_with_provider(model_id=candidate, team_id=team_id)
113 return credentials is not None and litellm_executed_provider_of(credentials) is not None
116async def _litellm_executed_batch_input_model(
117 llm_router: Router | None,
118 purpose: OpenAIFilesPurpose,
119 model: str | None,
120 target_model_names_list: Sequence[str],
121 user_api_key_dict: UserAPIKeyAuth,
122 explicit_storage: str | None,
123) -> str | None:
124 if llm_router is None:
125 return None
126 candidates: Final = (model,) if model is not None else tuple(target_model_names_list)
127 team_id: Final = user_api_key_dict.team_id
128 await asyncio.gather(
129 *(
130 authorize_model_for_key(model_id=candidate, llm_router=llm_router, user_api_key_dict=user_api_key_dict)
131 for candidate in candidates
132 if _names_a_litellm_executed_provider(llm_router, candidate, team_id)
133 )
134 )
135 if explicit_storage is not None:
136 return None
137 providers: Final = await asyncio.gather(
138 *(resolve_litellm_executed_provider(llm_router, candidate, team_id) for candidate in candidates)
139 )
140 executed: Final = tuple(
141 candidate for candidate, provider in zip(candidates, providers, strict=True) if provider is not None
142 )
143 if not executed:
144 return None
145 if purpose != "batch":
146 raise ProxyException(
147 message=(
148 f"The server behind {', '.join(executed)} has no Files API, so LiteLLM keeps only batch input "
149 f"files for it and runs the batch itself: upload with purpose=batch; got purpose={purpose}"
150 ),
151 type="invalid_request_error",
152 param="purpose",
153 code=400,
154 )
155 if len(candidates) == 1:
156 return executed[0]
157 raise ProxyException(
158 message=(
159 f"LiteLLM runs batches for {', '.join(executed)} itself and keeps their input files, so a batch "
160 f"input file can target only that one model; got target_model_names={', '.join(candidates)}"
161 ),
162 type="invalid_request_error",
163 param="target_model_names",
164 code=400,
165 )
168_MAX_BATCH_FILE_SIZE_MB_ADAPTER: Final = TypeAdapter(int | None)
169_LISTED_FILES_ADAPTER: Final = TypeAdapter(list[OpenAIFileObject])
172class UploadedFileInfo(TypedDict):
173 filename: ReadOnly[str | None]
174 content_type: ReadOnly[str | None]
175 size: ReadOnly[int | None]
178files_config = None
181def set_files_config(config):
182 global files_config
183 if config is None: 183 ↛ 186line 183 didn't jump to line 186 because the condition on line 183 was always true
184 return
186 if not isinstance(config, list):
187 raise ValueError("invalid files config, expected a list is not a list")
189 for element in config:
190 if isinstance(element, dict):
191 for key, value in element.items():
192 if isinstance(value, str) and value.startswith("os.environ/"):
193 element[key] = get_secret_str(value)
195 files_config = config
198def get_files_provider_config(
199 custom_llm_provider: str,
200):
201 global files_config
202 if custom_llm_provider == "vertex_ai":
203 return None
204 if files_config is None:
205 raise ValueError("files_settings is not set, set it on your config.yaml file.")
206 for setting in files_config:
207 if setting.get("custom_llm_provider") == custom_llm_provider:
208 return setting
209 return None
212def _deployment_provider(llm_router: Router, model_id: str, team_id: str | None) -> str | None:
213 credentials: Final = llm_router.get_deployment_credentials_with_provider(model_id=model_id, team_id=team_id)
214 return None if credentials is None else credentials.get("custom_llm_provider")
217def _resolves_to_vertex_deployments_only(llm_router: Router | None, model_name: str, team_id: str | None) -> bool:
218 if llm_router is None or _deployment_provider(llm_router, model_name, team_id) != "vertex_ai":
219 return False
220 return all(
221 _deployment_provider(llm_router, str(deployment["model_info"]["id"]), team_id) == "vertex_ai"
222 for deployment in llm_router.get_model_list(model_name=model_name, team_id=team_id) or ()
223 if "id" in deployment.get("model_info", {})
224 )
227def _validate_passthrough_upload(
228 *,
229 purpose: str,
230 target_model_names: Sequence[str],
231 model: str | None,
232 target_storage: str | None,
233 llm_router: Router | None,
234 team_id: str | None,
235) -> None:
236 if purpose != "batch":
237 raise ProxyException(
238 message=(
239 "`passthrough` uploads the file bytes unchanged for a native Vertex batch, "
240 f"so purpose must be 'batch', got '{purpose}'."
241 ),
242 type="invalid_request_error",
243 param="passthrough",
244 code=400,
245 )
246 if target_storage and target_storage != "default":
247 raise ProxyException(
248 message=(
249 "`passthrough` writes the native batch file to the Vertex AI deployment's GCS bucket, "
250 f"so it cannot be combined with target_storage='{target_storage}'."
251 ),
252 type="invalid_request_error",
253 param="target_storage",
254 code=400,
255 )
256 named_deployments: Final = (
257 *(("target_model_names", name) for name in target_model_names),
258 *((("model", model),) if model else ()),
259 )
260 if not named_deployments:
261 raise ProxyException(
262 message=(
263 "`passthrough` needs the Vertex AI deployment that will run the batch, "
264 "since native rows carry no model: pass `target_model_names` or `model`."
265 ),
266 type="invalid_request_error",
267 param="target_model_names",
268 code=400,
269 )
270 offending: Final = next(
271 (
272 (param, name)
273 for param, name in named_deployments
274 if not _resolves_to_vertex_deployments_only(llm_router, name, team_id)
275 ),
276 None,
277 )
278 if offending is None:
279 return
280 param, name = offending
281 raise ProxyException(
282 message=(
283 f"`passthrough` is only supported for Vertex AI deployments; '{name}' does not resolve "
284 "to vertex_ai deployments only."
285 ),
286 type="invalid_request_error",
287 param=param,
288 code=400,
289 )
292async def _scan_batch_upload(
293 *,
294 file_source: bytes | BinaryIO,
295 purpose: str,
296 passthrough: bool,
297 request_metadata: Mapping[str, object],
298 user_api_key_dict: UserAPIKeyAuth,
299 proxy_logging_obj: ProxyLogging,
300) -> BatchScanResult | None:
301 """Guardrail the records of a batch input file, or None when this upload has nothing to scan."""
302 if (
303 purpose != "batch"
304 or isinstance(file_source, bytes)
305 or not proxy_logging_obj.has_pre_call_guardrails(request_metadata)
306 ):
307 return None
308 if passthrough:
309 raise ProxyException(
310 message=(
311 "Batch guardrails cannot scan native Vertex batch rows, so a `passthrough` upload is refused "
312 "when the key, team, or request has pre-call guardrails configured. "
313 "The file was not forwarded to the provider."
314 ),
315 type="invalid_request_error",
316 param="passthrough",
317 code=400,
318 )
319 outcome: Final = await scan_batch_input_file(
320 file_source=file_source,
321 request_metadata=request_metadata,
322 user_api_key_dict=user_api_key_dict,
323 proxy_logging_obj=proxy_logging_obj,
324 )
325 if not isinstance(outcome, BatchScanResult):
326 raise_public(outcome)
327 if outcome.changes and outcome.submitted_records == 0:
328 raise_nothing_to_submit()
329 return outcome
332def get_first_json_object(file_source: bytes | BinaryIO) -> dict | None:
333 """
334 The first record, used to pick a deployment when batch load balancing is on.
336 Read the way the upload validation reads it, since a file it accepted must not lose its
337 routing here: blank lines are not records and are skipped, and the line is parsed as bytes so
338 the json module sniffs the encoding rather than rejecting a leading byte order mark. Either
339 difference makes this return None, which silently sends the batch to the default provider.
340 """
341 try:
342 if isinstance(file_source, (bytes, bytearray)):
343 first_record: bytes | None = next((line for line in file_source.splitlines() if line.strip()), None)
344 else:
345 # lazily, so a batch file that can be gigabytes is not read past its first record
346 file_source.seek(0)
347 first_record = next((line for line in file_source if line.strip()), None)
348 file_source.seek(0)
349 return None if first_record is None else json.loads(first_record.strip())
350 except (json.JSONDecodeError, UnicodeDecodeError, OSError, ValueError):
351 return None
354def get_model_from_json_obj(json_object: dict) -> str | None:
355 """
356 The model a record names, or None when it does not name one readably.
358 The upload validation only checks that `body` is present, not that it is an object, so a
359 record can carry a string there and reach this. Returning None sends the upload down the
360 default-provider branch, which is what a record with no resolvable model already did.
361 """
362 body: Final = json_object.get("body")
363 return body.get("model") if isinstance(body, dict) else None
366async def _deprecated_loadbalanced_create_file(
367 llm_router: Router | None,
368 router_model: str,
369 _create_file_request: CreateFileRequest,
370) -> OpenAIFileObject:
371 if llm_router is None:
372 raise HTTPException(
373 status_code=500,
374 detail={"error": "LLM Router not initialized. Ensure models added to proxy."},
375 )
377 response: Final = await llm_router.acreate_file(model=router_model, **_create_file_request)
378 return response
381async def route_create_file(
382 llm_router: Router | None,
383 _create_file_request: CreateFileRequest,
384 purpose: OpenAIFilesPurpose,
385 proxy_logging_obj: ProxyLogging,
386 user_api_key_dict: UserAPIKeyAuth,
387 target_model_names_list: list[str],
388 is_router_model: bool,
389 router_model: str | None,
390 custom_llm_provider: str,
391 model: str | None = None,
392 target_storage: str | None = "default",
393) -> OpenAIFileObject:
394 """
395 Route file creation request to the appropriate provider.
397 Priority:
398 1. If target_storage is specified and not "default" -> use storage backend
399 2. If model parameter provided -> use model credentials and encode ID
400 3. If target_model_names_list -> managed files (requires DB, supports loadbalancing)
401 4. If enable_loadbalancing_on_batch_endpoints -> deprecated loadbalancing
402 5. Else -> use custom_llm_provider with files_settings
403 """
405 explicit_storage: Final = target_storage if target_storage and target_storage != "default" else None
406 if explicit_storage == LITELLM_DB_STORAGE_BACKEND_NAME:
407 raise ProxyException(
408 message=(
409 f"target_storage={LITELLM_DB_STORAGE_BACKEND_NAME} is not a storage a caller can pick: LiteLLM "
410 "chooses it on its own for the batch input files of a model whose batches it runs itself, so "
411 "upload with purpose=batch and name that model instead of target_storage"
412 ),
413 type="invalid_request_error",
414 param="target_storage",
415 code=400,
416 )
417 executed_model: Final = await _litellm_executed_batch_input_model(
418 llm_router, purpose, model, target_model_names_list, user_api_key_dict, explicit_storage
419 )
420 storage: Final = explicit_storage or (LITELLM_DB_STORAGE_BACKEND_NAME if executed_model is not None else None)
421 if storage is not None:
422 from litellm.litellm_core_utils.prompt_templates.common_utils import (
423 extract_file_data,
424 )
425 from litellm.proxy.openai_files_endpoints.storage_backend_service import (
426 StorageBackendFileService,
427 )
428 from litellm.proxy.proxy_server import prisma_client
430 return await StorageBackendFileService.upload_file_to_storage_backend(
431 file_data=extract_file_data(cast(Any, _create_file_request.get("file"))),
432 target_storage=storage,
433 target_model_names=(executed_model,) if executed_model is not None else target_model_names_list,
434 purpose=purpose,
435 proxy_logging_obj=proxy_logging_obj,
436 user_api_key_dict=user_api_key_dict,
437 prisma_client=prisma_client,
438 )
440 # NEW: Handle model-based routing (no DB required)
441 if model is not None:
442 # Get credentials from model_list via router
443 credentials: Final = await get_authorized_credentials_for_model(
444 llm_router=llm_router,
445 model_id=model,
446 user_api_key_dict=user_api_key_dict,
447 operation_context="file upload",
448 )
450 # Merge credentials into the request
451 prepare_data_with_credentials(
452 data=_create_file_request,
453 credentials=credentials,
454 )
456 # Create the file with model credentials
457 response = await litellm.acreate_file(
458 **_create_file_request,
459 custom_llm_provider=credentials["custom_llm_provider"],
460 )
462 # Encode the file ID with model information
463 if response and hasattr(response, "id") and response.id:
464 original_id: Final = response.id
465 encoded_id: Final = encode_file_id_with_model(file_id=original_id, model=model)
466 response.id = encoded_id
467 verbose_proxy_logger.debug("Encoded file ID: %s -> %s (model: %s)", original_id, encoded_id, model)
469 return response
471 # Handle managed files (supports loadbalancing via llm_router.acreate_file)
472 # Priority: Check for managed files BEFORE deprecated loadbalancing
473 if target_model_names_list:
474 managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
475 if managed_files_obj is None:
476 raise ProxyException(
477 message="Managed files hook not found",
478 type=ProxyErrorTypes.internal_server_error.value,
479 param=None,
480 code=500,
481 )
482 if llm_router is None:
483 raise ProxyException(
484 message="LLM Router not found",
485 type=ProxyErrorTypes.internal_server_error.value,
486 param=None,
487 code=500,
488 )
489 if not isinstance(managed_files_obj, BaseFileEndpoints):
490 raise ProxyException(
491 message="Managed files hook is not a BaseFileEndpoints",
492 type=ProxyErrorTypes.internal_server_error.value,
493 param=None,
494 code=500,
495 )
496 # Managed files internally calls llm_router.acreate_file() which includes loadbalancing
497 response = await managed_files_obj.acreate_file(
498 llm_router=llm_router,
499 create_file_request=_create_file_request,
500 target_model_names_list=target_model_names_list,
501 litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
502 user_api_key_dict=user_api_key_dict,
503 )
504 # EXISTING: Deprecated loadbalancing approach (for backwards compatibility when not using managed files)
505 elif litellm.enable_loadbalancing_on_batch_endpoints is True and is_router_model and router_model is not None:
506 response = await _deprecated_loadbalanced_create_file(
507 llm_router=llm_router,
508 router_model=router_model,
509 _create_file_request=_create_file_request,
510 )
511 else:
512 apply_team_provider_credentials(
513 data=cast(dict, _create_file_request), # cast-ok: TypedDict is a plain dict at runtime; merged in place
514 llm_router=llm_router,
515 user_api_key_dict=user_api_key_dict,
516 custom_llm_provider=custom_llm_provider,
517 )
518 # get configs for custom_llm_provider
519 llm_provider_config: Final = get_files_provider_config(custom_llm_provider=custom_llm_provider)
520 if llm_provider_config is not None:
521 # add llm_provider_config to data
522 _create_file_request.update(llm_provider_config)
523 _create_file_request.pop("custom_llm_provider", None)
524 # for now use custom_llm_provider=="openai" -> this will change as LiteLLM adds more providers for acreate_batch
525 response = await litellm.acreate_file(**_create_file_request, custom_llm_provider=custom_llm_provider)
527 return response
530@router.post(
531 "/{provider}/v1/files",
532 dependencies=[Depends(user_api_key_auth)],
533 tags=["files"],
534)
535@router.post(
536 "/v1/files",
537 dependencies=[Depends(user_api_key_auth)],
538 tags=["files"],
539)
540@router.post(
541 "/files",
542 dependencies=[Depends(user_api_key_auth)],
543 tags=["files"],
544)
545async def create_file(
546 request: Request,
547 fastapi_response: Response,
548 purpose: str = Form(...),
549 target_model_names: str = Form(default=""),
550 target_storage: str = Form(default="default"),
551 provider: str | None = None,
552 custom_llm_provider: str = Form(default="openai"),
553 file: UploadFile = File(...),
554 litellm_metadata: str | None = Form(default=None),
555 passthrough: bool = Form(default=False),
556 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
557):
558 """
559 Upload a file that can be used across - Assistants API, Batch API
560 This is the equivalent of POST https://api.openai.com/v1/files
562 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/create
564 Example Curl
565 ```
566 curl http://localhost:4000/v1/files \
567 -H "Authorization: Bearer sk-1234" \
568 -F purpose="batch" \
569 -F file="@mydata.jsonl"
570 -F expires_after[anchor]="created_at" \
571 -F expires_after[seconds]=2592000
572 ```
573 """
574 from litellm.proxy.proxy_server import (
575 add_litellm_data_to_request,
576 general_settings,
577 llm_router,
578 proxy_config,
579 proxy_logging_obj,
580 version,
581 )
583 data: dict = {}
584 # Spools this request owns. Starlette owns the upload handle; anything the guardrail scan
585 # opens is ours, and a batch upload that fails after the scan would otherwise hold the
586 # descriptor and its disk blocks until the collector runs.
587 spools: Final[list[BinaryIO]] = [] # mutable-ok: filled as the scan opens handles
588 try:
589 unsafe_filename_failure: Final = check_unsafe_filename(file.filename)
590 if unsafe_filename_failure is not None: 590 ↛ 591line 590 didn't jump to line 591 because the condition on line 590 was never true
591 raise_upload_validation_failure(unsafe_filename_failure)
593 max_file_size_mb: Final = coerce_optional_int_setting(general_settings.get("max_file_size_mb"))
595 # Batch uploads can be gigabytes. Starlette has already spooled the upload
596 # to disk, so stream from that handle instead of reading it into memory.
597 # Other uploads stay in-memory bytes, bounded to max_file_size_mb (plus one
598 # byte, to still tell "exactly at the limit" from "over it") when it is set,
599 # so an oversized upload cannot be read to completion before it is rejected.
600 file_source: bytes | BinaryIO
601 if purpose == "batch": 601 ↛ 602line 601 didn't jump to line 602 because the condition on line 601 was never true
602 await file.seek(0)
603 file_source = file.file
604 elif max_file_size_mb is not None and max_file_size_mb > 0: 604 ↛ 605line 604 didn't jump to line 605 because the condition on line 604 was never true
605 file_source = await file.read(max_file_size_mb * MB + 1)
606 else:
607 file_source = await file.read()
608 custom_llm_provider = (
609 provider
610 or get_custom_llm_provider_from_request_headers(request=request)
611 or get_custom_llm_provider_from_request_query(request=request)
612 or await get_custom_llm_provider_from_request_body(request=request)
613 or "openai"
614 )
616 # Extract file creation parameters using utility function
617 request_body: Final = await _read_request_body(request=request) or {}
618 file_params: Final = await extract_file_creation_params(
619 request=request,
620 request_body=request_body,
621 target_model_names_form=target_model_names,
622 target_storage_form=target_storage,
623 )
625 target_storage = file_params.target_storage
626 target_model_names_list: Final = file_params.target_model_names
627 model_param: Final = file_params.model
629 validate_managed_files_requirement(target_model_names=target_model_names_list, model=model_param)
631 # Prepare the data for forwarding
633 valid_purposes: Final = get_args(OpenAIFilesPurpose)
634 if purpose not in valid_purposes: 634 ↛ 642line 634 didn't jump to line 642 because the condition on line 634 was always true
635 raise ProxyException(
636 message=f"Invalid purpose: {purpose}. Must be one of: {valid_purposes}",
637 type="invalid_request_error",
638 param="purpose",
639 code=400,
640 )
641 # Cast purpose to OpenAIFilesPurpose type
642 purpose = cast(OpenAIFilesPurpose, purpose)
644 general_size_failure: Final = check_upload_file_size(file_source, max_file_size_mb)
645 if general_size_failure is not None:
646 raise_upload_validation_failure(general_size_failure)
648 allowed_extensions: Final = coerce_optional_str_list_setting(general_settings.get("allowed_file_extensions"))
649 allowed_extension_failure: Final = check_allowed_extension(file.filename, allowed_extensions)
650 if allowed_extension_failure is not None:
651 raise_upload_validation_failure(allowed_extension_failure)
653 blocked_extensions: Final = coerce_optional_str_list_setting(general_settings.get("blocked_file_extensions"))
654 blocked_extension_failure: Final = check_blocked_extension(file.filename, blocked_extensions)
655 if blocked_extension_failure is not None:
656 raise_upload_validation_failure(blocked_extension_failure)
658 if passthrough:
659 _validate_passthrough_upload(
660 purpose=purpose,
661 target_model_names=target_model_names_list,
662 model=model_param,
663 target_storage=target_storage,
664 llm_router=llm_router,
665 team_id=user_api_key_dict.team_id,
666 )
668 if purpose == "batch":
669 batch_file_failure: Final = await asyncio.to_thread(
670 check_batch_file_upload,
671 file.filename,
672 file_source,
673 _MAX_BATCH_FILE_SIZE_MB_ADAPTER.validate_python(general_settings.get("max_batch_file_size_mb")),
674 PASSTHROUGH_BATCH_LINE_SHAPE if passthrough else BATCH_LINE_SHAPE,
675 )
676 if batch_file_failure is not None:
677 raise_batch_file_validation_failure(batch_file_failure)
679 data = {"passthrough": True} if passthrough else {}
681 # Parse expires_after if provided
682 expires_after: FileExpiresAfter | None = None
683 form_data_raw: Final = await request.form()
684 form_data_dict: Final[Mapping[str, object]] = dict(form_data_raw)
685 extracted_litellm_metadata: Final[Mapping[str, object] | None] = extract_nested_form_metadata(
686 form_data=form_data_dict, prefix="litellm_metadata["
687 )
688 expires_after_anchor: Final = form_data_raw.get("expires_after[anchor]")
689 expires_after_seconds_str: Final = form_data_raw.get("expires_after[seconds]")
691 # Add litellm_metadata to data if provided (from form field)
692 if extracted_litellm_metadata is not None:
693 data["litellm_metadata"] = extracted_litellm_metadata
695 if expires_after_anchor is not None or expires_after_seconds_str is not None:
696 if expires_after_anchor is None or expires_after_seconds_str is None:
697 raise HTTPException(
698 status_code=400,
699 detail={
700 "error": "Both expires_after[anchor] and expires_after[seconds] must be provided if expires_after is specified",
701 },
702 )
704 # Validate expires_after[anchor] is a string (not UploadFile)
705 if isinstance(expires_after_anchor, UploadFile):
706 raise HTTPException(
707 status_code=400,
708 detail={
709 "error": "expires_after[anchor] must be a string, not a file upload",
710 },
711 )
713 # Validate expires_after[seconds] is a string (not UploadFile)
714 # Use positive isinstance check for proper type narrowing (matches codebase pattern)
715 if not isinstance(expires_after_seconds_str, str):
716 raise HTTPException(
717 status_code=400,
718 detail={
719 "error": "expires_after[seconds] must be a string, not a file upload",
720 },
721 )
722 # After this check, mypy knows expires_after_seconds_str is str
723 expires_after_seconds_str_validated: Final[str] = expires_after_seconds_str
725 # Validate anchor is "created_at"
726 if expires_after_anchor != "created_at":
727 raise HTTPException(
728 status_code=400,
729 detail={
730 "error": f"expires_after[anchor] must be 'created_at', got '{expires_after_anchor}'",
731 },
732 )
734 # Convert seconds to int
735 try:
736 expires_after_seconds: Final = int(expires_after_seconds_str_validated)
737 except (ValueError, TypeError) as e:
738 raise HTTPException(
739 status_code=400,
740 detail={
741 "error": f"expires_after[seconds] must be a valid integer, got '{expires_after_seconds_str}': {e}",
742 },
743 )
745 # Use literal "created_at" (not variable) for TypedDict to satisfy Literal type
746 expires_after = FileExpiresAfter(
747 anchor="created_at", # Literal, not expires_after_anchor variable
748 seconds=expires_after_seconds,
749 )
751 # Include original request and headers in the data
752 data = await add_litellm_data_to_request(
753 data=data,
754 request=request,
755 general_settings=general_settings,
756 user_api_key_dict=user_api_key_dict,
757 version=version,
758 proxy_config=proxy_config,
759 )
761 uploaded_file_info: Final[UploadedFileInfo] = {
762 "filename": file.filename,
763 "content_type": file.content_type,
764 "size": file.size,
765 }
766 data["purpose"] = purpose
767 data["file"] = uploaded_file_info
768 hooked_data: Final = await proxy_logging_obj.pre_call_hook(
769 user_api_key_dict=user_api_key_dict,
770 data=data,
771 call_type="acreate_file",
772 )
773 data = hooked_data if hooked_data is not None else data
774 data.pop("purpose", None)
775 data.pop("file", None)
777 # /v1/files stores its proxy metadata under litellm_metadata, not metadata
778 request_metadata: Final = data.get("metadata") or data.get("litellm_metadata") or EMPTY_MAPPING
779 scan_result: Final = await _scan_batch_upload(
780 file_source=file_source,
781 purpose=purpose,
782 passthrough=passthrough,
783 request_metadata=request_metadata,
784 user_api_key_dict=user_api_key_dict,
785 proxy_logging_obj=proxy_logging_obj,
786 )
787 if scan_result is not None and scan_result.changes:
788 # The caller sees this in the response; a proxy admin needs it server side too,
789 # and it has to land before the post-call hook for logging callbacks to pick it up.
790 get_or_create_metadata_bucket(data)[1]["batch_guardrail"] = scan_result.report().model_dump()
791 verbose_proxy_logger.warning(
792 "batch guardrails changed %s of %s records in %s: %s",
793 len(scan_result.changes),
794 scan_result.scanned_records,
795 file.filename,
796 scan_result.summary(),
797 )
799 # Prepare the file data according to FileTypes
800 if scan_result is not None:
801 spools.append(scan_result.redactions)
802 upload_source: Final = (
803 await asyncio.to_thread(rewrite_batch_input_file, file_source, scan_result)
804 if scan_result is not None and scan_result.changes
805 else file_source
806 )
807 if upload_source is not file_source:
808 spools.append(upload_source)
809 file_data: Final = (file.filename, upload_source, file.content_type)
811 ## check if model is a loadbalanced model
812 router_model: str | None = None
813 is_router_model = False
814 if litellm.enable_loadbalancing_on_batch_endpoints is True:
815 json_obj: Final = get_first_json_object(upload_source)
816 if json_obj:
817 router_model = get_model_from_json_obj(json_object=json_obj)
818 is_router_model = is_known_model(model=router_model, llm_router=llm_router)
820 # Apply team-level file expiry enforcement
821 team_metadata: Final = user_api_key_dict.team_metadata or {}
822 enforced_file_expiry: Final = team_metadata.get("enforced_file_expires_after")
823 if enforced_file_expiry is not None:
824 if "anchor" not in enforced_file_expiry or "seconds" not in enforced_file_expiry:
825 raise HTTPException(
826 status_code=500,
827 detail={
828 "error": "Server configuration error: team metadata field 'enforced_file_expires_after' is malformed - must contain 'anchor' and 'seconds' keys. Contact your team or proxy admin to fix this setting.",
829 },
830 )
831 if enforced_file_expiry["anchor"] != "created_at":
832 raise HTTPException(
833 status_code=500,
834 detail={
835 "error": f"Server configuration error: team metadata field 'enforced_file_expires_after' has invalid anchor '{enforced_file_expiry['anchor']}' - must be 'created_at'. Contact your team or proxy admin to fix this setting.",
836 },
837 )
838 expires_after = FileExpiresAfter(
839 anchor="created_at",
840 seconds=int(enforced_file_expiry["seconds"]),
841 )
843 verbose_proxy_logger.debug("create_file expires_after: %s", expires_after)
845 _create_file_request: Final = CreateFileRequest(
846 file=file_data,
847 purpose=cast(CREATE_FILE_REQUESTS_PURPOSE, purpose),
848 expires_after=expires_after,
849 **data,
850 )
852 response = await route_create_file(
853 llm_router=llm_router,
854 _create_file_request=_create_file_request,
855 purpose=purpose,
856 proxy_logging_obj=proxy_logging_obj,
857 user_api_key_dict=user_api_key_dict,
858 target_model_names_list=target_model_names_list,
859 is_router_model=is_router_model,
860 router_model=router_model,
861 custom_llm_provider=custom_llm_provider,
862 model=model_param,
863 target_storage=target_storage,
864 )
866 if response is None:
867 raise HTTPException(
868 status_code=500,
869 detail={"error": "Failed to create file. Please try again."},
870 )
871 ### ALERTING ###
872 asyncio.create_task(
873 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
874 )
876 ## POST CALL HOOKS ###
877 _response: Final = await proxy_logging_obj.post_call_success_hook(
878 data=data, user_api_key_dict=user_api_key_dict, response=response
879 )
880 if _response is not None and isinstance(_response, OpenAIFileObject):
881 response = _response
883 if scan_result is not None and scan_result.changes:
884 response.litellm_batch_guardrail = scan_result.report()
886 ### RESPONSE HEADERS ###
887 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
888 model_id: Final = hidden_params.get("model_id", None) or ""
889 cache_key: Final = hidden_params.get("cache_key", None) or ""
890 api_base: Final = hidden_params.get("api_base", None) or ""
892 fastapi_response.headers.update(
893 ProxyBaseLLMRequestProcessing.get_custom_headers(
894 user_api_key_dict=user_api_key_dict,
895 model_id=model_id,
896 cache_key=cache_key,
897 api_base=api_base,
898 version=version,
899 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
900 )
901 )
902 return response
903 except Exception as e:
904 await proxy_logging_obj.post_call_failure_hook(
905 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
906 )
907 verbose_proxy_logger.exception("litellm.proxy.proxy_server.create_file(): Exception occured - %s", e)
908 if isinstance(e, ProxyException): 908 ↛ 910line 908 didn't jump to line 910 because the condition on line 908 was always true
909 raise e
910 if isinstance(e, HTTPException):
911 raise ProxyException(
912 message=getattr(e, "message", str(e.detail)),
913 type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
914 param=openai_error_param(e),
915 code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
916 )
917 else:
918 error_msg: Final = f"{e}"
919 raise ProxyException(
920 message=getattr(e, "message", error_msg),
921 type=openai_error_type(e, error_status_code(e, 500)),
922 param=openai_error_param(e),
923 code=error_status_code(e, 500),
924 )
925 finally:
926 for spool in spools:
927 spool.close()
930@router.get(
931 "/{provider}/v1/files/{file_id:path}/content",
932 dependencies=[Depends(user_api_key_auth)],
933 tags=["files"],
934)
935@router.get(
936 "/v1/files/{file_id:path}/content",
937 dependencies=[Depends(user_api_key_auth)],
938 tags=["files"],
939)
940@router.get(
941 "/files/{file_id:path}/content",
942 dependencies=[Depends(user_api_key_auth)],
943 tags=["files"],
944)
945async def get_file_content(
946 request: Request,
947 fastapi_response: Response,
948 file_id: str,
949 provider: str | None = None,
950 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
951):
952 """
953 Returns information about a specific file. that can be used across - Assistants API, Batch API
954 This is the equivalent of GET https://api.openai.com/v1/files/{file_id}/content
956 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/retrieve-contents
958 Example Curl
959 ```
960 curl http://localhost:4000/v1/files/file-abc123/content \
961 -H "Authorization: Bearer sk-1234"
963 ```
964 """
965 from litellm.proxy.proxy_server import (
966 general_settings,
967 llm_router,
968 proxy_config,
969 proxy_logging_obj,
970 version,
971 )
973 data: dict = {"file_id": file_id}
974 try:
975 await validate_managed_id_requirement(
976 resource_id=file_id,
977 resource_kind="file",
978 user_api_key_dict=user_api_key_dict,
979 managed_files_obj=proxy_logging_obj.get_proxy_hook("managed_files"),
980 )
982 # Include original request and headers in the data
983 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
984 (
985 data,
986 litellm_logging_obj,
987 ) = await base_llm_response_processor.common_processing_pre_call_logic(
988 request=request,
989 general_settings=general_settings,
990 user_api_key_dict=user_api_key_dict,
991 version=version,
992 proxy_logging_obj=proxy_logging_obj,
993 proxy_config=proxy_config,
994 route_type="afile_content",
995 )
997 custom_llm_provider: Final = (
998 provider
999 or get_custom_llm_provider_from_request_headers(request=request)
1000 or get_custom_llm_provider_from_request_query(request=request)
1001 or await get_custom_llm_provider_from_request_body(request=request)
1002 or "openai"
1003 )
1005 ## check if file_id is a litellm managed file
1006 is_base64_unified_file_id: Final = _is_base64_encoded_unified_file_id(file_id)
1007 if is_base64_unified_file_id: 1007 ↛ 1008line 1007 didn't jump to line 1008 because the condition on line 1007 was never true
1008 managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
1009 if managed_files_obj is None:
1010 raise ProxyException(
1011 message="Managed files hook not found",
1012 type=ProxyErrorTypes.internal_server_error.value,
1013 param=None,
1014 code=500,
1015 )
1016 if llm_router is None:
1017 raise ProxyException(
1018 message="LLM Router not found",
1019 type=ProxyErrorTypes.internal_server_error.value,
1020 param=None,
1021 code=500,
1022 )
1023 if not isinstance(managed_files_obj, BaseFileEndpoints):
1024 raise ProxyException(
1025 message="Managed files hook is not a BaseFileEndpoints",
1026 type=ProxyErrorTypes.internal_server_error.value,
1027 param=None,
1028 code=500,
1029 )
1031 # Check if file is stored in a storage backend (check DB)
1032 if hasattr(managed_files_obj, "prisma_client") and getattr(managed_files_obj, "prisma_client", None):
1033 prisma_client: Final[PrismaClient] = getattr(managed_files_obj, "prisma_client")
1034 db_file: Final = await ManagedFileRepository(prisma_client).table.find_first(
1035 where={"unified_file_id": file_id}
1036 )
1037 if db_file and db_file.storage_backend and db_file.storage_url:
1038 # File is stored in a storage backend, download it
1039 from litellm.llms.base_llm.files.storage_backend_factory import (
1040 get_storage_backend,
1041 )
1043 storage_backend_name: Final = db_file.storage_backend
1044 storage_url: Final = db_file.storage_url
1046 try:
1047 # Get storage backend (uses same env vars as callback)
1048 storage_backend: Final = get_storage_backend(storage_backend_name, prisma_client=prisma_client)
1049 file_content: Final = await storage_backend.download_file(storage_url)
1051 # Return file content
1052 from fastapi.responses import Response as FastAPIResponse
1054 return FastAPIResponse(
1055 content=file_content,
1056 media_type="application/octet-stream",
1057 )
1058 except ValueError as e:
1059 raise ProxyException(
1060 message=f"Storage backend error: {e}",
1061 type="invalid_request_error",
1062 param="file_id",
1063 code=400,
1064 )
1066 model: Final = cast(str | None, data.get("model"))
1067 if model:
1068 add_internal_model_credentials(data=data, llm_router=llm_router, model_id=model)
1069 response = await llm_router.afile_content(
1070 **{
1071 "model": model,
1072 "file_id": file_id,
1073 **data,
1074 }
1075 )
1077 else:
1078 response = await managed_files_obj.afile_content(
1079 **{
1080 "file_id": file_id,
1081 "litellm_parent_otel_span": user_api_key_dict.parent_otel_span,
1082 "llm_router": llm_router,
1083 **data,
1084 }
1085 )
1086 else:
1087 # A raw cloud-storage URI (s3://, gs://) supplied here would skip the
1088 # managed-file owner/team check that only runs for unified ids, letting
1089 # a caller read another tenant's object by its key. Such objects are only
1090 # reachable through their managed unified id.
1091 if is_managed_cloud_storage_uri(file_id): 1091 ↛ 1092line 1091 didn't jump to line 1092 because the condition on line 1091 was never true
1092 raise HTTPException(
1093 status_code=400,
1094 detail="Raw cloud storage file ids cannot be retrieved directly. Use the LiteLLM managed file id returned when the file was created.",
1095 )
1096 # Check for model-based credential routing
1097 (
1098 should_route,
1099 model_used,
1100 original_file_id,
1101 credentials,
1102 ) = await handle_model_based_routing(
1103 file_id=file_id,
1104 request=request,
1105 llm_router=llm_router,
1106 data=data,
1107 user_api_key_dict=user_api_key_dict,
1108 check_file_id_encoding=True,
1109 )
1111 if not should_route: 1111 ↛ 1119line 1111 didn't jump to line 1119 because the condition on line 1111 was always true
1112 apply_team_provider_credentials(
1113 data=data,
1114 llm_router=llm_router,
1115 user_api_key_dict=user_api_key_dict,
1116 custom_llm_provider=custom_llm_provider,
1117 )
1119 from litellm.proxy.openai_files_endpoints.file_content_streaming_handler import (
1120 FileContentStreamingHandler,
1121 )
1123 (
1124 resolved_custom_llm_provider,
1125 resolved_file_id,
1126 resolved_streaming_data,
1127 ) = FileContentStreamingHandler.resolve_streaming_request_params(
1128 custom_llm_provider=custom_llm_provider,
1129 file_id=file_id,
1130 data=data,
1131 should_route=should_route,
1132 original_file_id=original_file_id,
1133 credentials=credentials,
1134 )
1136 if FileContentStreamingHandler.should_stream_file_content(
1137 custom_llm_provider=resolved_custom_llm_provider,
1138 ):
1139 verbose_proxy_logger.debug(
1140 "Using streaming file content helper for custom_llm_provider=%s, original_file_id=%s, file_id=%s, model_used=%s",
1141 resolved_custom_llm_provider,
1142 original_file_id,
1143 resolved_file_id,
1144 model_used,
1145 )
1146 return await FileContentStreamingHandler.get_streaming_file_content_response(
1147 custom_llm_provider=resolved_custom_llm_provider,
1148 file_id=resolved_file_id,
1149 data=resolved_streaming_data,
1150 proxy_logging_obj=proxy_logging_obj,
1151 user_api_key_dict=user_api_key_dict,
1152 version=version,
1153 )
1155 if should_route and credentials is not None: 1155 ↛ 1157line 1155 didn't jump to line 1157 because the condition on line 1155 was never true
1156 # Use model-based routing with credentials from config
1157 prepare_data_with_credentials(
1158 data=data,
1159 credentials=credentials,
1160 file_id=original_file_id, # Use decoded file ID if from encoded ID
1161 include_internal_credentials=True,
1162 )
1163 response = await litellm.afile_content(
1164 custom_llm_provider=credentials["custom_llm_provider"],
1165 **data,
1166 )
1168 verbose_proxy_logger.debug(
1169 f"Retrieved file content using model: {model_used}"
1170 + (f", file_id: {file_id} -> {original_file_id}" if original_file_id else "")
1171 )
1172 else:
1173 # Fallback to default behavior (uses env variables or provider-based routing)
1174 response = await litellm.afile_content(
1175 **{
1176 "custom_llm_provider": custom_llm_provider,
1177 "file_id": file_id,
1178 **data,
1179 }
1180 )
1182 ### ALERTING ###
1183 asyncio.create_task(
1184 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
1185 )
1187 ### RESPONSE HEADERS ###
1188 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
1189 model_id: Final = hidden_params.get("model_id", None) or ""
1190 cache_key: Final = hidden_params.get("cache_key", None) or ""
1191 api_base: Final = hidden_params.get("api_base", None) or ""
1193 fastapi_response.headers.update(
1194 ProxyBaseLLMRequestProcessing.get_custom_headers(
1195 user_api_key_dict=user_api_key_dict,
1196 model_id=model_id,
1197 cache_key=cache_key,
1198 api_base=api_base,
1199 version=version,
1200 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
1201 )
1202 )
1203 httpx_response: Final[httpx.Response | None] = getattr(response, "response", None)
1204 if httpx_response is None:
1205 raise ValueError(f"Invalid response - response.response is None - got {response}")
1207 return Response(
1208 content=httpx_response.content,
1209 status_code=httpx_response.status_code,
1210 headers=httpx_response.headers,
1211 )
1213 except Exception as e:
1214 await proxy_logging_obj.post_call_failure_hook(
1215 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
1216 )
1217 verbose_proxy_logger.exception("litellm.proxy.proxy_server.retrieve_file_content(): Exception occured - %s", e)
1218 verbose_proxy_logger.debug(traceback.format_exc())
1219 if isinstance(e, HTTPException): 1219 ↛ 1220line 1219 didn't jump to line 1220 because the condition on line 1219 was never true
1220 raise ProxyException(
1221 message=getattr(e, "message", str(e.detail)),
1222 type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
1223 param=openai_error_param(e),
1224 code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
1225 )
1226 else:
1227 error_msg: Final = f"{e}"
1228 raise ProxyException(
1229 message=getattr(e, "message", error_msg),
1230 type=openai_error_type(e, error_status_code(e, 500)),
1231 param=openai_error_param(e),
1232 code=error_status_code(e, 500),
1233 )
1236@router.get(
1237 "/{provider}/v1/files/{file_id:path}",
1238 dependencies=[Depends(user_api_key_auth)],
1239 tags=["files"],
1240)
1241@router.get(
1242 "/v1/files/{file_id:path}",
1243 dependencies=[Depends(user_api_key_auth)],
1244 tags=["files"],
1245)
1246@router.get(
1247 "/files/{file_id:path}",
1248 dependencies=[Depends(user_api_key_auth)],
1249 tags=["files"],
1250)
1251async def get_file(
1252 request: Request,
1253 fastapi_response: Response,
1254 file_id: str,
1255 provider: str | None = None,
1256 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
1257):
1258 """
1259 Returns information about a specific file. that can be used across - Assistants API, Batch API
1260 This is the equivalent of GET https://api.openai.com/v1/files/{file_id}
1262 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/retrieve
1264 Example Curl
1265 ```
1266 curl http://localhost:4000/v1/files/file-abc123 \
1267 -H "Authorization: Bearer sk-1234"
1269 ```
1270 """
1271 from litellm.proxy.proxy_server import (
1272 general_settings,
1273 proxy_config,
1274 proxy_logging_obj,
1275 version,
1276 )
1278 data: dict = {"file_id": file_id}
1279 try:
1280 await validate_managed_id_requirement(
1281 resource_id=file_id,
1282 resource_kind="file",
1283 user_api_key_dict=user_api_key_dict,
1284 managed_files_obj=proxy_logging_obj.get_proxy_hook("managed_files"),
1285 )
1287 custom_llm_provider: Final = (
1288 provider
1289 or get_custom_llm_provider_from_request_headers(request=request)
1290 or get_custom_llm_provider_from_request_query(request=request)
1291 or await get_custom_llm_provider_from_request_body(request=request)
1292 or "openai"
1293 )
1295 # Include original request and headers in the data
1296 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
1297 (
1298 data,
1299 litellm_logging_obj,
1300 ) = await base_llm_response_processor.common_processing_pre_call_logic(
1301 request=request,
1302 general_settings=general_settings,
1303 user_api_key_dict=user_api_key_dict,
1304 version=version,
1305 proxy_logging_obj=proxy_logging_obj,
1306 proxy_config=proxy_config,
1307 route_type="afile_retrieve",
1308 )
1310 ## Check for model-based credential routing
1311 from litellm.proxy.proxy_server import llm_router
1313 (
1314 should_route,
1315 model_used,
1316 original_file_id,
1317 credentials,
1318 ) = await handle_model_based_routing(
1319 file_id=file_id,
1320 request=request,
1321 llm_router=llm_router,
1322 data=data,
1323 user_api_key_dict=user_api_key_dict,
1324 check_file_id_encoding=True,
1325 )
1327 if should_route and credentials is not None: 1327 ↛ 1329line 1327 didn't jump to line 1329 because the condition on line 1327 was never true
1328 # Use model-based routing with credentials from config
1329 prepare_data_with_credentials(
1330 data=data,
1331 credentials=credentials,
1332 file_id=original_file_id,
1333 include_internal_credentials=True,
1334 )
1336 response = await litellm.afile_retrieve(
1337 custom_llm_provider=credentials["custom_llm_provider"],
1338 **data,
1339 )
1341 # Keep the encoded ID in response if it was originally encoded
1342 if original_file_id and response and hasattr(response, "id") and response.id:
1343 response.id = file_id
1345 verbose_proxy_logger.debug(
1346 f"Retrieved file using model: {model_used}"
1347 + (f", original_id: {original_file_id}" if original_file_id else "")
1348 )
1350 ## EXISTING: check if file_id is a litellm managed file
1351 elif _is_base64_encoded_unified_file_id(file_id): 1351 ↛ 1352line 1351 didn't jump to line 1352 because the condition on line 1351 was never true
1352 managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
1353 if managed_files_obj is None:
1354 raise ProxyException(
1355 message="Managed files hook not found",
1356 type=ProxyErrorTypes.internal_server_error.value,
1357 param=None,
1358 code=500,
1359 )
1360 if not isinstance(managed_files_obj, BaseFileEndpoints):
1361 raise ProxyException(
1362 message="Managed files hook is not a BaseFileEndpoints",
1363 type=ProxyErrorTypes.internal_server_error.value,
1364 param=None,
1365 code=500,
1366 )
1367 response = await managed_files_obj.afile_retrieve(
1368 file_id=file_id,
1369 litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
1370 llm_router=llm_router,
1371 )
1372 else:
1373 # Remove file_id from data to avoid "multiple values for keyword argument" error
1374 # data was initialized with {"file_id": file_id}
1375 data.pop("file_id", None)
1376 apply_team_provider_credentials(
1377 data=data,
1378 llm_router=llm_router,
1379 user_api_key_dict=user_api_key_dict,
1380 custom_llm_provider=custom_llm_provider,
1381 )
1382 response = await litellm.afile_retrieve(
1383 custom_llm_provider=custom_llm_provider,
1384 file_id=file_id,
1385 **data,
1386 )
1388 ### ALERTING ###
1389 asyncio.create_task(
1390 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
1391 )
1393 ### RESPONSE HEADERS ###
1394 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
1395 model_id: Final = hidden_params.get("model_id", None) or ""
1396 cache_key: Final = hidden_params.get("cache_key", None) or ""
1397 api_base: Final = hidden_params.get("api_base", None) or ""
1399 fastapi_response.headers.update(
1400 ProxyBaseLLMRequestProcessing.get_custom_headers(
1401 user_api_key_dict=user_api_key_dict,
1402 model_id=model_id,
1403 cache_key=cache_key,
1404 api_base=api_base,
1405 version=version,
1406 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
1407 )
1408 )
1409 return response
1411 except Exception as e:
1412 await proxy_logging_obj.post_call_failure_hook(
1413 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
1414 )
1415 verbose_proxy_logger.error("litellm.proxy.proxy_server.retrieve_file(): Exception occured - %s", e)
1416 verbose_proxy_logger.debug(traceback.format_exc())
1417 if isinstance(e, HTTPException): 1417 ↛ 1418line 1417 didn't jump to line 1418 because the condition on line 1417 was never true
1418 raise ProxyException(
1419 message=getattr(e, "message", str(e.detail)),
1420 type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
1421 param=openai_error_param(e),
1422 code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
1423 )
1424 else:
1425 error_msg: Final = f"{e}"
1426 raise ProxyException(
1427 message=getattr(e, "message", error_msg),
1428 type=openai_error_type(e, error_status_code(e, 500)),
1429 param=openai_error_param(e),
1430 code=error_status_code(e, 500),
1431 )
1434@router.delete(
1435 "/{provider}/v1/files/{file_id:path}",
1436 dependencies=[Depends(user_api_key_auth)],
1437 tags=["files"],
1438)
1439@router.delete(
1440 "/v1/files/{file_id:path}",
1441 dependencies=[Depends(user_api_key_auth)],
1442 tags=["files"],
1443)
1444@router.delete(
1445 "/files/{file_id:path}",
1446 dependencies=[Depends(user_api_key_auth)],
1447 tags=["files"],
1448)
1449async def delete_file(
1450 request: Request,
1451 fastapi_response: Response,
1452 file_id: str,
1453 provider: str | None = None,
1454 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
1455):
1456 """
1457 Deletes a specified file. that can be used across - Assistants API, Batch API
1458 This is the equivalent of DELETE https://api.openai.com/v1/files/{file_id}
1460 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/delete
1462 Example Curl
1463 ```
1464 curl http://localhost:4000/v1/files/file-abc123 \
1465 -X DELETE \
1466 -H "Authorization: Bearer $OPENAI_API_KEY"
1468 ```
1469 """
1470 from litellm.proxy.proxy_server import (
1471 add_litellm_data_to_request,
1472 general_settings,
1473 llm_router,
1474 proxy_config,
1475 proxy_logging_obj,
1476 version,
1477 )
1479 data: dict = {"file_id": file_id}
1480 try:
1481 await validate_managed_id_requirement(
1482 resource_id=file_id,
1483 resource_kind="file",
1484 user_api_key_dict=user_api_key_dict,
1485 managed_files_obj=proxy_logging_obj.get_proxy_hook("managed_files"),
1486 )
1487 if is_managed_cloud_storage_uri(file_id) and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: 1487 ↛ 1488line 1487 didn't jump to line 1488 because the condition on line 1487 was never true
1488 raise HTTPException(
1489 status_code=403,
1490 detail="Raw cloud storage file ids can only be deleted by a proxy admin key. Use the LiteLLM managed file id returned when the file was created.",
1491 )
1493 custom_llm_provider: Final = (
1494 provider
1495 or get_custom_llm_provider_from_request_headers(request=request)
1496 or get_custom_llm_provider_from_request_query(request=request)
1497 or await get_custom_llm_provider_from_request_body(request=request)
1498 or "openai"
1499 )
1501 # Call common_processing_pre_call_logic to trigger permission checks
1502 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
1503 (
1504 data,
1505 litellm_logging_obj,
1506 ) = await base_llm_response_processor.common_processing_pre_call_logic(
1507 request=request,
1508 general_settings=general_settings,
1509 user_api_key_dict=user_api_key_dict,
1510 version=version,
1511 proxy_logging_obj=proxy_logging_obj,
1512 proxy_config=proxy_config,
1513 route_type="afile_delete",
1514 )
1516 # Include original request and headers in the data
1517 data = await add_litellm_data_to_request(
1518 data=data,
1519 request=request,
1520 general_settings=general_settings,
1521 user_api_key_dict=user_api_key_dict,
1522 version=version,
1523 proxy_config=proxy_config,
1524 )
1526 # Check for model-based credential routing
1527 (
1528 should_route,
1529 model_used,
1530 original_file_id,
1531 credentials,
1532 ) = await handle_model_based_routing(
1533 file_id=file_id,
1534 request=request,
1535 llm_router=llm_router,
1536 data=data,
1537 user_api_key_dict=user_api_key_dict,
1538 check_file_id_encoding=True,
1539 )
1541 if should_route and credentials is not None: 1541 ↛ 1543line 1541 didn't jump to line 1543 because the condition on line 1541 was never true
1542 # Use model-based routing with credentials from config
1543 prepare_data_with_credentials(
1544 data=data,
1545 credentials=credentials,
1546 file_id=original_file_id,
1547 include_internal_credentials=True,
1548 )
1550 response = await litellm.afile_delete(
1551 custom_llm_provider=credentials["custom_llm_provider"],
1552 **data,
1553 )
1555 verbose_proxy_logger.debug(
1556 f"Deleted file using model: {model_used}"
1557 + (f", original_id: {original_file_id}" if original_file_id else "")
1558 )
1560 ## EXISTING: check if file_id is a litellm managed file
1561 elif _is_base64_encoded_unified_file_id(file_id): 1561 ↛ 1562line 1561 didn't jump to line 1562 because the condition on line 1561 was never true
1562 managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
1563 if managed_files_obj is None:
1564 raise ProxyException(
1565 message="Managed files hook not found",
1566 type=ProxyErrorTypes.internal_server_error.value,
1567 param=None,
1568 code=500,
1569 )
1570 if llm_router is None:
1571 raise ProxyException(
1572 message="LLM Router not found",
1573 type=ProxyErrorTypes.internal_server_error.value,
1574 param=None,
1575 code=500,
1576 )
1577 if not isinstance(managed_files_obj, BaseFileEndpoints):
1578 raise ProxyException(
1579 message="Managed files hook is not a BaseFileEndpoints",
1580 type=ProxyErrorTypes.internal_server_error.value,
1581 param=None,
1582 code=500,
1583 )
1585 # Remove file_id from data to avoid duplicate keyword argument
1586 data_without_file_id: Final = {k: v for k, v in data.items() if k != "file_id"}
1587 response = await managed_files_obj.afile_delete(
1588 file_id=file_id,
1589 litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
1590 llm_router=llm_router,
1591 **data_without_file_id,
1592 )
1593 else:
1594 data.pop("file_id", None)
1595 apply_team_provider_credentials(
1596 data=data,
1597 llm_router=llm_router,
1598 user_api_key_dict=user_api_key_dict,
1599 custom_llm_provider=custom_llm_provider,
1600 )
1601 response = await litellm.afile_delete(
1602 custom_llm_provider=custom_llm_provider,
1603 file_id=file_id,
1604 **data,
1605 )
1607 ### ALERTING ###
1608 asyncio.create_task(
1609 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
1610 )
1612 ### RESPONSE HEADERS ###
1613 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
1614 model_id: Final = hidden_params.get("model_id", None) or ""
1615 cache_key: Final = hidden_params.get("cache_key", None) or ""
1616 api_base: Final = hidden_params.get("api_base", None) or ""
1618 fastapi_response.headers.update(
1619 ProxyBaseLLMRequestProcessing.get_custom_headers(
1620 user_api_key_dict=user_api_key_dict,
1621 model_id=model_id,
1622 cache_key=cache_key,
1623 api_base=api_base,
1624 version=version,
1625 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
1626 )
1627 )
1628 return response
1630 except Exception as e:
1631 await proxy_logging_obj.post_call_failure_hook(
1632 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
1633 )
1634 verbose_proxy_logger.exception("litellm.proxy.proxy_server.delete_file(): Exception occured - %s", e)
1635 if isinstance(e, HTTPException): 1635 ↛ 1636line 1635 didn't jump to line 1636 because the condition on line 1635 was never true
1636 raise ProxyException(
1637 message=getattr(e, "message", str(e.detail)),
1638 type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
1639 param=openai_error_param(e),
1640 code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
1641 )
1642 else:
1643 error_msg: Final = f"{e}"
1644 raise ProxyException(
1645 message=getattr(e, "message", error_msg),
1646 type=openai_error_type(e, error_status_code(e, 500)),
1647 param=openai_error_param(e),
1648 code=error_status_code(e, 500),
1649 )
1652def _as_file_list_page(response: object) -> object:
1653 if not isinstance(response, list): 1653 ↛ 1655line 1653 didn't jump to line 1655 because the condition on line 1653 was always true
1654 return response
1655 return FileListPage(**build_list_page(_LISTED_FILES_ADAPTER.validate_python(response)))
1658@router.get(
1659 "/{provider}/v1/files",
1660 dependencies=[Depends(user_api_key_auth)],
1661 tags=["files"],
1662)
1663@router.get(
1664 "/v1/files",
1665 dependencies=[Depends(user_api_key_auth)],
1666 tags=["files"],
1667)
1668@router.get(
1669 "/files",
1670 dependencies=[Depends(user_api_key_auth)],
1671 tags=["files"],
1672)
1673async def list_files(
1674 request: Request,
1675 fastapi_response: Response,
1676 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
1677 provider: str | None = None,
1678 target_model_names: str | None = None,
1679 purpose: str | None = None,
1680 limit: int | None = None,
1681 after: str | None = None,
1682):
1683 """
1684 Returns information about a specific file. that can be used across - Assistants API, Batch API
1685 This is the equivalent of GET https://api.openai.com/v1/files/
1687 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/list
1689 Example Curl
1690 ```
1691 curl http://localhost:4000/v1/files\
1692 -H "Authorization: Bearer sk-1234"
1694 ```
1695 """
1696 from litellm.proxy.proxy_server import (
1697 general_settings,
1698 llm_router,
1699 proxy_config,
1700 proxy_logging_obj,
1701 version,
1702 )
1704 data: dict = {}
1705 try:
1706 validate_file_list_limit(limit)
1708 # Include original request and headers in the data
1709 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
1710 (
1711 data,
1712 litellm_logging_obj,
1713 ) = await base_llm_response_processor.common_processing_pre_call_logic(
1714 request=request,
1715 general_settings=general_settings,
1716 user_api_key_dict=user_api_key_dict,
1717 version=version,
1718 proxy_logging_obj=proxy_logging_obj,
1719 proxy_config=proxy_config,
1720 route_type=CallTypes.alist_fine_tuning_jobs.value,
1721 )
1723 response: Any | None = None
1725 # Check for model-based credential routing (no file_id encoding check for list)
1726 should_route, model_used, _, credentials = await handle_model_based_routing(
1727 file_id="", # No file_id for list endpoint
1728 request=request,
1729 llm_router=llm_router,
1730 data=data,
1731 user_api_key_dict=user_api_key_dict,
1732 check_file_id_encoding=False,
1733 )
1735 if should_route and credentials is not None: 1735 ↛ 1737line 1735 didn't jump to line 1737 because the condition on line 1735 was never true
1736 # Use model-based routing with credentials from config
1737 prepare_data_with_credentials(data=data, credentials=credentials, include_internal_credentials=True)
1738 response = await litellm.afile_list(
1739 custom_llm_provider=credentials["custom_llm_provider"],
1740 purpose=purpose,
1741 **data,
1742 )
1744 verbose_proxy_logger.debug("Listed files using model: %s", model_used)
1746 elif target_model_names and isinstance(target_model_names, str):
1747 target_model_names_list: Final = target_model_names.split(",")
1748 if len(target_model_names_list) != 1:
1749 raise HTTPException(
1750 status_code=400,
1751 detail="target_model_names on list files must be a list of one model name. Example: ['gpt-4o']",
1752 )
1753 if llm_router is None: 1753 ↛ 1754line 1753 didn't jump to line 1754 because the condition on line 1753 was never true
1754 raise HTTPException(
1755 status_code=500,
1756 detail="LLM Router not initialized. Ensure models added to proxy.",
1757 )
1758 credentials = await get_authorized_credentials_for_model(
1759 llm_router=llm_router,
1760 model_id=target_model_names_list[0],
1761 user_api_key_dict=user_api_key_dict,
1762 operation_context="file list",
1763 )
1764 prepare_data_with_credentials(data=data, credentials=credentials, include_internal_credentials=True)
1765 response = await litellm.afile_list(
1766 custom_llm_provider=credentials["custom_llm_provider"],
1767 purpose=purpose,
1768 **data,
1769 )
1770 else:
1771 custom_llm_provider: Final = (
1772 provider
1773 or get_custom_llm_provider_from_request_headers(request=request)
1774 or get_custom_llm_provider_from_request_query(request=request)
1775 or await get_custom_llm_provider_from_request_body(request=request)
1776 )
1777 managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
1778 if custom_llm_provider is None and isinstance(managed_files_obj, BaseFileEndpoints):
1779 response = await managed_files_obj.afile_list(
1780 purpose=purpose,
1781 litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
1782 user_api_key_dict=user_api_key_dict,
1783 limit=limit,
1784 after=after,
1785 )
1786 else:
1787 resolved_custom_llm_provider: Final = custom_llm_provider or "openai"
1788 apply_team_provider_credentials(
1789 data=data,
1790 llm_router=llm_router,
1791 user_api_key_dict=user_api_key_dict,
1792 custom_llm_provider=resolved_custom_llm_provider,
1793 )
1795 response = await litellm.afile_list(
1796 custom_llm_provider=resolved_custom_llm_provider,
1797 purpose=purpose,
1798 **data,
1799 )
1801 if response is None: 1801 ↛ 1802line 1801 didn't jump to line 1802 because the condition on line 1801 was never true
1802 raise HTTPException(
1803 status_code=500,
1804 detail="Either 'provider' or 'target_model_names' must be provided e.g. `?target_model_names=gpt-4o`",
1805 )
1806 response = _as_file_list_page(response) # rebind-ok: each dispatch branch above binds response
1808 ## POST CALL HOOKS ###
1809 _response: Final = await proxy_logging_obj.post_call_success_hook(
1810 data=data, user_api_key_dict=user_api_key_dict, response=response
1811 )
1812 if _response is not None and isinstance(_response, OpenAIFileObject): 1812 ↛ 1813line 1812 didn't jump to line 1813 because the condition on line 1812 was never true
1813 response = _response
1815 ### ALERTING ###
1816 asyncio.create_task(
1817 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
1818 )
1820 ### RESPONSE HEADERS ###
1821 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
1822 model_id: Final = hidden_params.get("model_id", None) or ""
1823 cache_key: Final = hidden_params.get("cache_key", None) or ""
1824 api_base: Final = hidden_params.get("api_base", None) or ""
1826 fastapi_response.headers.update(
1827 ProxyBaseLLMRequestProcessing.get_custom_headers(
1828 user_api_key_dict=user_api_key_dict,
1829 model_id=model_id,
1830 cache_key=cache_key,
1831 api_base=api_base,
1832 version=version,
1833 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
1834 )
1835 )
1836 return response
1838 except Exception as e:
1839 await proxy_logging_obj.post_call_failure_hook(
1840 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
1841 )
1842 verbose_proxy_logger.error("litellm.proxy.proxy_server.list_files(): Exception occured - %s", e)
1843 verbose_proxy_logger.debug(traceback.format_exc())
1844 if isinstance(e, ProxyException):
1845 raise
1846 if isinstance(e, HTTPException):
1847 raise ProxyException(
1848 message=getattr(e, "message", str(e.detail)),
1849 type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
1850 param=openai_error_param(e),
1851 code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
1852 )
1853 else:
1854 error_msg: Final = f"{e}"
1855 raise ProxyException(
1856 message=getattr(e, "message", error_msg),
1857 type=openai_error_type(e, error_status_code(e, 500)),
1858 param=openai_error_param(e),
1859 code=error_status_code(e, 500),
1860 )