mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(policy_engine): preserve config-defined policies across DB sync and expose them via list APIs
This commit is contained in:
parent
ae242fdd06
commit
4e9df55392
12 changed files with 611 additions and 43 deletions
|
|
@ -20402,6 +20402,16 @@
|
|||
"description": "Who created the attachment.",
|
||||
"title": "Created By"
|
||||
},
|
||||
"definition_location": {
|
||||
"default": "db",
|
||||
"description": "Where this attachment is defined: 'db' (database) or 'config' (config.yaml).",
|
||||
"enum": [
|
||||
"db",
|
||||
"config"
|
||||
],
|
||||
"title": "Definition Location",
|
||||
"type": "string"
|
||||
},
|
||||
"keys": {
|
||||
"description": "Key patterns.",
|
||||
"items": {
|
||||
|
|
@ -20658,6 +20668,16 @@
|
|||
"description": "Who created the policy.",
|
||||
"title": "Created By"
|
||||
},
|
||||
"definition_location": {
|
||||
"default": "db",
|
||||
"description": "Where this policy is defined: 'db' (database) or 'config' (config.yaml).",
|
||||
"enum": [
|
||||
"db",
|
||||
"config"
|
||||
],
|
||||
"title": "Definition Location",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -21129,12 +21149,45 @@
|
|||
"title": "PolicyVersionStatusUpdateRequest",
|
||||
"type": "object"
|
||||
},
|
||||
"UsageChartPoint": {
|
||||
"properties": {
|
||||
"blocked": {
|
||||
"title": "Blocked",
|
||||
"type": "integer"
|
||||
},
|
||||
"date": {
|
||||
"title": "Date",
|
||||
"type": "string"
|
||||
},
|
||||
"passed": {
|
||||
"title": "Passed",
|
||||
"type": "integer"
|
||||
},
|
||||
"score": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Score"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"date",
|
||||
"passed",
|
||||
"blocked"
|
||||
],
|
||||
"title": "UsageChartPoint",
|
||||
"type": "object"
|
||||
},
|
||||
"UsageOverviewResponse": {
|
||||
"properties": {
|
||||
"chart": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/UsageChartPoint"
|
||||
},
|
||||
"title": "Chart",
|
||||
"type": "array"
|
||||
|
|
@ -21243,6 +21296,13 @@
|
|||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"ctx": {
|
||||
"title": "Context",
|
||||
"type": "object"
|
||||
},
|
||||
"input": {
|
||||
"title": "Input"
|
||||
},
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
|
|
@ -21420,7 +21480,7 @@
|
|||
},
|
||||
"/policies/attachments/list": {
|
||||
"get": {
|
||||
"description": "List all policy attachments from the database.\n\nExample Request:\n```bash\ncurl -X GET \"http://localhost:4000/policies/attachments/list\" \\\n -H \"Authorization: Bearer <your_api_key>\"\n```\n\nExample Response:\n```json\n{\n \"attachments\": [\n {\n \"attachment_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"policy_name\": \"global-baseline\",\n \"scope\": \"*\",\n \"teams\": [],\n \"keys\": [],\n \"models\": [],\n \"created_at\": \"2024-01-01T00:00:00Z\",\n \"updated_at\": \"2024-01-01T00:00:00Z\"\n }\n ],\n \"total_count\": 1\n}\n```",
|
||||
"description": "List all policy attachments from the database and config.yaml.\n\nConfig-defined attachments are returned with definition_location \"config\" and a\nsynthetic attachment_id (\"config-<index>\").\n\nExample Request:\n```bash\ncurl -X GET \"http://localhost:4000/policies/attachments/list\" \\\n -H \"Authorization: Bearer <your_api_key>\"\n```\n\nExample Response:\n```json\n{\n \"attachments\": [\n {\n \"attachment_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"policy_name\": \"global-baseline\",\n \"scope\": \"*\",\n \"teams\": [],\n \"keys\": [],\n \"models\": [],\n \"created_at\": \"2024-01-01T00:00:00Z\",\n \"updated_at\": \"2024-01-01T00:00:00Z\"\n }\n ],\n \"total_count\": 1\n}\n```",
|
||||
"operationId": "list_policy_attachments_policies_attachments_list_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
|
@ -21596,7 +21656,7 @@
|
|||
},
|
||||
"/policies/list": {
|
||||
"get": {
|
||||
"description": "List all policies from the database. Optionally filter by version_status.\n\nQuery params:\n- version_status: Optional. One of \"draft\", \"published\", \"production\".\n If omitted, all versions are returned.\n\nExample Request:\n```bash\ncurl -X GET \"http://localhost:4000/policies/list\" \\\n -H \"Authorization: Bearer <your_api_key>\"\ncurl -X GET \"http://localhost:4000/policies/list?version_status=production\" \\\n -H \"Authorization: Bearer <your_api_key>\"\n```\n\nExample Response:\n```json\n{\n \"policies\": [\n {\n \"policy_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"policy_name\": \"global-baseline\",\n \"version_number\": 1,\n \"version_status\": \"production\",\n \"inherit\": null,\n \"description\": \"Base guardrails for all requests\",\n \"guardrails_add\": [\"pii_masking\"],\n \"guardrails_remove\": [],\n \"condition\": null,\n \"created_at\": \"2024-01-01T00:00:00Z\",\n \"updated_at\": \"2024-01-01T00:00:00Z\"\n }\n ],\n \"total_count\": 1\n}\n```",
|
||||
"description": "List all policies from the database and config.yaml. Optionally filter by version_status.\n\nConfig-defined policies are returned with definition_location \"config\" and are treated\nas production versions. On a name conflict with a DB policy, only the DB policy is returned.\n\nQuery params:\n- version_status: Optional. One of \"draft\", \"published\", \"production\".\n If omitted, all versions are returned.\n\nExample Request:\n```bash\ncurl -X GET \"http://localhost:4000/policies/list\" \\\n -H \"Authorization: Bearer <your_api_key>\"\ncurl -X GET \"http://localhost:4000/policies/list?version_status=production\" \\\n -H \"Authorization: Bearer <your_api_key>\"\n```\n\nExample Response:\n```json\n{\n \"policies\": [\n {\n \"policy_id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"policy_name\": \"global-baseline\",\n \"version_number\": 1,\n \"version_status\": \"production\",\n \"inherit\": null,\n \"description\": \"Base guardrails for all requests\",\n \"guardrails_add\": [\"pii_masking\"],\n \"guardrails_remove\": [],\n \"condition\": null,\n \"created_at\": \"2024-01-01T00:00:00Z\",\n \"updated_at\": \"2024-01-01T00:00:00Z\"\n }\n ],\n \"total_count\": 1\n}\n```",
|
||||
"operationId": "list_policies_policies_list_get",
|
||||
"parameters": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class AttachmentRegistry:
|
|||
|
||||
def __init__(self):
|
||||
self._attachments: List[PolicyAttachment] = []
|
||||
self._config_attachments: tuple[PolicyAttachment, ...] = ()
|
||||
self._initialized: bool = False
|
||||
|
||||
def load_attachments(self, attachments_config: List[Dict[str, Any]]) -> None:
|
||||
|
|
@ -62,6 +63,7 @@ class AttachmentRegistry:
|
|||
verbose_proxy_logger.error(f"Error loading attachment: {str(e)}")
|
||||
raise ValueError(f"Invalid attachment: {str(e)}") from e
|
||||
|
||||
self._config_attachments = tuple(self._attachments)
|
||||
self._initialized = True
|
||||
verbose_proxy_logger.info(f"Loaded {len(self._attachments)} policy attachments")
|
||||
|
||||
|
|
@ -173,6 +175,15 @@ class AttachmentRegistry:
|
|||
"""
|
||||
return self._attachments.copy()
|
||||
|
||||
def get_config_attachments(self) -> tuple[PolicyAttachment, ...]:
|
||||
"""
|
||||
Get the attachments loaded from config.yaml.
|
||||
|
||||
Returns:
|
||||
Tuple of config-defined PolicyAttachment objects
|
||||
"""
|
||||
return self._config_attachments
|
||||
|
||||
def get_attachments_for_policy(self, policy_name: str) -> List[PolicyAttachment]:
|
||||
"""
|
||||
Get all attachments for a specific policy.
|
||||
|
|
@ -199,6 +210,7 @@ class AttachmentRegistry:
|
|||
Clear all attachments from the registry.
|
||||
"""
|
||||
self._attachments = []
|
||||
self._config_attachments = ()
|
||||
self._initialized = False
|
||||
|
||||
def add_attachment(self, attachment: PolicyAttachment) -> None:
|
||||
|
|
@ -428,6 +440,7 @@ class AttachmentRegistry:
|
|||
) -> None:
|
||||
"""
|
||||
Sync policy attachments from the database to in-memory registry.
|
||||
Config-loaded attachments are preserved.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
|
|
@ -435,11 +448,8 @@ class AttachmentRegistry:
|
|||
try:
|
||||
attachments = await self.get_all_attachments_from_db(prisma_client)
|
||||
|
||||
# Clear existing attachments and reload from DB
|
||||
self._attachments = []
|
||||
|
||||
for attachment_response in attachments:
|
||||
attachment = PolicyAttachment(
|
||||
db_attachments = [
|
||||
PolicyAttachment(
|
||||
policy=attachment_response.policy_name,
|
||||
scope=attachment_response.scope,
|
||||
teams=(attachment_response.teams if attachment_response.teams else None),
|
||||
|
|
@ -447,10 +457,15 @@ class AttachmentRegistry:
|
|||
models=(attachment_response.models if attachment_response.models else None),
|
||||
tags=attachment_response.tags if attachment_response.tags else None,
|
||||
)
|
||||
self._attachments.append(attachment)
|
||||
for attachment_response in attachments
|
||||
]
|
||||
self._attachments = [*self._config_attachments, *db_attachments]
|
||||
|
||||
self._initialized = True
|
||||
verbose_proxy_logger.info(f"Synced {len(attachments)} attachments from DB to in-memory registry")
|
||||
verbose_proxy_logger.info(
|
||||
f"Synced {len(attachments)} attachments from DB to in-memory registry "
|
||||
f"({len(self._config_attachments)} config-defined attachments preserved)"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error syncing attachments from DB: {e}")
|
||||
raise Exception(f"Error syncing attachments from DB: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
|||
from litellm.types.proxy.policy_engine import (
|
||||
GuardrailPipeline,
|
||||
PipelineTestRequest,
|
||||
Policy,
|
||||
PolicyAttachment,
|
||||
PolicyAttachmentCreateRequest,
|
||||
PolicyAttachmentDBResponse,
|
||||
PolicyAttachmentListResponse,
|
||||
|
|
@ -33,6 +35,35 @@ from litellm.types.proxy.policy_engine import (
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
def _config_policy_to_db_response(policy_name: str, policy: Policy) -> PolicyDBResponse:
|
||||
return PolicyDBResponse(
|
||||
policy_id=policy_name,
|
||||
policy_name=policy_name,
|
||||
version_number=1,
|
||||
version_status="production",
|
||||
inherit=policy.inherit,
|
||||
description=policy.description,
|
||||
guardrails_add=policy.guardrails.get_add(),
|
||||
guardrails_remove=policy.guardrails.get_remove(),
|
||||
condition=policy.condition.model_dump() if policy.condition else None,
|
||||
pipeline=policy.pipeline.model_dump() if policy.pipeline else None,
|
||||
definition_location="config",
|
||||
)
|
||||
|
||||
|
||||
def _config_attachment_to_db_response(index: int, attachment: PolicyAttachment) -> PolicyAttachmentDBResponse:
|
||||
return PolicyAttachmentDBResponse(
|
||||
attachment_id=f"config-{index}",
|
||||
policy_name=attachment.policy,
|
||||
scope=attachment.scope,
|
||||
teams=attachment.teams or [],
|
||||
keys=attachment.keys or [],
|
||||
models=attachment.models or [],
|
||||
tags=attachment.tags or [],
|
||||
definition_location="config",
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Policy CRUD Endpoints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -46,7 +77,10 @@ router = APIRouter()
|
|||
)
|
||||
async def list_policies(version_status: Optional[str] = None):
|
||||
"""
|
||||
List all policies from the database. Optionally filter by version_status.
|
||||
List all policies from the database and config.yaml. Optionally filter by version_status.
|
||||
|
||||
Config-defined policies are returned with definition_location "config" and are treated
|
||||
as production versions. On a name conflict with a DB policy, only the DB policy is returned.
|
||||
|
||||
Query params:
|
||||
- version_status: Optional. One of "draft", "published", "production".
|
||||
|
|
@ -84,11 +118,25 @@ async def list_policies(version_status: Optional[str] = None):
|
|||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
policies = await get_policy_registry().get_all_policies_from_db(prisma_client, version_status=version_status)
|
||||
registry = get_policy_registry()
|
||||
db_policies = (
|
||||
await registry.get_all_policies_from_db(prisma_client, version_status=version_status)
|
||||
if prisma_client is not None
|
||||
else []
|
||||
)
|
||||
db_policy_names = {db_policy.policy_name for db_policy in db_policies}
|
||||
include_config = version_status in (None, "production")
|
||||
config_policies = (
|
||||
[
|
||||
_config_policy_to_db_response(policy_name, policy)
|
||||
for policy_name, policy in registry.list_config_policies().items()
|
||||
if policy_name not in db_policy_names and registry.get_source(policy_name) != "db"
|
||||
]
|
||||
if include_config
|
||||
else []
|
||||
)
|
||||
policies = db_policies + config_policies
|
||||
return PolicyListDBResponse(policies=policies, total_count=len(policies))
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error listing policies: {e}")
|
||||
|
|
@ -606,7 +654,10 @@ async def test_pipeline(
|
|||
)
|
||||
async def list_policy_attachments():
|
||||
"""
|
||||
List all policy attachments from the database.
|
||||
List all policy attachments from the database and config.yaml.
|
||||
|
||||
Config-defined attachments are returned with definition_location "config" and a
|
||||
synthetic attachment_id ("config-<index>").
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
|
|
@ -635,11 +686,14 @@ async def list_policy_attachments():
|
|||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
attachments = await get_attachment_registry().get_all_attachments_from_db(prisma_client)
|
||||
registry = get_attachment_registry()
|
||||
db_attachments = await registry.get_all_attachments_from_db(prisma_client) if prisma_client is not None else []
|
||||
config_attachments = [
|
||||
_config_attachment_to_db_response(index, attachment)
|
||||
for index, attachment in enumerate(registry.get_config_attachments())
|
||||
]
|
||||
attachments = db_attachments + config_attachments
|
||||
return PolicyAttachmentListResponse(attachments=attachments, total_count=len(attachments))
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error listing policy attachments: {e}")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from datetime import datetime, timezone
|
|||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
Optional,
|
||||
Protocol,
|
||||
TypedDict,
|
||||
|
|
@ -162,6 +163,8 @@ class PolicyRegistry:
|
|||
|
||||
def __init__(self):
|
||||
self._policies: dict[str, Policy] = {}
|
||||
self._config_policies: Mapping[str, Policy] = {}
|
||||
self._sources: Mapping[str, Literal["db", "config"]] = {}
|
||||
self._policies_by_id: dict[str, tuple[str, Policy]] = {}
|
||||
self._initialized: bool = False
|
||||
|
||||
|
|
@ -174,6 +177,8 @@ class PolicyRegistry:
|
|||
This is the raw config from the YAML file.
|
||||
"""
|
||||
self._policies = {}
|
||||
self._config_policies = {}
|
||||
self._sources = {}
|
||||
self._policies_by_id = {}
|
||||
|
||||
for policy_name, policy_data in policies_config.items():
|
||||
|
|
@ -185,6 +190,8 @@ class PolicyRegistry:
|
|||
verbose_proxy_logger.error(f"Error loading policy '{policy_name}': {str(e)}")
|
||||
raise ValueError(f"Invalid policy '{policy_name}': {str(e)}") from e
|
||||
|
||||
self._config_policies = dict(self._policies)
|
||||
self._sources = {policy_name: "config" for policy_name in self._policies}
|
||||
self._initialized = True
|
||||
verbose_proxy_logger.info(f"Loaded {len(self._policies)} policies")
|
||||
|
||||
|
|
@ -299,17 +306,35 @@ class PolicyRegistry:
|
|||
Clear all policies from the registry.
|
||||
"""
|
||||
self._policies = {}
|
||||
self._config_policies = {}
|
||||
self._sources = {}
|
||||
self._initialized = False
|
||||
|
||||
def add_policy(self, policy_name: str, policy: Policy) -> None:
|
||||
def get_source(self, policy_name: str) -> Optional[Literal["db", "config"]]:
|
||||
"""
|
||||
Return the provenance of an in-memory policy, or None if unknown.
|
||||
"""
|
||||
return self._sources.get(policy_name)
|
||||
|
||||
def list_config_policies(self) -> Mapping[str, Policy]:
|
||||
"""
|
||||
Return the policies loaded from config.yaml, keyed by policy name.
|
||||
"""
|
||||
return dict(self._config_policies)
|
||||
|
||||
def add_policy(self, policy_name: str, policy: Policy, source: Literal["db", "config"] = "db") -> None:
|
||||
"""
|
||||
Add or update a single policy.
|
||||
|
||||
Args:
|
||||
policy_name: Name of the policy
|
||||
policy: Policy object to add
|
||||
source: Provenance of the policy ("db" or "config")
|
||||
"""
|
||||
self._policies[policy_name] = policy
|
||||
self._sources = {**self._sources, policy_name: source}
|
||||
if source == "config":
|
||||
self._config_policies = {**self._config_policies, policy_name: policy}
|
||||
self._initialized = True
|
||||
verbose_proxy_logger.debug(f"Added/updated policy: {policy_name}")
|
||||
|
||||
|
|
@ -325,6 +350,7 @@ class PolicyRegistry:
|
|||
"""
|
||||
if policy_name in self._policies:
|
||||
del self._policies[policy_name]
|
||||
self._sources = {name: source for name, source in self._sources.items() if name != policy_name}
|
||||
verbose_proxy_logger.debug(f"Removed policy: {policy_name}")
|
||||
return True
|
||||
return False
|
||||
|
|
@ -591,14 +617,14 @@ class PolicyRegistry:
|
|||
"""
|
||||
Sync policies from the database to in-memory registry.
|
||||
- Production versions are loaded into _policies (by policy name) for resolution.
|
||||
- Config-loaded policies are preserved; on a name conflict the DB version wins.
|
||||
- Draft and published versions are loaded into _policies_by_id so request-body
|
||||
policy_<uuid> overrides can be resolved without DB access in the hot path.
|
||||
"""
|
||||
try:
|
||||
self._policies = {}
|
||||
production = await self.get_all_policies_from_db(prisma_client, version_status="production")
|
||||
for policy_response in production:
|
||||
policy = self._parse_policy(
|
||||
db_policies = {
|
||||
policy_response.policy_name: self._parse_policy(
|
||||
policy_response.policy_name,
|
||||
{
|
||||
"inherit": policy_response.inherit,
|
||||
|
|
@ -611,7 +637,16 @@ class PolicyRegistry:
|
|||
"pipeline": policy_response.pipeline,
|
||||
},
|
||||
)
|
||||
self.add_policy(policy_response.policy_name, policy)
|
||||
for policy_response in production
|
||||
}
|
||||
for policy_name in set(db_policies) & set(self._config_policies):
|
||||
verbose_proxy_logger.warning(
|
||||
f"Policy '{policy_name}' is defined in both config.yaml and the DB; the DB version takes precedence"
|
||||
)
|
||||
config_sources: Mapping[str, Literal["db", "config"]] = {name: "config" for name in self._config_policies}
|
||||
db_sources: Mapping[str, Literal["db", "config"]] = {name: "db" for name in db_policies}
|
||||
self._policies = {**self._config_policies, **db_policies}
|
||||
self._sources = {**config_sources, **db_sources}
|
||||
|
||||
self._policies_by_id = {}
|
||||
non_production = await _policy_table(prisma_client).find_many(
|
||||
|
|
@ -637,7 +672,8 @@ class PolicyRegistry:
|
|||
self._initialized = True
|
||||
verbose_proxy_logger.info(
|
||||
f"Synced {len(production)} production policies and {len(non_production)} "
|
||||
"draft/published (by ID) from DB to in-memory registry"
|
||||
"draft/published (by ID) from DB to in-memory registry "
|
||||
f"({len(self._config_policies)} config-defined policies preserved)"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error syncing policies from DB: {e}")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ the final guardrails list.
|
|||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
|
@ -220,6 +220,10 @@ class PolicyDBResponse(BaseModel):
|
|||
updated_at: Optional[datetime] = Field(default=None, description="When the policy was last updated.")
|
||||
created_by: Optional[str] = Field(default=None, description="Who created the policy.")
|
||||
updated_by: Optional[str] = Field(default=None, description="Who last updated the policy.")
|
||||
definition_location: Literal["db", "config"] = Field(
|
||||
default="db",
|
||||
description="Where this policy is defined: 'db' (database) or 'config' (config.yaml).",
|
||||
)
|
||||
|
||||
|
||||
class PolicyListDBResponse(BaseModel):
|
||||
|
|
@ -317,6 +321,10 @@ class PolicyAttachmentDBResponse(BaseModel):
|
|||
updated_at: Optional[datetime] = Field(default=None, description="When the attachment was last updated.")
|
||||
created_by: Optional[str] = Field(default=None, description="Who created the attachment.")
|
||||
updated_by: Optional[str] = Field(default=None, description="Who last updated the attachment.")
|
||||
definition_location: Literal["db", "config"] = Field(
|
||||
default="db",
|
||||
description="Where this attachment is defined: 'db' (database) or 'config' (config.yaml).",
|
||||
)
|
||||
|
||||
|
||||
class PolicyAttachmentListResponse(BaseModel):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ Unit tests for AttachmentRegistry - tests policy attachment matching.
|
|||
Tests the main entry point: get_attached_policies()
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.proxy.policy_engine.attachment_registry import (
|
||||
|
|
@ -389,3 +392,64 @@ class TestAttachmentRegistrySingleton:
|
|||
registry1 = get_attachment_registry()
|
||||
registry2 = get_attachment_registry()
|
||||
assert registry1 is registry2
|
||||
|
||||
|
||||
def _make_db_attachment_row(attachment_id="att-1", policy_name="db-policy", scope=None, teams=None):
|
||||
row = MagicMock()
|
||||
row.attachment_id = attachment_id
|
||||
row.policy_name = policy_name
|
||||
row.scope = scope
|
||||
row.teams = teams or []
|
||||
row.keys = []
|
||||
row.models = []
|
||||
row.tags = []
|
||||
row.created_at = datetime.now(timezone.utc)
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
row.created_by = None
|
||||
row.updated_by = None
|
||||
return row
|
||||
|
||||
|
||||
def _prisma_with_attachment_rows(rows):
|
||||
prisma = MagicMock()
|
||||
prisma.db.litellm_policyattachmenttable.find_many = AsyncMock(return_value=rows)
|
||||
return prisma
|
||||
|
||||
|
||||
class TestConfigAttachmentsPreservedAcrossDbSync:
|
||||
"""Config-defined attachments must survive sync_attachments_from_db (regression for issue #35255)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_with_empty_db_preserves_config_attachments(self):
|
||||
registry = AttachmentRegistry()
|
||||
registry.load_attachments([{"policy": "config-policy", "scope": "*"}])
|
||||
|
||||
await registry.sync_attachments_from_db(_prisma_with_attachment_rows([]))
|
||||
|
||||
context = PolicyMatchContext(team_alias="any-team", key_alias="any-key", model="gpt-5.2")
|
||||
assert registry.get_attached_policies(context) == ["config-policy"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_merges_db_attachments_with_config_attachments(self):
|
||||
registry = AttachmentRegistry()
|
||||
registry.load_attachments([{"policy": "config-policy", "scope": "*"}])
|
||||
db_row = _make_db_attachment_row(policy_name="db-policy", teams=["db-team"])
|
||||
|
||||
await registry.sync_attachments_from_db(_prisma_with_attachment_rows([db_row]))
|
||||
|
||||
assert len(registry.get_all_attachments()) == 2
|
||||
assert len(registry.get_config_attachments()) == 1
|
||||
context = PolicyMatchContext(team_alias="db-team", key_alias="k", model="gpt-5.2")
|
||||
attached = registry.get_attached_policies(context)
|
||||
assert "config-policy" in attached
|
||||
assert "db-policy" in attached
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repeated_syncs_do_not_duplicate_config_attachments(self):
|
||||
registry = AttachmentRegistry()
|
||||
registry.load_attachments([{"policy": "config-policy", "scope": "*"}])
|
||||
|
||||
await registry.sync_attachments_from_db(_prisma_with_attachment_rows([]))
|
||||
await registry.sync_attachments_from_db(_prisma_with_attachment_rows([]))
|
||||
|
||||
assert len(registry.get_all_attachments()) == 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
"""
|
||||
Unit tests for policy_engine/policy_endpoints.py list endpoints.
|
||||
|
||||
Regression tests for issue #35255: config-defined policies and attachments must be
|
||||
returned by the list endpoints (marked definition_location="config"), DB rows must keep
|
||||
their exact shape, and the endpoints must not 500 when no database is connected.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm.proxy.policy_engine.policy_endpoints as policy_endpoints
|
||||
from litellm.proxy.policy_engine.attachment_registry import AttachmentRegistry
|
||||
from litellm.proxy.policy_engine.policy_registry import PolicyRegistry
|
||||
|
||||
|
||||
def _make_policy_row(
|
||||
policy_id="uuid-1",
|
||||
policy_name="db-policy",
|
||||
version_status="production",
|
||||
guardrails_add=None,
|
||||
):
|
||||
row = MagicMock()
|
||||
row.policy_id = policy_id
|
||||
row.policy_name = policy_name
|
||||
row.version_number = 1
|
||||
row.version_status = version_status
|
||||
row.parent_version_id = None
|
||||
row.is_latest = True
|
||||
row.published_at = None
|
||||
row.production_at = None
|
||||
row.inherit = None
|
||||
row.description = "db description"
|
||||
row.guardrails_add = guardrails_add or []
|
||||
row.guardrails_remove = []
|
||||
row.condition = None
|
||||
row.pipeline = None
|
||||
row.created_at = datetime.now(timezone.utc)
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
row.created_by = "admin"
|
||||
row.updated_by = "admin"
|
||||
return row
|
||||
|
||||
|
||||
def _make_attachment_row(attachment_id="att-1", policy_name="db-policy", scope="*"):
|
||||
row = MagicMock()
|
||||
row.attachment_id = attachment_id
|
||||
row.policy_name = policy_name
|
||||
row.scope = scope
|
||||
row.teams = []
|
||||
row.keys = []
|
||||
row.models = []
|
||||
row.tags = []
|
||||
row.created_at = datetime.now(timezone.utc)
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
row.created_by = "admin"
|
||||
row.updated_by = "admin"
|
||||
return row
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def policy_registry(monkeypatch):
|
||||
registry = PolicyRegistry()
|
||||
monkeypatch.setattr(policy_endpoints, "get_policy_registry", lambda: registry)
|
||||
return registry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def attachment_registry(monkeypatch):
|
||||
registry = AttachmentRegistry()
|
||||
monkeypatch.setattr(policy_endpoints, "get_attachment_registry", lambda: registry)
|
||||
return registry
|
||||
|
||||
|
||||
def _set_prisma(monkeypatch, prisma):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", prisma)
|
||||
|
||||
|
||||
class TestListPoliciesIncludesConfig:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_config_policies_without_prisma(self, policy_registry, monkeypatch):
|
||||
_set_prisma(monkeypatch, None)
|
||||
policy_registry.load_policies(
|
||||
{"config-policy": {"description": "from config", "guardrails": {"add": ["tooling"]}}}
|
||||
)
|
||||
|
||||
response = await policy_endpoints.list_policies()
|
||||
|
||||
assert response.total_count == 1
|
||||
entry = response.policies[0]
|
||||
assert entry.policy_name == "config-policy"
|
||||
assert entry.policy_id == "config-policy"
|
||||
assert entry.definition_location == "config"
|
||||
assert entry.version_status == "production"
|
||||
assert entry.guardrails_add == ["tooling"]
|
||||
assert entry.description == "from config"
|
||||
assert entry.created_at is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merges_db_rows_with_config_and_keeps_db_row_shape(self, policy_registry, monkeypatch):
|
||||
row = _make_policy_row(policy_id="uuid-1", policy_name="db-policy", guardrails_add=["db-guard"])
|
||||
prisma = MagicMock()
|
||||
prisma.db.litellm_policytable.find_many = AsyncMock(return_value=[row])
|
||||
_set_prisma(monkeypatch, prisma)
|
||||
policy_registry.load_policies({"config-policy": {"guardrails": {"add": ["tooling"]}}})
|
||||
|
||||
response = await policy_endpoints.list_policies()
|
||||
|
||||
assert response.total_count == 2
|
||||
db_entry = next(p for p in response.policies if p.policy_name == "db-policy")
|
||||
assert db_entry.definition_location == "db"
|
||||
assert db_entry.policy_id == "uuid-1"
|
||||
assert db_entry.guardrails_add == ["db-guard"]
|
||||
assert db_entry.description == "db description"
|
||||
assert db_entry.created_at == row.created_at
|
||||
assert db_entry.created_by == "admin"
|
||||
config_entry = next(p for p in response.policies if p.policy_name == "config-policy")
|
||||
assert config_entry.definition_location == "config"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_policy_shadows_config_policy_with_same_name(self, policy_registry, monkeypatch):
|
||||
row = _make_policy_row(policy_id="uuid-1", policy_name="shared-name", guardrails_add=["db-guard"])
|
||||
prisma = MagicMock()
|
||||
prisma.db.litellm_policytable.find_many = AsyncMock(return_value=[row])
|
||||
_set_prisma(monkeypatch, prisma)
|
||||
policy_registry.load_policies({"shared-name": {"guardrails": {"add": ["config-guard"]}}})
|
||||
|
||||
response = await policy_endpoints.list_policies()
|
||||
|
||||
assert response.total_count == 1
|
||||
assert response.policies[0].definition_location == "db"
|
||||
assert response.policies[0].guardrails_add == ["db-guard"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_version_status_filter_excludes_config_policies(self, policy_registry, monkeypatch):
|
||||
row = _make_policy_row(policy_id="uuid-1", policy_name="db-policy", version_status="draft")
|
||||
prisma = MagicMock()
|
||||
prisma.db.litellm_policytable.find_many = AsyncMock(return_value=[row])
|
||||
_set_prisma(monkeypatch, prisma)
|
||||
policy_registry.load_policies({"config-policy": {"guardrails": {"add": ["tooling"]}}})
|
||||
|
||||
response = await policy_endpoints.list_policies(version_status="draft")
|
||||
|
||||
assert response.total_count == 1
|
||||
assert response.policies[0].policy_name == "db-policy"
|
||||
assert response.policies[0].definition_location == "db"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_production_filter_includes_config_policies(self, policy_registry, monkeypatch):
|
||||
_set_prisma(monkeypatch, None)
|
||||
policy_registry.load_policies({"config-policy": {"guardrails": {"add": ["tooling"]}}})
|
||||
|
||||
response = await policy_endpoints.list_policies(version_status="production")
|
||||
|
||||
assert response.total_count == 1
|
||||
assert response.policies[0].definition_location == "config"
|
||||
|
||||
|
||||
class TestListAttachmentsIncludesConfig:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_config_attachments_without_prisma(self, attachment_registry, monkeypatch):
|
||||
_set_prisma(monkeypatch, None)
|
||||
attachment_registry.load_attachments([{"policy": "config-policy", "scope": "*"}])
|
||||
|
||||
response = await policy_endpoints.list_policy_attachments()
|
||||
|
||||
assert response.total_count == 1
|
||||
entry = response.attachments[0]
|
||||
assert entry.attachment_id == "config-0"
|
||||
assert entry.policy_name == "config-policy"
|
||||
assert entry.scope == "*"
|
||||
assert entry.definition_location == "config"
|
||||
assert entry.created_at is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merges_db_attachments_with_config_and_keeps_db_row_shape(self, attachment_registry, monkeypatch):
|
||||
row = _make_attachment_row(attachment_id="att-1", policy_name="db-policy")
|
||||
prisma = MagicMock()
|
||||
prisma.db.litellm_policyattachmenttable.find_many = AsyncMock(return_value=[row])
|
||||
_set_prisma(monkeypatch, prisma)
|
||||
attachment_registry.load_attachments([{"policy": "config-policy", "scope": "*"}])
|
||||
|
||||
response = await policy_endpoints.list_policy_attachments()
|
||||
|
||||
assert response.total_count == 2
|
||||
db_entry = next(a for a in response.attachments if a.policy_name == "db-policy")
|
||||
assert db_entry.attachment_id == "att-1"
|
||||
assert db_entry.definition_location == "db"
|
||||
assert db_entry.created_at == row.created_at
|
||||
assert db_entry.created_by == "admin"
|
||||
config_entry = next(a for a in response.attachments if a.policy_name == "config-policy")
|
||||
assert config_entry.attachment_id == "config-0"
|
||||
assert config_entry.definition_location == "config"
|
||||
|
|
@ -450,3 +450,82 @@ class TestGetPolicyRegistrySingleton:
|
|||
a = get_policy_registry()
|
||||
b = get_policy_registry()
|
||||
assert a is b
|
||||
|
||||
|
||||
def _prisma_with_policy_rows(production_rows, non_production_rows=None):
|
||||
prisma = MagicMock()
|
||||
prisma.db.litellm_policytable.find_many = AsyncMock(side_effect=[production_rows, non_production_rows or []])
|
||||
return prisma
|
||||
|
||||
|
||||
class TestConfigPoliciesPreservedAcrossDbSync:
|
||||
"""Config-defined policies must survive sync_policies_from_db (regression for issue #35255)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_with_empty_db_preserves_config_policies(self):
|
||||
registry = PolicyRegistry()
|
||||
registry.load_policies({"config-policy": {"description": "from config", "guardrails": {"add": ["tooling"]}}})
|
||||
|
||||
await registry.sync_policies_from_db(_prisma_with_policy_rows([]))
|
||||
|
||||
assert registry.has_policy("config-policy")
|
||||
policy = registry.get_policy("config-policy")
|
||||
assert policy is not None
|
||||
assert policy.guardrails.add == ["tooling"]
|
||||
assert registry.get_source("config-policy") == "config"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_merges_db_policies_with_config_policies(self):
|
||||
registry = PolicyRegistry()
|
||||
registry.load_policies({"config-policy": {"guardrails": {"add": ["tooling"]}}})
|
||||
db_row = _make_row(policy_id="db-1", policy_name="db-policy", guardrails_add=["db-guard"])
|
||||
|
||||
await registry.sync_policies_from_db(_prisma_with_policy_rows([db_row]))
|
||||
|
||||
assert registry.has_policy("config-policy")
|
||||
assert registry.has_policy("db-policy")
|
||||
assert registry.get_source("config-policy") == "config"
|
||||
assert registry.get_source("db-policy") == "db"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_wins_on_policy_name_conflict(self):
|
||||
registry = PolicyRegistry()
|
||||
registry.load_policies({"shared-name": {"guardrails": {"add": ["config-guard"]}}})
|
||||
db_row = _make_row(policy_id="db-1", policy_name="shared-name", guardrails_add=["db-guard"])
|
||||
|
||||
await registry.sync_policies_from_db(_prisma_with_policy_rows([db_row]))
|
||||
|
||||
policy = registry.get_policy("shared-name")
|
||||
assert policy is not None
|
||||
assert policy.guardrails.add == ["db-guard"]
|
||||
assert registry.get_source("shared-name") == "db"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_policy_restored_after_conflicting_db_row_deleted(self):
|
||||
registry = PolicyRegistry()
|
||||
registry.load_policies({"shared-name": {"guardrails": {"add": ["config-guard"]}}})
|
||||
db_row = _make_row(policy_id="db-1", policy_name="shared-name", guardrails_add=["db-guard"])
|
||||
|
||||
await registry.sync_policies_from_db(_prisma_with_policy_rows([db_row]))
|
||||
await registry.sync_policies_from_db(_prisma_with_policy_rows([]))
|
||||
|
||||
policy = registry.get_policy("shared-name")
|
||||
assert policy is not None
|
||||
assert policy.guardrails.add == ["config-guard"]
|
||||
assert registry.get_source("shared-name") == "config"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_policy_resolves_guardrails_after_sync(self):
|
||||
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
|
||||
|
||||
registry = PolicyRegistry()
|
||||
registry.load_policies({"config-policy": {"guardrails": {"add": ["tooling"]}}})
|
||||
|
||||
await registry.sync_policies_from_db(_prisma_with_policy_rows([]))
|
||||
|
||||
resolved = PolicyResolver.resolve_policy_guardrails(
|
||||
policy_name="config-policy",
|
||||
policies=registry.get_all_policies(),
|
||||
context=None,
|
||||
)
|
||||
assert resolved.guardrails == ["tooling"]
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ interface AttachmentRowActionsProps {
|
|||
onDeleteClick: (attachmentId: string) => void;
|
||||
}
|
||||
|
||||
const CONFIG_ATTACHMENT_HINT =
|
||||
"Config attachments are defined in the config file and cannot be deleted from the dashboard.";
|
||||
|
||||
function AttachmentRowActions({ attachment, isAdmin, onDeleteClick }: AttachmentRowActionsProps) {
|
||||
const isConfigAttachment = attachment.definition_location === "config";
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
|
|
@ -65,6 +70,8 @@ function AttachmentRowActions({ attachment, isAdmin, onDeleteClick }: Attachment
|
|||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
data-testid="attachment-action-delete"
|
||||
disabled={isConfigAttachment}
|
||||
title={isConfigAttachment ? CONFIG_ATTACHMENT_HINT : undefined}
|
||||
onClick={() => onDeleteClick(attachment.attachment_id)}
|
||||
>
|
||||
<Trash2 />
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ export interface PolicyRow {
|
|||
versionCount: number;
|
||||
}
|
||||
|
||||
const CONFIG_POLICY_HINT =
|
||||
"Config policies are defined in the config file and cannot be edited or deleted from the dashboard.";
|
||||
|
||||
function GuardrailChips({ guardrails, tone }: { guardrails: string[]; tone: "success" | "error" }) {
|
||||
if (guardrails.length === 0) {
|
||||
return <span className="text-muted-foreground">-</span>;
|
||||
|
|
@ -45,6 +48,8 @@ interface PolicyRowActionsProps {
|
|||
}
|
||||
|
||||
function PolicyRowActions({ policy, onEditClick, onDeleteClick }: PolicyRowActionsProps) {
|
||||
const isConfigPolicy = policy.definition_location === "config";
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
|
|
@ -55,7 +60,12 @@ function PolicyRowActions({ policy, onEditClick, onDeleteClick }: PolicyRowActio
|
|||
<MoreHorizontal className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuItem data-testid="policy-action-edit" onClick={() => onEditClick(policy)}>
|
||||
<DropdownMenuItem
|
||||
data-testid="policy-action-edit"
|
||||
disabled={isConfigPolicy}
|
||||
title={isConfigPolicy ? CONFIG_POLICY_HINT : undefined}
|
||||
onClick={() => onEditClick(policy)}
|
||||
>
|
||||
<Pencil />
|
||||
Edit policy
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -63,6 +73,8 @@ function PolicyRowActions({ policy, onEditClick, onDeleteClick }: PolicyRowActio
|
|||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
data-testid="policy-action-delete"
|
||||
disabled={isConfigPolicy}
|
||||
title={isConfigPolicy ? CONFIG_POLICY_HINT : undefined}
|
||||
onClick={() => onDeleteClick(policy.policy_id, policy.policy_name || "Unnamed Policy")}
|
||||
>
|
||||
<Trash2 />
|
||||
|
|
@ -93,18 +105,23 @@ export const getPolicyTableColumns = ({
|
|||
header: ({ column }) => <DataTableSortHeader column={column} title="Name" />,
|
||||
size: 220,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<IdentityCell
|
||||
title={row.original.policy_name}
|
||||
titleClassName="max-w-60"
|
||||
badge={
|
||||
row.original.versionCount > 1 ? (
|
||||
<StatusBadge tone="neutral" label={`${row.original.versionCount} versions`} />
|
||||
) : undefined
|
||||
}
|
||||
onClick={() => onViewClick(row.original.primaryPolicy.policy_id)}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const isConfigPolicy = row.original.primaryPolicy.definition_location === "config";
|
||||
const versionBadge =
|
||||
row.original.versionCount > 1 ? (
|
||||
<StatusBadge tone="neutral" label={`${row.original.versionCount} versions`} />
|
||||
) : undefined;
|
||||
return (
|
||||
<IdentityCell
|
||||
title={row.original.policy_name}
|
||||
titleClassName="max-w-60"
|
||||
badge={
|
||||
isConfigPolicy ? <StatusBadge tone="neutral" label="Config" tooltip={CONFIG_POLICY_HINT} /> : versionBadge
|
||||
}
|
||||
onClick={isConfigPolicy ? undefined : () => onViewClick(row.original.primaryPolicy.policy_id)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface Policy {
|
|||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
definition_location?: "db" | "config";
|
||||
}
|
||||
|
||||
export interface PolicyCondition {
|
||||
|
|
@ -47,6 +48,7 @@ export interface PolicyAttachment {
|
|||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
definition_location?: "db" | "config";
|
||||
}
|
||||
|
||||
export interface PolicyCreateRequest {
|
||||
|
|
|
|||
35
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
35
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -9379,7 +9379,10 @@ export interface paths {
|
|||
};
|
||||
/**
|
||||
* List Policy Attachments
|
||||
* @description List all policy attachments from the database.
|
||||
* @description List all policy attachments from the database and config.yaml.
|
||||
*
|
||||
* Config-defined attachments are returned with definition_location "config" and a
|
||||
* synthetic attachment_id ("config-<index>").
|
||||
*
|
||||
* Example Request:
|
||||
* ```bash
|
||||
|
|
@ -9487,7 +9490,10 @@ export interface paths {
|
|||
};
|
||||
/**
|
||||
* List Policies
|
||||
* @description List all policies from the database. Optionally filter by version_status.
|
||||
* @description List all policies from the database and config.yaml. Optionally filter by version_status.
|
||||
*
|
||||
* Config-defined policies are returned with definition_location "config" and are treated
|
||||
* as production versions. On a name conflict with a DB policy, only the DB policy is returned.
|
||||
*
|
||||
* Query params:
|
||||
* - version_status: Optional. One of "draft", "published", "production".
|
||||
|
|
@ -29379,6 +29385,13 @@ export interface components {
|
|||
* @description Who created the attachment.
|
||||
*/
|
||||
created_by?: string | null;
|
||||
/**
|
||||
* Definition Location
|
||||
* @description Where this attachment is defined: 'db' (database) or 'config' (config.yaml).
|
||||
* @default db
|
||||
* @enum {string}
|
||||
*/
|
||||
definition_location: "db" | "config";
|
||||
/**
|
||||
* Keys
|
||||
* @description Key patterns.
|
||||
|
|
@ -29510,6 +29523,13 @@ export interface components {
|
|||
* @description Who created the policy.
|
||||
*/
|
||||
created_by?: string | null;
|
||||
/**
|
||||
* Definition Location
|
||||
* @description Where this policy is defined: 'db' (database) or 'config' (config.yaml).
|
||||
* @default db
|
||||
* @enum {string}
|
||||
*/
|
||||
definition_location: "db" | "config";
|
||||
/**
|
||||
* Description
|
||||
* @description Policy description.
|
||||
|
|
@ -33117,6 +33137,17 @@ export interface components {
|
|||
*/
|
||||
model?: string | null;
|
||||
};
|
||||
/** UsageChartPoint */
|
||||
UsageChartPoint: {
|
||||
/** Blocked */
|
||||
blocked: number;
|
||||
/** Date */
|
||||
date: string;
|
||||
/** Passed */
|
||||
passed: number;
|
||||
/** Score */
|
||||
score?: number | null;
|
||||
};
|
||||
/** UsageDetailResponse */
|
||||
UsageDetailResponse: {
|
||||
/** Avglatency */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue