Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/policy_engine/policy_endpoints.py: 73%

250 statements  

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

1""" 

2CRUD ENDPOINTS FOR POLICIES 

3 

4Provides REST API endpoints for managing policies and policy attachments. 

5""" 

6 

7from typing import Final 

8 

9from fastapi import APIRouter, Depends, HTTPException 

10 

11from litellm._logging import verbose_proxy_logger 

12from litellm.proxy._types import UserAPIKeyAuth 

13from litellm.proxy.auth.user_api_key_auth import user_api_key_auth 

14from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry 

15from litellm.proxy.policy_engine.pipeline_executor import PipelineExecutor 

16from litellm.proxy.policy_engine.policy_registry import get_policy_registry 

17from litellm.types.proxy.policy_engine import ( 

18 GuardrailPipeline, 

19 PipelineTestRequest, 

20 Policy, 

21 PolicyAttachment, 

22 PolicyAttachmentCreateRequest, 

23 PolicyAttachmentDBResponse, 

24 PolicyAttachmentListResponse, 

25 PolicyCreateRequest, 

26 PolicyDBResponse, 

27 PolicyListDBResponse, 

28 PolicyUpdateRequest, 

29 PolicyVersionCompareResponse, 

30 PolicyVersionCreateRequest, 

31 PolicyVersionListResponse, 

32 PolicyVersionStatusUpdateRequest, 

33) 

34 

35router: Final = APIRouter() 

36 

37 

38def _config_policy_to_db_response(policy_name: str, policy: Policy) -> PolicyDBResponse: 

39 return PolicyDBResponse( 

40 policy_id=policy_name, 

41 policy_name=policy_name, 

42 version_number=1, 

43 version_status="production", 

44 inherit=policy.inherit, 

45 description=policy.description, 

46 guardrails_add=policy.guardrails.get_add(), 

47 guardrails_remove=policy.guardrails.get_remove(), 

48 condition=policy.condition.model_dump() if policy.condition else None, 

49 pipeline=policy.pipeline.model_dump() if policy.pipeline else None, 

50 definition_location="config", 

51 ) 

52 

53 

54def _config_attachment_to_db_response(index: int, attachment: PolicyAttachment) -> PolicyAttachmentDBResponse: 

55 return PolicyAttachmentDBResponse( 

56 attachment_id=f"config-{index}", 

57 policy_name=attachment.policy, 

58 scope=attachment.scope, 

59 teams=attachment.teams or [], 

60 keys=attachment.keys or [], 

61 models=attachment.models or [], 

62 tags=attachment.tags or [], 

63 priority=attachment.priority, 

64 default=attachment.default, 

65 definition_location="config", 

66 ) 

67 

68 

69# ───────────────────────────────────────────────────────────────────────────── 

70# Policy CRUD Endpoints 

71# ───────────────────────────────────────────────────────────────────────────── 

72 

73 

74@router.get( 

75 "/policies/list", 

76 tags=["Policies"], 

77 dependencies=[Depends(user_api_key_auth)], 

78 response_model=PolicyListDBResponse, 

79) 

80async def list_policies(version_status: str | None = None): 

81 """ 

82 List all policies from the database and config.yaml. Optionally filter by version_status. 

83 

84 Config-defined policies are returned with definition_location "config" and are treated 

85 as production versions. On a name conflict with a production DB policy, only the DB policy 

86 is returned, mirroring runtime resolution where only production DB versions override config. 

87 A draft or published DB version does not hide the config policy, since the config version 

88 is still the one being enforced. 

89 

90 Query params: 

91 - version_status: Optional. One of "draft", "published", "production". 

92 If omitted, all versions are returned. 

93 

94 Example Request: 

95 ```bash 

96 curl -X GET "http://localhost:4000/policies/list" \\ 

97 -H "Authorization: Bearer <your_api_key>" 

98 curl -X GET "http://localhost:4000/policies/list?version_status=production" \\ 

99 -H "Authorization: Bearer <your_api_key>" 

100 ``` 

101 

102 Example Response: 

103 ```json 

104 { 

105 "policies": [ 

106 { 

107 "policy_id": "123e4567-e89b-12d3-a456-426614174000", 

108 "policy_name": "global-baseline", 

109 "version_number": 1, 

110 "version_status": "production", 

111 "inherit": null, 

112 "description": "Base guardrails for all requests", 

113 "guardrails_add": ["pii_masking"], 

114 "guardrails_remove": [], 

115 "condition": null, 

116 "created_at": "2024-01-01T00:00:00Z", 

117 "updated_at": "2024-01-01T00:00:00Z" 

118 } 

119 ], 

120 "total_count": 1 

121 } 

122 ``` 

123 """ 

