feat(team): add DELETE /team/{team_id}/callback/{callback_name}

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
milan 2026-07-28 18:06:51 +00:00
parent b930e2fc2b
commit 8fb6689dca
3 changed files with 330 additions and 1 deletions

View file

@ -9,7 +9,7 @@ import copy
import json
import traceback
from datetime import datetime, timezone
from typing import Any, List, Optional
from typing import Annotated, Any, List, Optional
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
@ -268,6 +268,106 @@ async def add_team_callbacks(
)
@router.delete(
"/team/{team_id}/callback/{callback_name}",
tags=["team management"],
dependencies=[Depends(user_api_key_auth)],
)
@management_endpoint_wrapper
async def delete_team_callback(
http_request: Request,
team_id: str,
callback_name: str,
user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
litellm_changed_by: Annotated[
str | None,
Header(
description="The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability"
),
] = None,
):
"""
Delete a single logging callback from a team, leaving the team's other callbacks untouched
Parameters:
- team_id (str, required): The unique identifier for the team
- callback_name (str, required): The name of the callback to remove, as passed to `POST /team/{team_id}/callback` (e.g. "langsmith")
Every entry registered under `callback_name` is removed, regardless of its `callback_type`
Example curl:
```
curl -X DELETE 'http://localhost:4000/team/dbe2f686-a686-4896-864a-4c3924458709/callback/langsmith' \
-H 'Authorization: Bearer sk-1234'
```
Returns 404 if the team has no callback registered under that name
"""
from litellm.proxy._types import CommonProxyErrors
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
_existing_team = await prisma_client.get_data(team_id=team_id, table_name="team", query_type="find_unique")
if _existing_team is None:
raise HTTPException(
status_code=404,
detail={"error": f"Team id = {team_id} does not exist."},
)
await _verify_team_access(
team_obj=LiteLLM_TeamTable(**_existing_team.model_dump()),
user_api_key_dict=user_api_key_dict,
)
team_metadata = _existing_team.metadata
existing_callbacks = team_metadata.get("logging")
if not isinstance(existing_callbacks, list):
existing_callbacks = []
remaining_callbacks = tuple(
callback
for callback in existing_callbacks
if not (isinstance(callback, dict) and callback.get("callback_name") == callback_name)
)
if len(remaining_callbacks) == len(existing_callbacks):
raise HTTPException(
status_code=404,
detail={"error": f"callback_name = {callback_name} is not registered for team_id = {team_id}."},
)
before_metadata = copy.deepcopy(team_metadata)
updated_metadata = encrypt_callback_vars({**team_metadata, "logging": list(remaining_callbacks)})
await TeamRepository(prisma_client).table.update(
where={"team_id": team_id},
data={"metadata": json.dumps(updated_metadata)},
)
await _emit_team_callback_audit_log(
team_id=team_id,
before_metadata=before_metadata,
after_metadata=updated_metadata,
user_api_key_dict=user_api_key_dict,
litellm_changed_by=litellm_changed_by,
)
return {
"status": "success",
"message": f"Callback {callback_name} deleted for team {team_id}",
"data": {
"team_id": team_id,
"remaining_callback_names": [
callback.get("callback_name") for callback in remaining_callbacks if isinstance(callback, dict)
],
},
}
@router.post(
"/team/{team_id}/disable_logging",
tags=["team management"],

View file

@ -21,6 +21,7 @@ from litellm.proxy._types import (
)
from litellm.proxy.management_endpoints.team_callback_endpoints import (
add_team_callbacks,
delete_team_callback,
disable_team_logging,
get_team_callbacks,
)
@ -459,3 +460,163 @@ async def test_add_team_callbacks_writes_encrypted_callback_vars(monkeypatch):
recovered = decrypt_callback_vars(written)["logging"][0]["callback_vars"]
assert recovered["langfuse_secret_key"] == "sk-lf-real-secret"
assert recovered["langfuse_public_key"] == "pk-lf-real-public"
@pytest.mark.asyncio
async def test_delete_team_callback_removes_only_named_callback(monkeypatch):
"""delete_team_callback must drop every entry for one name and leave the rest intact."""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-32-bytes-aaaaaaaaaaaaaa")
mock_prisma = _patch_prisma(
_team_row(
team_id="team-1",
metadata={
"logging": [
{
"callback_name": "langsmith",
"callback_type": "success",
"callback_vars": {"langsmith_api_key": "ls-success"},
},
{
"callback_name": "langsmith",
"callback_type": "failure",
"callback_vars": {"langsmith_api_key": "ls-failure"},
},
{
"callback_name": "langfuse",
"callback_type": "success",
"callback_vars": {"langfuse_secret_key": "sk-keep-me"},
},
]
},
)
)
with (
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma),
patch("litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin"),
patch("litellm.proxy.proxy_server.master_key", None),
):
response = await delete_team_callback(
http_request=MagicMock(spec=Request),
team_id="team-1",
callback_name="langsmith",
user_api_key_dict=_admin_auth(),
litellm_changed_by=None,
)
assert response["data"]["remaining_callback_names"] == ["langfuse"]
written = json.loads(
mock_prisma.db.litellm_teamtable.update.await_args.kwargs["data"]["metadata"]
)
assert [entry["callback_name"] for entry in written["logging"]] == ["langfuse"]
from litellm.proxy.common_utils.callback_utils import decrypt_callback_vars
recovered = decrypt_callback_vars(written)["logging"][0]["callback_vars"]
assert recovered["langfuse_secret_key"] == "sk-keep-me"
@pytest.mark.asyncio
async def test_delete_team_callback_unknown_name_returns_404():
mock_prisma = _patch_prisma(
_team_row(
team_id="team-1",
metadata={
"logging": [
{
"callback_name": "langfuse",
"callback_type": "success",
"callback_vars": {},
}
]
},
)
)
with (
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma),
patch("litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin"),
):
with pytest.raises(HTTPException) as exc:
await delete_team_callback(
http_request=MagicMock(spec=Request),
team_id="team-1",
callback_name="langsmith",
user_api_key_dict=_admin_auth(),
litellm_changed_by=None,
)
assert exc.value.status_code == 404
mock_prisma.db.litellm_teamtable.update.assert_not_called()
@pytest.mark.asyncio
async def test_delete_team_callback_rejects_unauthorized_caller(
patched_prisma, unauthorized_caller
):
with pytest.raises(HTTPException) as exc:
await delete_team_callback(
http_request=Mock(spec=Request),
team_id="team-victim",
callback_name="langsmith",
user_api_key_dict=unauthorized_caller,
)
assert exc.value.status_code == 403
patched_prisma.db.litellm_teamtable.update.assert_not_called()
@pytest.mark.asyncio
async def test_delete_team_callback_audit_log_redacts_secrets(monkeypatch):
monkeypatch.setattr(litellm, "store_audit_logs", True)
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-32-bytes-aaaaaaaaaaaaaa")
mock_prisma = _patch_prisma(
_team_row(
team_id="team-1",
metadata={
"logging": [
{
"callback_name": "langsmith",
"callback_type": "success",
"callback_vars": {"langsmith_api_key": "ls-secret"},
}
]
},
)
)
audit_calls = []
async def capture(request_data):
audit_calls.append(request_data)
with (
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma),
patch("litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin"),
patch("litellm.proxy.proxy_server.master_key", None),
patch(
"litellm.proxy.management_helpers.audit_logs.create_audit_log_for_update",
new=capture,
),
):
await delete_team_callback(
http_request=MagicMock(spec=Request),
team_id="team-1",
callback_name="langsmith",
user_api_key_dict=_admin_auth(),
litellm_changed_by=None,
)
import asyncio
for _ in range(3):
await asyncio.sleep(0)
assert len(audit_calls) == 1
log = audit_calls[0]
assert log.table_name == LitellmTableNames.TEAM_TABLE_NAME
assert log.object_id == "team-1"
before = json.loads(log.before_value)
after = json.loads(log.updated_values)
assert "ls-secret" not in log.before_value
assert "langsmith_api_key" in before["metadata"]["logging"][0]["callback_vars"]
assert after["metadata"]["logging"] == []

