Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/container_endpoints/handler_factory.py: 59%

127 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1""" 

2Factory for generating container proxy endpoints from JSON config. 

3 

4This module reads the endpoints.json config and dynamically creates 

5FastAPI route handlers for ALL container file endpoints. 

6""" 

7 

8import json 

9from collections.abc import Mapping, Sequence 

10from pathlib import Path 

11from types import MappingProxyType 

12from typing import Final 

13 

14from fastapi import APIRouter, Depends, Request, Response 

15from fastapi.responses import ORJSONResponse 

16 

17from litellm.proxy._types import UserAPIKeyAuth 

18from litellm.proxy.auth.user_api_key_auth import user_api_key_auth 

19from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing 

20from litellm.proxy.common_utils.openai_endpoint_utils import ( 

21 get_custom_llm_provider_from_request_headers, 

22 get_custom_llm_provider_from_request_query, 

23) 

24from litellm.proxy.container_endpoints.ownership import ( 

25 assert_user_can_access_container, 

26 get_container_forwarding_params, 

27) 

28 

29 

30def _load_endpoints_config() -> dict: 

31 """Load the endpoints configuration from JSON file.""" 

32 config_path: Final = Path(__file__).parent.parent.parent / "containers" / "endpoints.json" 

33 with open(config_path) as f: 

34 return json.load(f) 

35 

36 

37def get_all_route_types() -> list[str]: 

38 """Get all async route types for registration in route_llm_request.py""" 

39 config: Final = _load_endpoints_config() 

40 return [endpoint["async_name"] for endpoint in config["endpoints"]] 

41 

42 

43def _get_container_provider_config(custom_llm_provider: str): 

44 """Get the container provider config for the given provider.""" 

45 if custom_llm_provider == "openai": 

46 from litellm.llms.openai.containers.transformation import OpenAIContainerConfig 

47 

48 return OpenAIContainerConfig() 

49 elif custom_llm_provider in ("azure", "azure_text"): 

50 from litellm.llms.azure.containers.transformation import AzureContainerConfig 

51 

52 return AzureContainerConfig() 

53 raise ValueError(f"Container API not supported for provider: {custom_llm_provider}") 

54 

55 

56def _create_handler_for_path_params( 

57 path_params: list[str], 

58 route_type: str, 

59 returns_binary: bool = False, 

60 is_multipart: bool = False, 

61 query_param_names: Sequence[str] = (), 

62): 

63 """ 

64 Dynamically create a handler with the correct path parameter signature. 

65 """ 

66 # For binary content endpoints, use a different handler 

67 if returns_binary and path_params == ["container_id", "file_id"]: 

68 

69 async def handler_binary_content( 

70 request: Request, 

71 container_id: str, 

72 file_id: str, 

73 fastapi_response: Response, 

74 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

75 ): 

76 return await _process_binary_request( 

77 request=request, 

78 fastapi_response=fastapi_response, 

79 container_id=container_id, 

80 file_id=file_id, 

81 user_api_key_dict=user_api_key_dict, 

82 ) 

83 

84 return handler_binary_content 

85 

86 # For multipart file upload endpoints 

87 if is_multipart: 

88 

89 async def handler_multipart_upload( 

90 request: Request, 

91 container_id: str, 

92 fastapi_response: Response, 

93 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

94 ): 

95 return await _process_multipart_upload_request( 

96 request=request, 

97 fastapi_response=fastapi_response, 

98 user_api_key_dict=user_api_key_dict, 

99 route_type=route_type, 

100 container_id=container_id, 

101 ) 

102 

103 return handler_multipart_upload 

104 

105 # Create handlers for different path parameter combinations 

106 if path_params == ["container_id"]: 

107 

108 async def handler_container_id( 

109 request: Request, 

110 container_id: str, 

111 fastapi_response: Response, 

112 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

113 ): 

114 return await _process_request( 

115 request=request, 

116 fastapi_response=fastapi_response, 

117 user_api_key_dict=user_api_key_dict, 

118 route_type=route_type, 

119 path_params={"container_id": container_id}, 

120 query_param_names=query_param_names, 

121 ) 

122 

123 return handler_container_id 

124 

125 elif path_params == ["container_id", "file_id"]: 125 ↛ 147line 125 didn't jump to line 147 because the condition on line 125 was always true

126 

127 async def handler_container_file( 

128 request: Request, 

129 container_id: str, 

130 file_id: str, 

131 fastapi_response: Response, 

132 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

133 ): 

134 return await _process_request( 

135 request=request, 

136 fastapi_response=fastapi_response, 

137 user_api_key_dict=user_api_key_dict, 

138 route_type=route_type, 

139 path_params={"container_id": container_id, "file_id": file_id}, 

140 query_param_names=query_param_names, 

141 ) 

142 

143 return handler_container_file 

144 

145 else: 

146 # Fallback for no path params 

147 async def handler_no_params( 

148 request: Request, 

149 fastapi_response: Response, 

150 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

151 ): 

152 return await _process_request( 

153 request=request, 

154 fastapi_response=fastapi_response, 

155 user_api_key_dict=user_api_key_dict, 

156 route_type=route_type, 

157 path_params={}, 

158 query_param_names=query_param_names, 

159 ) 

160 

161 return handler_no_params 

162 

163 

164async def _process_binary_request( 

165 request: Request, 

166 fastapi_response: Response, 

167 container_id: str, 

168 file_id: str, 

169 user_api_key_dict: UserAPIKeyAuth, 

170): 

171 """ 

172 Process binary content requests through the standard proxy/router pipeline. 

173 

174 Validate ownership before forwarding the provider-native container id through 

175 the standard proxy/router pipeline. This handler only adapts the byte 

176 response to FastAPI. 

177 """ 

178 from litellm.proxy.proxy_server import ( 

179 general_settings, 

180 llm_router, 

181 proxy_config, 

182 proxy_logging_obj, 

183 select_data_generator, 

184 user_api_base, 

185 user_max_tokens, 

186 user_model, 

187 user_request_timeout, 

188 user_temperature, 

189 version, 

190 ) 

191 

192 custom_llm_provider: Final = ( 

193 get_custom_llm_provider_from_request_headers(request=request) 

194 or get_custom_llm_provider_from_request_query(request=request) 

195 or "openai" 

196 ) 

197 

198 original_container_id, resolved_provider = await assert_user_can_access_container( 

199 container_id=container_id, 

200 user_api_key_dict=user_api_key_dict, 

201 custom_llm_provider=custom_llm_provider, 

202 ) 

203 data: Final[dict[str, object]] = { 

204 "file_id": file_id, 

205 **( 

206 await get_container_forwarding_params( 

207 container_id=container_id, 

208 original_container_id=original_container_id, 

209 custom_llm_provider=resolved_provider, 

210 ) 

211 ), 

212 } 

213 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

214 

215 try: 

216 content: Final[object] = await processor.base_process_llm_request( 

217 request=request, 

218 fastapi_response=fastapi_response, 

219 user_api_key_dict=user_api_key_dict, 

220 route_type="aretrieve_container_file_content", 

221 proxy_logging_obj=proxy_logging_obj, 

222 llm_router=llm_router, 

223 general_settings=general_settings, 

224 proxy_config=proxy_config, 

225 select_data_generator=select_data_generator, 

226 model=None, 

227 user_model=user_model, 

228 user_temperature=user_temperature, 

229 user_request_timeout=user_request_timeout, 

230 user_max_tokens=user_max_tokens, 

231 user_api_base=user_api_base, 

232 version=version, 

233 ) 

234 

235 # Determine content type based on common file extensions in the file_id 

236 content_type = "application/octet-stream" 

237 file_id_lower: Final = file_id.lower() 

238 if ".png" in file_id_lower or file_id_lower.endswith("png"): 

239 content_type = "image/png" 

240 elif ".jpg" in file_id_lower or ".jpeg" in file_id_lower: 

241 content_type = "image/jpeg" 

242 elif ".gif" in file_id_lower: 

243 content_type = "image/gif" 

244 elif ".csv" in file_id_lower: 

245 content_type = "text/csv" 

246 elif ".json" in file_id_lower: 

247 content_type = "application/json" 

248 elif ".txt" in file_id_lower: 

249 content_type = "text/plain" 

250 elif ".pdf" in file_id_lower: 

251 content_type = "application/pdf" 

252 

253 if not isinstance(content, bytes): 

254 raise TypeError(f"aretrieve_container_file_content expected bytes, got {type(content).__name__}") 

255 

256 return Response( 

257 content=content, 

258 headers=dict(fastapi_response.headers), 

259 media_type=content_type, 

260 ) 

261 

262 except Exception as e: 

263 raise await processor._handle_llm_api_exception( 

264 e=e, 

265 user_api_key_dict=user_api_key_dict, 

266 proxy_logging_obj=proxy_logging_obj, 

267 version=version, 

268 ) 

269 

270 

271async def _process_multipart_upload_request( 

272 request: Request, 

273 fastapi_response: Response, 

274 user_api_key_dict: UserAPIKeyAuth, 

275 route_type: str, 

276 container_id: str, 

277) -> object: 

278 """Process multipart file upload requests.""" 

279 from litellm.proxy.common_utils.http_parsing_utils import ( 

280 convert_upload_files_to_file_data, 

281 get_form_data, 

282 ) 

283 from litellm.proxy.proxy_server import ( 

284 general_settings, 

285 llm_router, 

286 proxy_config, 

287 proxy_logging_obj, 

288 select_data_generator, 

289 user_api_base, 

290 user_max_tokens, 

291 user_model, 

292 user_request_timeout, 

293 user_temperature, 

294 version, 

295 ) 

296 

297 # Parse multipart form data and convert files 

298 form_data: Final = await get_form_data(request) 

299 data: Final = await convert_upload_files_to_file_data(form_data) 

300 

301 if "file" not in data: 301 ↛ 307line 301 didn't jump to line 307 because the condition on line 301 was always true

302 from fastapi import HTTPException 

303 

304 raise HTTPException(status_code=400, detail="Missing required 'file' field") 

305 

306 # convert_upload_files_to_file_data returns list of tuples, extract single file 

307 file_list: Final = data["file"] 

308 if isinstance(file_list, list) and len(file_list) > 0: 

309 data["file"] = file_list[0] 

310 

311 custom_llm_provider: Final = ( 

312 get_custom_llm_provider_from_request_headers(request=request) 

313 or get_custom_llm_provider_from_request_query(request=request) 

314 or "openai" 

315 ) 

316 

317 original_container_id, resolved_provider = await assert_user_can_access_container( 

318 container_id=container_id, 

319 user_api_key_dict=user_api_key_dict, 

320 custom_llm_provider=custom_llm_provider, 

321 ) 

322 

323 data.update( 

324 await get_container_forwarding_params( 

325 container_id=container_id, 

326 original_container_id=original_container_id, 

327 custom_llm_provider=resolved_provider, 

328 ) 

329 ) 

330 

331 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

332 try: 

333 return await processor.base_process_llm_request( 

334 request=request, 

335 fastapi_response=fastapi_response, 

336 user_api_key_dict=user_api_key_dict, 

337 route_type=route_type, 

338 proxy_logging_obj=proxy_logging_obj, 

339 llm_router=llm_router, 

340 general_settings=general_settings, 

341 proxy_config=proxy_config, 

342 select_data_generator=select_data_generator, 

343 model=None, 

344 user_model=user_model, 

345 user_temperature=user_temperature, 

346 user_request_timeout=user_request_timeout, 

347 user_max_tokens=user_max_tokens, 

348 user_api_base=user_api_base, 

349 version=version, 

350 ) 

351 except Exception as e: 

352 raise await processor._handle_llm_api_exception( 

353 e=e, 

354 user_api_key_dict=user_api_key_dict, 

355 proxy_logging_obj=proxy_logging_obj, 

356 version=version, 

357 ) 

358 

359 

360def _declared_query_params(query_params: Mapping[str, str], query_param_names: Sequence[str]) -> Mapping[str, str]: 

361 return MappingProxyType({name: query_params[name] for name in query_param_names if name in query_params}) 

362 

363 

364async def _process_request( 

365 request: Request, 

366 fastapi_response: Response, 

367 user_api_key_dict: UserAPIKeyAuth, 

368 route_type: str, 

369 path_params: dict[str, str], 

370 query_param_names: Sequence[str] = (), 

371) -> object: 

372 """Common request processing logic.""" 

373 from litellm.proxy.proxy_server import ( 

374 general_settings, 

375 llm_router, 

376 proxy_config, 

377 proxy_logging_obj, 

378 select_data_generator, 

379 user_api_base, 

380 user_max_tokens, 

381 user_model, 

382 user_request_timeout, 

383 user_temperature, 

384 version, 

385 ) 