124 from litellm.proxy.proxy_server import prisma_client 

125 

126 try: 

127 registry: Final = get_policy_registry() 

128 db_policies: Final = ( 

129 await registry.get_all_policies_from_db(prisma_client, version_status=version_status) 

130 if prisma_client is not None 

131 else [] 

132 ) 

133 db_policy_names: Final = { 

134 db_policy.policy_name for db_policy in db_policies if db_policy.version_status == "production" 

135 } 

136 include_config: Final = version_status in (None, "production") 

137 config_policies: Final = ( 

138 [ 

139 _config_policy_to_db_response(policy_name, policy) 

140 for policy_name, policy in registry.list_config_policies().items() 

141 if policy_name not in db_policy_names 

142 ] 

143 if include_config 

144 else [] 

145 ) 

146 policies: Final = db_policies + config_policies 

147 return PolicyListDBResponse(policies=policies, total_count=len(policies)) 

148 except Exception as e: 

149 verbose_proxy_logger.exception("Error listing policies: %s", e) 

150 raise HTTPException(status_code=500, detail=str(e)) 

151 

152 

153@router.post( 

154 "/policies", 

155 tags=["Policies"], 

156 dependencies=[Depends(user_api_key_auth)], 

157 response_model=PolicyDBResponse, 

158) 

159async def create_policy( 

160 request: PolicyCreateRequest, 

161 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

162): 

163 """ 

164 Create a new policy. 

165 

166 Example Request: 

167 ```bash 

168 curl -X POST "http://localhost:4000/policies" \\ 

169 -H "Authorization: Bearer <your_api_key>" \\ 

170 -H "Content-Type: application/json" \\ 

171 -d '{ 

172 "policy_name": "global-baseline", 

173 "description": "Base guardrails for all requests", 

174 "guardrails_add": ["pii_masking", "prompt_injection"], 

175 "guardrails_remove": [] 

176 }' 

177 ``` 

178 

179 Example Response: 

180 ```json 

181 { 

182 "policy_id": "123e4567-e89b-12d3-a456-426614174000", 

183 "policy_name": "global-baseline", 

184 "inherit": null, 

185 "description": "Base guardrails for all requests", 

186 "guardrails_add": ["pii_masking", "prompt_injection"], 

187 "guardrails_remove": [], 

188 "condition": null, 

189 "created_at": "2024-01-01T00:00:00Z", 

190 "updated_at": "2024-01-01T00:00:00Z" 

191 } 

192 ``` 

193 """ 

194 from litellm.proxy.proxy_server import prisma_client 

195 

196 if prisma_client is None: 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true

197 raise HTTPException(status_code=500, detail="Database not connected") 

198 

199 try: 

200 created_by: Final = user_api_key_dict.user_id 

201 result: Final = await get_policy_registry().add_policy_to_db( 

202 policy_request=request, 

203 prisma_client=prisma_client, 

204 created_by=created_by, 

205 ) 

206 return result 

207 except Exception as e: 

208 verbose_proxy_logger.exception("Error creating policy: %s", e) 

209 if "unique constraint" in str(e).lower(): 

210 raise HTTPException( 

211 status_code=400, 

212 detail=f"Policy with name '{request.policy_name}' already exists", 

213 ) 

214 raise HTTPException(status_code=500, detail=str(e)) 

215 

216 

217# ───────────────────────────────────────────────────────────────────────────── 

218# Policy Versioning Endpoints (must be before /policies/{policy_id} to avoid path conflicts) 

219# ───────────────────────────────────────────────────────────────────────────── 

220 

221 

222@router.get( 

223 "/policies/name/{policy_name}/versions", 

224 tags=["Policies"], 

225 dependencies=[Depends(user_api_key_auth)], 

226 response_model=PolicyVersionListResponse, 

227) 

228async def list_policy_versions(policy_name: str): 

229 """ 

230 List all versions of a policy by name, ordered by version_number descending. 

231 """ 

232 from litellm.proxy.proxy_server import prisma_client 

233 

234 if prisma_client is None: 234 ↛ 235line 234 didn't jump to line 235 because the condition on line 234 was never true

235 raise HTTPException(status_code=500, detail="Database not connected") 

236 

237 try: 

238 return await get_policy_registry().get_versions_by_policy_name( 

239 policy_name=policy_name, 

240 prisma_client=prisma_client, 

241 ) 

242 except Exception as e: 

243 verbose_proxy_logger.exception("Error listing policy versions: %s", e) 

244 raise HTTPException(status_code=500, detail=str(e)) 

245 

246 

247@router.post( 

248 "/policies/name/{policy_name}/versions", 

249 tags=["Policies"], 

250 dependencies=[Depends(user_api_key_auth)], 

251 response_model=PolicyDBResponse, 

252) 

253async def create_policy_version( 

254 policy_name: str, 

255 request: PolicyVersionCreateRequest, 

256 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

257): 

258 """ 

259 Create a new draft version of a policy. Copies all fields from the source. 

260 Source is current production if source_policy_id is not provided. 

261 """ 

262 from litellm.proxy.proxy_server import prisma_client 

263 

264 if prisma_client is None: 264 ↛ 265line 264 didn't jump to line 265 because the condition on line 264 was never true

265 raise HTTPException(status_code=500, detail="Database not connected") 

266 

267 try: 

268 created_by: Final = user_api_key_dict.user_id 

269 return await get_policy_registry().create_new_version( 

270 policy_name=policy_name, 

271 prisma_client=prisma_client, 

272 source_policy_id=request.source_policy_id, 

273 created_by=created_by, 

274 ) 

275 except Exception as e: 

276 verbose_proxy_logger.exception("Error creating policy version: %s", e) 

277 if "not found" in str(e).lower() or "no production" in str(e).lower(): 277 ↛ 279line 277 didn't jump to line 279 because the condition on line 277 was always true

278 raise HTTPException(status_code=404, detail=str(e)) 

279 raise HTTPException(status_code=500, detail=str(e)) 

280 

281 

282@router.put( 

283 "/policies/{policy_id}/status", 

284 tags=["Policies"], 

285 dependencies=[Depends(user_api_key_auth)], 

286 response_model=PolicyDBResponse, 

287) 

288async def update_policy_version_status( 

289 policy_id: str, 

290 request: PolicyVersionStatusUpdateRequest, 

291 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

292): 

293 """ 

294 Update a policy version's status. Valid transitions: 

295 - draft -> published 

296 - published -> production (demotes current production to published) 

297 - production -> published (demotes, policy becomes inactive) 

298 """ 

299 from litellm.proxy.proxy_server import prisma_client 

300 

301 if prisma_client is None: 301 ↛ 302line 301 didn't jump to line 302 because the condition on line 301 was never true

302 raise HTTPException(status_code=500, detail="Database not connected") 

303 

304 try: 

305 updated_by: Final = user_api_key_dict.user_id 

306 return await get_policy_registry().update_version_status( 

307 policy_id=policy_id, 

308 new_status=request.version_status, 

309 prisma_client=prisma_client, 

310 updated_by=updated_by, 

311 ) 

312 except HTTPException: 

313 raise 

314 except Exception as e: 

315 verbose_proxy_logger.exception("Error updating version status: %s", e) 

316 if "invalid status" in str(e).lower() or "only draft" in str(e).lower() or "cannot promote" in str(e).lower(): 

317 raise HTTPException(status_code=400, detail=str(e)) 

318 if "not found" in str(e).lower(): 318 ↛ 320line 318 didn't jump to line 320 because the condition on line 318 was always true

319 raise HTTPException(status_code=404, detail=str(e)) 

320 raise HTTPException(status_code=500, detail=str(e)) 

