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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2CRUD ENDPOINTS FOR POLICIES
4Provides REST API endpoints for managing policies and policy attachments.
5"""
7from typing import Final
9from fastapi import APIRouter, Depends, HTTPException
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)
35router: Final = APIRouter()
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 )
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 )
69# ─────────────────────────────────────────────────────────────────────────────
70# Policy CRUD Endpoints
71# ─────────────────────────────────────────────────────────────────────────────
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.
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.
90 Query params:
91 - version_status: Optional. One of "draft", "published", "production".
92 If omitted, all versions are returned.
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 ```
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
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))
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.
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 ```
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
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")
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))
217# ─────────────────────────────────────────────────────────────────────────────
218# Policy Versioning Endpoints (must be before /policies/{policy_id} to avoid path conflicts)
219# ─────────────────────────────────────────────────────────────────────────────
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
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")
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))
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
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")
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))
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
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")
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))
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
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")
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))
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
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")
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))
378# ─────────────────────────────────────────────────────────────────────────────
379# Policy CRUD by ID
380# ─────────────────────────────────────────────────────────────────────────────
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.
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
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")
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))
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.
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
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")
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 )
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))
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.
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 ```
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
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")
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")
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))
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).
536 This endpoint resolves the full inheritance chain and returns the final
537 set of guardrails that would be applied for this policy.
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 ```
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
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")
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")
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 )
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))
588# ─────────────────────────────────────────────────────────────────────────────
589# Pipeline Test Endpoint
590# ─────────────────────────────────────────────────────────────────────────────
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.
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.
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}")
630 data: Final = {
631 "messages": request.test_messages,
632 "model": "test",
633 "metadata": {},
634 }
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))
651# ─────────────────────────────────────────────────────────────────────────────
652# Policy Attachment CRUD Endpoints
653# ─────────────────────────────────────────────────────────────────────────────
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.
666 Config-defined attachments are returned with definition_location "config" and a
667 synthetic attachment_id ("config-<index>").
669 Example Request:
670 ```bash
671 curl -X GET "http://localhost:4000/policies/attachments/list" \\
672 -H "Authorization: Bearer <your_api_key>"
673 ```
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
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))
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.
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 ```
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 ```
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
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")
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 )
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))
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))
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.
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
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")
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))
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.
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 ```
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
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")
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 )
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))