386 

387 query_params: Final = dict(request.query_params) 

388 data: Final[dict[str, object]] = { 

389 "query_params": query_params, 

390 **_declared_query_params(query_params, query_param_names), 

391 **path_params, 

392 } 

393 

394 custom_llm_provider: Final = ( 

395 get_custom_llm_provider_from_request_headers(request=request) 

396 or get_custom_llm_provider_from_request_query(request=request) 

397 or "openai" 

398 ) 

399 

400 # Validate container_id ownership if present in path_params. 

401 if "container_id" in path_params: 401 ↛ 418line 401 didn't jump to line 418 because the condition on line 401 was always true

402 ( 

403 original_container_id, 

404 resolved_provider, 

405 ) = await assert_user_can_access_container( 

406 container_id=path_params["container_id"], 

407 user_api_key_dict=user_api_key_dict, 

408 custom_llm_provider=custom_llm_provider, 

409 ) 

410 data.update( 

411 await get_container_forwarding_params( 

412 container_id=path_params["container_id"], 

413 original_container_id=original_container_id, 

414 custom_llm_provider=resolved_provider, 

415 ) 

416 ) 

417 else: 

418 data["custom_llm_provider"] = custom_llm_provider 

419 

420 processor: Final = ProxyBaseLLMRequestProcessing(data=data) 

421 try: 

422 return await processor.base_process_llm_request( 

423 request=request, 

424 fastapi_response=fastapi_response, 

425 user_api_key_dict=user_api_key_dict, 

426 route_type=route_type, 

427 proxy_logging_obj=proxy_logging_obj, 

428 llm_router=llm_router, 

429 general_settings=general_settings, 

430 proxy_config=proxy_config, 

431 select_data_generator=select_data_generator, 

432 model=None, 

433 user_model=user_model, 

434 user_temperature=user_temperature, 

435 user_request_timeout=user_request_timeout, 

436 user_max_tokens=user_max_tokens, 

437 user_api_base=user_api_base, 

438 version=version, 

439 ) 

440 except Exception as e: 

441 raise await processor._handle_llm_api_exception( 

442 e=e, 

443 user_api_key_dict=user_api_key_dict, 

444 proxy_logging_obj=proxy_logging_obj, 

445 version=version, 

446 ) 

447 

448 

449def register_container_file_endpoints(router: APIRouter) -> None: 

450 """ 

451 Register ALL container file endpoints from JSON config to the router. 

452 

453 This single function registers all endpoints defined in endpoints.json, 

454 eliminating the need for manual endpoint definitions. 

455 """ 

456 config: Final = _load_endpoints_config() 

457 

458 for endpoint_config in config["endpoints"]: 

459 path = endpoint_config["path"] 

460 method = endpoint_config["method"].lower() 

461 path_params = endpoint_config.get("path_params", []) 

462 route_type = endpoint_config["async_name"] 

463 returns_binary = endpoint_config.get("returns_binary", False) 

464 is_multipart = endpoint_config.get("is_multipart", False) 

465 

466 # Create handler with correct signature for path params 

467 handler = _create_handler_for_path_params( 

468 path_params, 

469 route_type, 

470 returns_binary, 

471 is_multipart, 

472 query_param_names=endpoint_config.get("query_params", ()), 

473 ) 

474 

475 # Register routes 

476 route_method = getattr(router, method) 

477 

478 # For binary endpoints, don't use ORJSONResponse 

479 if returns_binary: 

480 # Register both /v1/... and /... paths without JSON response class 

481 route_method( 

482 f"/v1{path}", 

483 dependencies=[Depends(user_api_key_auth)], 

484 tags=["containers"], 

485 )(handler) 

486 

487 route_method( 

488 path, 

489 dependencies=[Depends(user_api_key_auth)], 

490 tags=["containers"], 

491 )(handler) 

492 else: 

493 # Register both /v1/... and /... paths with JSON response 

494 route_method( 

495 f"/v1{path}", 

496 dependencies=[Depends(user_api_key_auth)], 

497 response_class=ORJSONResponse, 

498 tags=["containers"], 

499 )(handler) 

500 

501 route_method( 

502 path, 

503 dependencies=[Depends(user_api_key_auth)], 

504 response_class=ORJSONResponse, 

505 tags=["containers"], 

506 )(handler)