321 

322 

323@router.get( 

324 "/policies/compare", 

325 tags=["Policies"], 

326 dependencies=[Depends(user_api_key_auth)], 

327 response_model=PolicyVersionCompareResponse, 

328) 

329async def compare_policy_versions( 

330 version_a: str, 

331 version_b: str, 

332): 

333 """ 

334 Compare two policy versions. Query params: version_a, version_b (policy version IDs). 

335 """ 

336 from litellm.proxy.proxy_server import prisma_client 

337 

338 if prisma_client is None: 338 ↛ 339line 338 didn't jump to line 339 because the condition on line 338 was never true

339 raise HTTPException(status_code=500, detail="Database not connected") 

340 

341 try: 

342 return await get_policy_registry().compare_versions( 

343 policy_id_a=version_a, 

344 policy_id_b=version_b, 

345 prisma_client=prisma_client, 

346 ) 

347 except Exception as e: 

348 verbose_proxy_logger.exception("Error comparing versions: %s", e) 

349 if "not found" in str(e).lower(): 

350 raise HTTPException(status_code=404, detail=str(e)) 

351 raise HTTPException(status_code=500, detail=str(e)) 

352 

353 

354@router.delete( 

355 "/policies/name/{policy_name}/all-versions", 

356 tags=["Policies"], 

357 dependencies=[Depends(user_api_key_auth)], 

358) 

359async def delete_all_policy_versions(policy_name: str): 

360 """ 

361 Delete all versions of a policy. Also removes from in-memory registry. 

362 """ 

363 from litellm.proxy.proxy_server import prisma_client 

364 

365 if prisma_client is None: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true

366 raise HTTPException(status_code=500, detail="Database not connected") 

367 

368 try: 

369 return await get_policy_registry().delete_all_versions( 

370 policy_name=policy_name, 

371 prisma_client=prisma_client, 

372 ) 

373 except Exception as e: 

374 verbose_proxy_logger.exception("Error deleting all versions: %s", e) 

375 raise HTTPException(status_code=500, detail=str(e)) 

376 

377 

378# ───────────────────────────────────────────────────────────────────────────── 

379# Policy CRUD by ID 

380# ───────────────────────────────────────────────────────────────────────────── 

381 

382 

383@router.get( 

384 "/policies/{policy_id}", 

385 tags=["Policies"], 

386 dependencies=[Depends(user_api_key_auth)], 

387 response_model=PolicyDBResponse, 

388) 

389async def get_policy(policy_id: str): 

390 """ 

391 Get a policy by ID. 

392 

393 Example Request: 

394 ```bash 

395 curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\ 

396 -H "Authorization: Bearer <your_api_key>" 

397 ``` 

398 """ 

399 from litellm.proxy.proxy_server import prisma_client 

400 

401 if prisma_client is None: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true

402 raise HTTPException(status_code=500, detail="Database not connected") 

403 

404 try: 

405 result: Final = await get_policy_registry().get_policy_by_id_from_db( 

406 policy_id=policy_id, 

407 prisma_client=prisma_client, 

408 ) 

409 if result is None: 

410 raise HTTPException(status_code=404, detail=f"Policy with ID {policy_id} not found") 

411 return result 

412 except HTTPException: 

413 raise 

414 except Exception as e: 

415 verbose_proxy_logger.exception("Error getting policy: %s", e) 

416 raise HTTPException(status_code=500, detail=str(e)) 

417 

418 

419@router.put( 

420 "/policies/{policy_id}", 

421 tags=["Policies"], 

422 dependencies=[Depends(user_api_key_auth)], 

423 response_model=PolicyDBResponse, 

424) 

425async def update_policy( 

426 policy_id: str, 

427 request: PolicyUpdateRequest, 

428 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

429): 

430 """ 

431 Update an existing policy. 

432 

433 Example Request: 

434 ```bash 

435 curl -X PUT "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\ 

436 -H "Authorization: Bearer <your_api_key>" \\ 

437 -H "Content-Type: application/json" \\ 

438 -d '{ 

439 "description": "Updated description", 

440 "guardrails_add": ["pii_masking", "toxicity_filter"] 

441 }' 

442 ``` 

443 """ 

