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

1###################################################################### 

2 

3# /v1/files Endpoints 

4 

5# Equivalent of https://platform.openai.com/docs/api-reference/files 

6###################################################################### 

7 

8import asyncio 

9import traceback 

10from collections.abc import Mapping, Sequence 

11from typing import Any, BinaryIO, Final, TypedDict, cast, get_args 

12 

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 

27 

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) 

107 

108router: Final = APIRouter() 

109 

110 

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 

114 

115 

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 ) 

166 

167 

168_MAX_BATCH_FILE_SIZE_MB_ADAPTER: Final = TypeAdapter(int | None) 

169_LISTED_FILES_ADAPTER: Final = TypeAdapter(list[OpenAIFileObject]) 

170 

171 

172class UploadedFileInfo(TypedDict): 

173 filename: ReadOnly[str | None] 

174 content_type: ReadOnly[str | None] 

175 size: ReadOnly[int | None] 

176 

177 

178files_config = None 

179 

180 

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 

185 

186 if not isinstance(config, list): 

187 raise ValueError("invalid files config, expected a list is not a list") 

188 

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) 

194 

195 files_config = config 

196 

197 

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 

210 

211 

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") 

215 

216 

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 ) 

225 

226 

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 ) 

290 

291 

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 

330 

331 

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. 

335 

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 

352 

353 

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. 

357 

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 

364 

365 

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 ) 

376 

377 response: Final = await llm_router.acreate_file(model=router_model, **_create_file_request) 

378 return response 

379 

380 

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. 

396 

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 """ 

404 

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 

429 

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 ) 

439 

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 ) 

449 

450 # Merge credentials into the request 

451 prepare_data_with_credentials( 

452 data=_create_file_request, 

453 credentials=credentials, 

454 ) 

455 

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 ) 

461 

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) 

468 

469 return response 

470 

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) 

526 

527 return response 

528 

529 

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 

561 

562 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/create 

563 

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 ) 

582 

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) 

592 

593 max_file_size_mb: Final = coerce_optional_int_setting(general_settings.get("max_file_size_mb")) 

594 

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 ) 

615 

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 ) 

624 

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 

628 

629 validate_managed_files_requirement(target_model_names=target_model_names_list, model=model_param) 

630 

631 # Prepare the data for forwarding 

632 

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) 

643 

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) 

647 

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) 

652 

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) 

657 

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 ) 

667 

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) 

678 

679 data = {"passthrough": True} if passthrough else {} 

680 

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]") 

690 

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 

694 

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 ) 

703 

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 ) 

712 

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 

724 

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 ) 

733 

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 ) 

744 

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 ) 

750 

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 ) 

760 

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) 

776 

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 ) 

798 

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) 

810 

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) 

819 

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 ) 

842 

843 verbose_proxy_logger.debug("create_file expires_after: %s", expires_after) 

844 

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 ) 

851 

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 ) 

865 

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 ) 

875 

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 

882 

883 if scan_result is not None and scan_result.changes: 

884 response.litellm_batch_guardrail = scan_result.report() 

885 

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 "" 

891 

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() 

928 

929 

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 

955 

956 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/retrieve-contents 

957 

958 Example Curl 

959 ``` 

960 curl http://localhost:4000/v1/files/file-abc123/content \ 

961 -H "Authorization: Bearer sk-1234" 

962 

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 ) 

972 

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 ) 

981 

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 ) 

996 

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 ) 

1004 

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 ) 

1030 

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 ) 

1042 

1043 storage_backend_name: Final = db_file.storage_backend 

1044 storage_url: Final = db_file.storage_url 

1045 

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) 

1050 

1051 # Return file content 

1052 from fastapi.responses import Response as FastAPIResponse 

1053 

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 ) 

1065 

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 ) 

1076 

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 ) 

1110 

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 ) 

1118 

1119 from litellm.proxy.openai_files_endpoints.file_content_streaming_handler import ( 

1120 FileContentStreamingHandler, 

1121 ) 

1122 

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 ) 

1135 

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 ) 

1154 

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 ) 

1167 

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 ) 

1181 

1182 ### ALERTING ### 

1183 asyncio.create_task( 

1184 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success") 

1185 ) 

1186 

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 "" 

1192 

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}") 

1206 

1207 return Response( 

1208 content=httpx_response.content, 

1209 status_code=httpx_response.status_code, 

1210 headers=httpx_response.headers, 

1211 ) 

1212 

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 ) 

1234 

1235 

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} 

1261 

1262 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/retrieve 

1263 

1264 Example Curl 

1265 ``` 

1266 curl http://localhost:4000/v1/files/file-abc123 \ 

1267 -H "Authorization: Bearer sk-1234" 

1268 

1269 ``` 

1270 """ 

1271 from litellm.proxy.proxy_server import ( 

1272 general_settings, 

1273 proxy_config, 

1274 proxy_logging_obj, 

1275 version, 

1276 ) 

1277 

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 ) 

1286 

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 ) 

1294 

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 ) 

1309 

1310 ## Check for model-based credential routing 

1311 from litellm.proxy.proxy_server import llm_router 

1312 

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 ) 

1326 

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 ) 

1335 

1336 response = await litellm.afile_retrieve( 

1337 custom_llm_provider=credentials["custom_llm_provider"], 

1338 **data, 

1339 ) 

1340 

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 

1344 

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 ) 

1349 

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 ) 

1387 

1388 ### ALERTING ### 

1389 asyncio.create_task( 

1390 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success") 

1391 ) 

1392 

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 "" 

1398 

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 

1410 

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 ) 

1432 

1433 

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} 

1459 

1460 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/delete 

1461 

1462 Example Curl 

1463 ``` 

1464 curl http://localhost:4000/v1/files/file-abc123 \ 

1465 -X DELETE \ 

1466 -H "Authorization: Bearer $OPENAI_API_KEY" 

1467 

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 ) 

1478 

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 ) 

1492 

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 ) 

1500 

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 ) 

1515 

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 ) 

1525 

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 ) 

1540 

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 ) 

1549 

1550 response = await litellm.afile_delete( 

1551 custom_llm_provider=credentials["custom_llm_provider"], 

1552 **data, 

1553 ) 

1554 

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 ) 

1559 

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 ) 

1584 

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 ) 

1606 

1607 ### ALERTING ### 

1608 asyncio.create_task( 

1609 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success") 

1610 ) 

1611 

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 "" 

1617 

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 

1629 

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 ) 

1650 

1651 

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))) 

1656 

1657 

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/ 

1686 

1687 Supports Identical Params as: https://platform.openai.com/docs/api-reference/files/list 

1688 

1689 Example Curl 

1690 ``` 

1691 curl http://localhost:4000/v1/files\ 

1692 -H "Authorization: Bearer sk-1234" 

1693 

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 ) 

1703 

1704 data: dict = {} 

1705 try: 

1706 validate_file_list_limit(limit) 

1707 

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 ) 

1722 

1723 response: Any | None = None 

1724 

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 ) 

1734 

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 ) 

1743 

1744 verbose_proxy_logger.debug("Listed files using model: %s", model_used) 

1745 

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 ) 

1794 

1795 response = await litellm.afile_list( 

1796 custom_llm_provider=resolved_custom_llm_provider, 

1797 purpose=purpose, 

1798 **data, 

1799 ) 

1800 

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 

1807 

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 

1814 

1815 ### ALERTING ### 

1816 asyncio.create_task( 

1817 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success") 

1818 ) 

1819 

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 "" 

1825 

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 

1837 

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 )