View file

@ -14045,6 +14045,39 @@ export interface paths {
patch?: never;
trace?: never;
};
"/team/{team_id}/callback/{callback_name}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/**
* Delete Team Callback
* @description Delete a single logging callback from a team, leaving the team's other callbacks untouched
*
* Parameters:
* - team_id (str, required): The unique identifier for the team
* - callback_name (str, required): The name of the callback to remove, as passed to `POST /team/{team_id}/callback` (e.g. "langsmith")
*
* Every entry registered under `callback_name` is removed, regardless of its `callback_type`
*
* Example curl:
* ```
* curl -X DELETE 'http://localhost:4000/team/dbe2f686-a686-4896-864a-4c3924458709/callback/langsmith' -H 'Authorization: Bearer sk-1234'
* ```
*
* Returns 404 if the team has no callback registered under that name
*/
delete: operations["delete_team_callback_team__team_id__callback__callback_name__delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/team/{team_id}/disable_logging": {
parameters: {
query?: never;
@ -51213,6 +51246,41 @@ export interface operations {
};
};
};
delete_team_callback_team__team_id__callback__callback_name__delete: {
parameters: {
query?: never;
header?: {
/** @description The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability */
"litellm-changed-by"?: string | null;
};
path: {
team_id: string;
callback_name: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
disable_team_logging_team__team_id__disable_logging_post: {
parameters: {
query?: never;