444 from litellm.proxy.proxy_server import prisma_client 

445 

446 if prisma_client is None: 446 ↛ 447line 446 didn't jump to line 447 because the condition on line 446 was never true

447 raise HTTPException(status_code=500, detail="Database not connected") 

448 

449 try: 

450 # Check if policy exists and is draft (only drafts can be updated) 

451 existing: Final = await get_policy_registry().get_policy_by_id_from_db( 

452 policy_id=policy_id, 

453 prisma_client=prisma_client, 

454 ) 

455 if existing is None: 

456 raise HTTPException(status_code=404, detail=f"Policy with ID {policy_id} not found") 

457 if getattr(existing, "version_status", "production") != "draft": 457 ↛ 463line 457 didn't jump to line 463 because the condition on line 457 was always true

458 raise HTTPException( 

459 status_code=400, 

460 detail="Only draft versions can be updated. Publish or create a new version to change published/production.", 

461 ) 

462 

463 updated_by: Final = user_api_key_dict.user_id 

464 result: Final = await get_policy_registry().update_policy_in_db( 

465 policy_id=policy_id, 

466 policy_request=request, 

467 prisma_client=prisma_client, 

468 updated_by=updated_by, 

469 ) 

470 return result 

471 except HTTPException: 

472 raise 

473 except Exception as e: 

474 verbose_proxy_logger.exception("Error updating policy: %s", e) 

475 raise HTTPException(status_code=500, detail=str(e)) 

476 

477 

478@router.delete( 

479 "/policies/{policy_id}", 

480 tags=["Policies"], 

481 dependencies=[Depends(user_api_key_auth)], 

482) 

483async def delete_policy(policy_id: str): 

484 """ 

485 Delete a policy. 

486 

487 Example Request: 

488 ```bash 

489 curl -X DELETE "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\ 

490 -H "Authorization: Bearer <your_api_key>" 

491 ``` 

492 

493 Example Response: 

494 ```json 

495 { 

496 "message": "Policy 123e4567-e89b-12d3-a456-426614174000 deleted successfully" 

497 } 

498 ``` 

499 """ 

500 from litellm.proxy.proxy_server import prisma_client 

501 

502 if prisma_client is None: 502 ↛ 503line 502 didn't jump to line 503 because the condition on line 502 was never true

503 raise HTTPException(status_code=500, detail="Database not connected") 

504 

505 try: 

506 # Check if policy exists 

507 existing: Final = await get_policy_registry().get_policy_by_id_from_db( 

508 policy_id=policy_id, 

509 prisma_client=prisma_client, 

510 ) 

511 if existing is None: 

512 raise HTTPException(status_code=404, detail=f"Policy with ID {policy_id} not found") 

513 

514 result: Final = await get_policy_registry().delete_policy_from_db( 

515 policy_id=policy_id, 

516 prisma_client=prisma_client, 

517 ) 

518 # Result may include "warning" if production was deleted 

519 return result 

520 except HTTPException: 

521 raise 

522 except Exception as e: 

523 verbose_proxy_logger.exception("Error deleting policy: %s", e) 

524 raise HTTPException(status_code=500, detail=str(e)) 

525 

526 

527@router.get( 

528 "/policies/{policy_id}/resolved-guardrails", 

529 tags=["Policies"], 

530 dependencies=[Depends(user_api_key_auth)], 

531) 

532async def get_resolved_guardrails(policy_id: str): 

533 """ 

534 Get the resolved guardrails for a policy (including inherited guardrails). 

535 

536 This endpoint resolves the full inheritance chain and returns the final 

537 set of guardrails that would be applied for this policy. 

538 

539 Example Request: 

540 ```bash 

541 curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000/resolved-guardrails" \\ 

542 -H "Authorization: Bearer <your_api_key>" 

543 ``` 

544 

545 Example Response: 

546 ```json 

547 { 

548 "policy_id": "123e4567-e89b-12d3-a456-426614174000", 

549 "policy_name": "healthcare-compliance", 

550 "resolved_guardrails": ["pii_masking", "prompt_injection", "toxicity_filter"] 

551 } 

552 ``` 

553 """ 

554 from litellm.proxy.proxy_server import prisma_client 

555 

556 if prisma_client is None: 556 ↛ 557line 556 didn't jump to line 557 because the condition on line 556 was never true

557 raise HTTPException(status_code=500, detail="Database not connected") 

558 

559 try: 

560 # Get the policy 

561 policy: Final = await get_policy_registry().get_policy_by_id_from_db( 

562 policy_id=policy_id, 

563 prisma_client=prisma_client, 

564 ) 

565 if policy is None: 

566 raise HTTPException(status_code=404, detail=f"Policy with ID {policy_id} not found") 

567 

568 # Resolve guardrails 

569 resolved: Final = await get_policy_registry().resolve_guardrails_from_db( 

570 policy_name=policy.policy_name, 

571 prisma_client=prisma_client, 

572 ) 

573 

574 return { 

575 "policy_id": policy.policy_id, 

576 "policy_name": policy.policy_name, 

577 "resolved_guardrails": resolved, 

578 } 

579 except HTTPException: 

580 raise 

581 except ValueError as e: 

582 raise HTTPException(status_code=400, detail=str(e)) 

583 except Exception as e: 

584 verbose_proxy_logger.exception("Error resolving guardrails: %s", e) 

585 raise HTTPException(status_code=500, detail=str(e)) 

586 

587 

588# ───────────────────────────────────────────────────────────────────────────── 

589# Pipeline Test Endpoint 

590# ───────────────────────────────────────────────────────────────────────────── 

591 

592 

593@router.post( 

594 "/policies/test-pipeline", 

595 tags=["Policies"], 

596 dependencies=[Depends(user_api_key_auth)], 

597) 

598async def test_pipeline( 

599 request: PipelineTestRequest, 

600 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

601): 

602 """ 

603 Test a guardrail pipeline with sample messages. 

604 

605 Executes the pipeline steps against the provided test messages and returns 

606 step-by-step results showing which guardrails passed/failed, actions taken, 

607 and timing information. 

608 

609 Example Request: 

610 ```bash 

611 curl -X POST "http://localhost:4000/policies/test-pipeline" \\ 

612 -H "Authorization: Bearer <your_api_key>" \\ 

613 -H "Content-Type: application/json" \\ 

614 -d '{ 

615 "pipeline": { 

616 "mode": "pre_call", 

617 "steps": [ 

618 {"guardrail": "pii-guard", "on_pass": "next", "on_fail": "block"} 

619 ] 

620 }, 

621 "test_messages": [{"role": "user", "content": "My SSN is 123-45-6789"}] 

622 }' 

623 ``` 

624 """ 

625 try: 

626 validated_pipeline: Final = GuardrailPipeline(**request.pipeline) 

627 except Exception as e: 

628 raise HTTPException(status_code=400, detail=f"Invalid pipeline: {e}") 

629 

630 data: Final = { 

631 "messages": request.test_messages, 

632 "model": "test", 

633 "metadata": {}, 

634 } 

635 

636 try: 

637 result: Final = await PipelineExecutor.execute_steps( 

638 steps=validated_pipeline.steps, 

639 mode=validated_pipeline.mode, 

640 data=data, 

641 user_api_key_dict=user_api_key_dict, 

642 call_type="completion", 

643 policy_name="test-pipeline", 

644 ) 

645 return result.model_dump() 

646 except Exception as e: 

647 verbose_proxy_logger.exception("Error testing pipeline: %s", e) 

648 raise HTTPException(status_code=500, detail=str(e)) 

649 

650 

651# ───────────────────────────────────────────────────────────────────────────── 

652# Policy Attachment CRUD Endpoints 

653# ───────────────────────────────────────────────────────────────────────────── 

654 

655 

656@router.get( 

657 "/policies/attachments/list", 

658 tags=["Policies"], 

659 dependencies=[Depends(user_api_key_auth)], 

660 response_model=PolicyAttachmentListResponse, 

661) 

662async def list_policy_attachments(): 

663 """ 

664 List all policy attachments from the database and config.yaml. 

665 

666 Config-defined attachments are returned with definition_location "config" and a 

667 synthetic attachment_id ("config-<index>"). 

668 

669 Example Request: 

670 ```bash 

671 curl -X GET "http://localhost:4000/policies/attachments/list" \\ 

672 -H "Authorization: Bearer <your_api_key>" 

673 ``` 

674 

675 Example Response: 

676 ```json 

677 { 

678 "attachments": [ 

679 { 

680 "attachment_id": "123e4567-e89b-12d3-a456-426614174000", 

681 "policy_name": "global-baseline", 

682 "scope": "*", 

683 "teams": [], 

684 "keys": [], 

685 "models": [], 

686 "created_at": "2024-01-01T00:00:00Z", 

687 "updated_at": "2024-01-01T00:00:00Z" 

688 } 

689 ], 

690 "total_count": 1 

691 } 

692 ``` 

693 """ 

694 from litellm.proxy.proxy_server import prisma_client 

695 

696 try: 

697 registry: Final = get_attachment_registry() 

698 db_attachments = await registry.get_all_attachments_from_db(prisma_client) if prisma_client is not None else [] 

699 config_attachments: Final = [ 

700 _config_attachment_to_db_response(index, attachment) 

701 for index, attachment in enumerate(registry.get_config_attachments()) 

702 ] 

703 attachments: Final = db_attachments + config_attachments 

704 return PolicyAttachmentListResponse(attachments=attachments, total_count=len(attachments)) 

705 except Exception as e: 

706 verbose_proxy_logger.exception("Error listing policy attachments: %s", e) 

707 raise HTTPException(status_code=500, detail=str(e)) 

708 

709 

710@router.post( 

711 "/policies/attachments", 

712 tags=["Policies"], 

713 dependencies=[Depends(user_api_key_auth)], 

714 response_model=PolicyAttachmentDBResponse, 

715) 

716async def create_policy_attachment( 

717 request: PolicyAttachmentCreateRequest, 

718 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

719): 

720 """ 

721 Create a new policy attachment. 

722 

723 Example Request: 

724 ```bash 

725 curl -X POST "http://localhost:4000/policies/attachments" \\ 

726 -H "Authorization: Bearer <your_api_key>" \\ 

727 -H "Content-Type: application/json" \\ 

728 -d '{ 

729 "policy_name": "global-baseline", 

730 "scope": "*" 

731 }' 

732 ``` 

733 

734 Example with team-specific attachment: 

735 ```bash 

736 curl -X POST "http://localhost:4000/policies/attachments" \\ 

737 -H "Authorization: Bearer <your_api_key>" \\ 

738 -H "Content-Type: application/json" \\ 

739 -d '{ 

740 "policy_name": "healthcare-compliance", 

741 "teams": ["healthcare-team", "medical-research"] 

742 }' 

743 ``` 

744 

745 Example Response: 

746 ```json 

747 { 

748 "attachment_id": "123e4567-e89b-12d3-a456-426614174000", 

749 "policy_name": "global-baseline", 

750 "scope": "*", 

751 "teams": [], 

752 "keys": [], 

753 "models": [], 

754 "created_at": "2024-01-01T00:00:00Z", 

755 "updated_at": "2024-01-01T00:00:00Z" 

756 } 

757 ``` 

758 """ 

759 from litellm.proxy.policy_engine.policy_validator import PolicyValidator 

760 from litellm.proxy.proxy_server import llm_router, prisma_client 

761 

762 if prisma_client is None: 762 ↛ 763line 762 didn't jump to line 763 because the condition on line 762 was never true

763 raise HTTPException(status_code=500, detail="Database not connected") 

764 

765 try: 

766 # Verify the policy has a production version (attachments resolve against production) 

767 policies = await get_policy_registry().get_all_policies_from_db(prisma_client, version_status="production") 

768 policy_names: Final = {p.policy_name for p in policies} 

769 if request.policy_name not in policy_names: 

770 raise HTTPException( 

771 status_code=404, 

772 detail=f"Policy '{request.policy_name}' not found. Create the policy first.", 

773 ) 

774 

775 # Reject concrete team/key/model scope entries that don't resolve to a real 

776 # entity. Wildcard patterns are allowed through (they may match zero today). 

777 scope_errors: Final = await PolicyValidator( 

778 prisma_client=prisma_client, llm_router=llm_router 

779 ).find_invalid_scope_entries( 

780 policy_name=request.policy_name, 

781 teams=request.teams, 

782 keys=request.keys, 

783 models=request.models, 

784 ) 

785 if scope_errors: 

786 raise HTTPException(status_code=400, detail=" | ".join(e.message for e in scope_errors)) 

787 

788 created_by: Final = user_api_key_dict.user_id 

789 result: Final = await get_attachment_registry().add_attachment_to_db( 

790 attachment_request=request, 

791 prisma_client=prisma_client, 

792 created_by=created_by, 

793 ) 

794 return result 

795 except HTTPException: 

796 raise 

797 except Exception as e: 

798 verbose_proxy_logger.exception("Error creating policy attachment: %s", e) 

799 raise HTTPException(status_code=500, detail=str(e)) 

800 

801 

802@router.get( 

803 "/policies/attachments/{attachment_id}", 

804 tags=["Policies"], 

805 dependencies=[Depends(user_api_key_auth)], 

806 response_model=PolicyAttachmentDBResponse, 

807) 

808async def get_policy_attachment(attachment_id: str): 

809 """ 

810 Get a policy attachment by ID. 

811 

812 Example Request: 

813 ```bash 

814 curl -X GET "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\ 

815 -H "Authorization: Bearer <your_api_key>" 

816 ``` 

817 """ 

818 from litellm.proxy.proxy_server import prisma_client 

819 

820 if prisma_client is None: 820 ↛ 821line 820 didn't jump to line 821 because the condition on line 820 was never true

821 raise HTTPException(status_code=500, detail="Database not connected") 

822 

823 try: 

824 result: Final = await get_attachment_registry().get_attachment_by_id_from_db( 

825 attachment_id=attachment_id, 

826 prisma_client=prisma_client, 

827 ) 

828 if result is None: 

829 raise HTTPException( 

830 status_code=404, 

831 detail=f"Attachment with ID {attachment_id} not found", 

832 ) 

833 return result 

834 except HTTPException: 

835 raise 

836 except Exception as e: 

837 verbose_proxy_logger.exception("Error getting policy attachment: %s", e) 

838 raise HTTPException(status_code=500, detail=str(e)) 

839 

840 

841@router.delete( 

842 "/policies/attachments/{attachment_id}", 

843 tags=["Policies"], 

844 dependencies=[Depends(user_api_key_auth)], 

845) 

846async def delete_policy_attachment(attachment_id: str): 

847 """ 

848 Delete a policy attachment. 

849 

850 Example Request: 

851 ```bash 

852 curl -X DELETE "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\ 

853 -H "Authorization: Bearer <your_api_key>" 

854 ``` 

855 

856 Example Response: 

857 ```json 

858 { 

859 "message": "Attachment 123e4567-e89b-12d3-a456-426614174000 deleted successfully" 

860 } 

861 ``` 

862 """ 

863 from litellm.proxy.proxy_server import prisma_client 

864 

865 if prisma_client is None: 865 ↛ 866line 865 didn't jump to line 866 because the condition on line 865 was never true

866 raise HTTPException(status_code=500, detail="Database not connected") 

867 

868 try: 

869 # Check if attachment exists 

870 existing: Final = await get_attachment_registry().get_attachment_by_id_from_db( 

871 attachment_id=attachment_id, 

872 prisma_client=prisma_client, 

873 ) 

874 if existing is None: 874 ↛ 875line 874 didn't jump to line 875 because the condition on line 874 was never true

875 raise HTTPException( 

876 status_code=404, 

877 detail=f"Attachment with ID {attachment_id} not found", 

878 ) 

879 

880 result: Final = await get_attachment_registry().delete_attachment_from_db( 

881 attachment_id=attachment_id, 

882 prisma_client=prisma_client, 

883 ) 

884 return result 

885 except HTTPException: 

886 raise 

887 except Exception as e: 

888 verbose_proxy_logger.exception("Error deleting policy attachment: %s", e) 

889 raise HTTPException(status_code=500, detail=str(e))