From 19240b6cfd4a9b44ba21dad59548ddc33f9abfeb Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 12 Aug 2024 16:06:10 -0700 Subject: [PATCH 1/9] feat gcs log user api key metadata --- litellm/integrations/gcs_bucket.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/litellm/integrations/gcs_bucket.py b/litellm/integrations/gcs_bucket.py index 3fb778e242..a16d952861 100644 --- a/litellm/integrations/gcs_bucket.py +++ b/litellm/integrations/gcs_bucket.py @@ -13,7 +13,7 @@ from litellm.litellm_core_utils.logging_utils import ( convert_litellm_response_object_to_dict, ) from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler -from litellm.proxy._types import CommonProxyErrors, SpendLogsPayload +from litellm.proxy._types import CommonProxyErrors, SpendLogsMetadata, SpendLogsPayload class RequestKwargs(TypedDict): @@ -27,6 +27,8 @@ class GCSBucketPayload(TypedDict): response_obj: Optional[Dict] start_time: str end_time: str + response_cost: Optional[float] + spend_log_metadata: str class GCSBucketLogger(CustomLogger): @@ -78,11 +80,12 @@ class GCSBucketLogger(CustomLogger): kwargs, response_obj, start_time_str, end_time_str ) + json_logged_payload = json.dumps(logging_payload) object_name = response_obj["id"] response = await self.async_httpx_client.post( headers=headers, url=f"https://storage.googleapis.com/upload/storage/v1/b/{self.BUCKET_NAME}/o?uploadType=media&name={object_name}", - json=logging_payload, + data=json_logged_payload, ) if response.status_code != 200: @@ -121,6 +124,10 @@ class GCSBucketLogger(CustomLogger): async def get_gcs_payload( self, kwargs, response_obj, start_time, end_time ) -> GCSBucketPayload: + from litellm.proxy.spend_tracking.spend_tracking_utils import ( + get_logging_payload, + ) + request_kwargs = RequestKwargs( model=kwargs.get("model", None), messages=kwargs.get("messages", None), @@ -131,11 +138,21 @@ class GCSBucketLogger(CustomLogger): response_obj=response_obj ) + _spend_log_payload: SpendLogsPayload = get_logging_payload( + kwargs=kwargs, + response_obj=response_obj, + start_time=start_time, + end_time=end_time, + end_user_id=kwargs.get("end_user_id", None), + ) + gcs_payload: GCSBucketPayload = GCSBucketPayload( request_kwargs=request_kwargs, response_obj=response_dict, start_time=start_time, end_time=end_time, + spend_log_metadata=_spend_log_payload["metadata"], + response_cost=kwargs.get("response_cost", None), ) return gcs_payload From 96f3eb99c92030710b7dd4dc5bda05f6f6ffbba3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 12 Aug 2024 16:07:08 -0700 Subject: [PATCH 2/9] test gcs logging payload --- litellm/tests/test_gcs_bucket.py | 59 +++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/litellm/tests/test_gcs_bucket.py b/litellm/tests/test_gcs_bucket.py index c5a6fb76ac..754b499342 100644 --- a/litellm/tests/test_gcs_bucket.py +++ b/litellm/tests/test_gcs_bucket.py @@ -63,7 +63,7 @@ def load_vertex_ai_credentials(): @pytest.mark.asyncio async def test_basic_gcs_logger(): - load_vertex_ai_credentials() + # load_vertex_ai_credentials() gcs_logger = GCSBucketLogger() print("GCSBucketLogger", gcs_logger) @@ -75,6 +75,41 @@ async def test_basic_gcs_logger(): max_tokens=10, user="ishaan-2", mock_response="Hi!", + metadata={ + "tags": ["model-anthropic-claude-v2.1", "app-ishaan-prod"], + "user_api_key": "88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b", + "user_api_key_alias": None, + "user_api_end_user_max_budget": None, + "litellm_api_version": "0.0.0", + "global_max_parallel_requests": None, + "user_api_key_user_id": "116544810872468347480", + "user_api_key_org_id": None, + "user_api_key_team_id": None, + "user_api_key_team_alias": None, + "user_api_key_metadata": {}, + "requester_ip_address": "127.0.0.1", + "spend_logs_metadata": {"hello": "world"}, + "headers": { + "content-type": "application/json", + "user-agent": "PostmanRuntime/7.32.3", + "accept": "*/*", + "postman-token": "92300061-eeaa-423b-a420-0b44896ecdc4", + "host": "localhost:4000", + "accept-encoding": "gzip, deflate, br", + "connection": "keep-alive", + "content-length": "163", + }, + "endpoint": "http://localhost:4000/chat/completions", + "model_group": "gpt-3.5-turbo", + "deployment": "azure/chatgpt-v-2", + "model_info": { + "id": "4bad40a1eb6bebd1682800f16f44b9f06c52a6703444c99c7f9f32e9de3693b4", + "db_model": False, + }, + "api_base": "https://openai-gpt-4-test-v-1.openai.azure.com/", + "caching_groups": None, + "raw_request": "\n\nPOST Request Sent from LiteLLM:\ncurl -X POST \\\nhttps://openai-gpt-4-test-v-1.openai.azure.com//openai/ \\\n-H 'Authorization: *****' \\\n-d '{'model': 'chatgpt-v-2', 'messages': [{'role': 'system', 'content': 'you are a helpful assistant.\\n'}, {'role': 'user', 'content': 'bom dia'}], 'stream': False, 'max_tokens': 10, 'user': '116544810872468347480', 'extra_body': {}}'\n", + }, ) print("response", response) @@ -83,11 +118,14 @@ async def test_basic_gcs_logger(): # Check if object landed on GCS object_from_gcs = await gcs_logger.download_gcs_object(object_name=response.id) + print("object from gcs=", object_from_gcs) # convert object_from_gcs from bytes to DICT - object_from_gcs = json.loads(object_from_gcs) - print("object_from_gcs", object_from_gcs) + parsed_data = json.loads(object_from_gcs) + print("object_from_gcs as dict", parsed_data) - gcs_payload = GCSBucketPayload(**object_from_gcs) + print("type of object_from_gcs", type(parsed_data)) + + gcs_payload = GCSBucketPayload(**parsed_data) print("gcs_payload", gcs_payload) @@ -97,6 +135,19 @@ async def test_basic_gcs_logger(): ] assert gcs_payload["response_obj"]["choices"][0]["message"]["content"] == "Hi!" + assert gcs_payload["response_cost"] > 0.0 + + gcs_payload["spend_log_metadata"] = json.loads(gcs_payload["spend_log_metadata"]) + + assert ( + gcs_payload["spend_log_metadata"]["user_api_key"] + == "88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b" + ) + assert ( + gcs_payload["spend_log_metadata"]["user_api_key_user_id"] + == "116544810872468347480" + ) + # Delete Object from GCS print("deleting object from GCS") await gcs_logger.delete_gcs_object(object_name=response.id) From 49b8dee14d4bffc77322767617d37c306fb62635 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 12 Aug 2024 16:28:12 -0700 Subject: [PATCH 3/9] feat log responses in folders --- litellm/integrations/gcs_bucket.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/gcs_bucket.py b/litellm/integrations/gcs_bucket.py index a16d952861..46f55f8f01 100644 --- a/litellm/integrations/gcs_bucket.py +++ b/litellm/integrations/gcs_bucket.py @@ -81,7 +81,12 @@ class GCSBucketLogger(CustomLogger): ) json_logged_payload = json.dumps(logging_payload) - object_name = response_obj["id"] + + # Get the current date + current_date = datetime.now().strftime("%Y-%m-%d") + + # Modify the object_name to include the date-based folder + object_name = f"{current_date}/{response_obj['id']}" response = await self.async_httpx_client.post( headers=headers, url=f"https://storage.googleapis.com/upload/storage/v1/b/{self.BUCKET_NAME}/o?uploadType=media&name={object_name}", From 3d06abb55a19279865068cab7d7fdf459ad7d4bc Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 12 Aug 2024 16:33:35 -0700 Subject: [PATCH 4/9] tes logging to gcs buckets --- litellm/tests/test_gcs_bucket.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/litellm/tests/test_gcs_bucket.py b/litellm/tests/test_gcs_bucket.py index 754b499342..607599d903 100644 --- a/litellm/tests/test_gcs_bucket.py +++ b/litellm/tests/test_gcs_bucket.py @@ -9,6 +9,7 @@ import json import logging import tempfile import uuid +from datetime import datetime import pytest @@ -116,8 +117,17 @@ async def test_basic_gcs_logger(): await asyncio.sleep(5) + # Get the current date + # Get the current date + current_date = datetime.now().strftime("%Y-%m-%d") + + # Modify the object_name to include the date-based folder + object_name = f"{current_date}%2F{response.id}" + + print("object_name", object_name) + # Check if object landed on GCS - object_from_gcs = await gcs_logger.download_gcs_object(object_name=response.id) + object_from_gcs = await gcs_logger.download_gcs_object(object_name=object_name) print("object from gcs=", object_from_gcs) # convert object_from_gcs from bytes to DICT parsed_data = json.loads(object_from_gcs) @@ -150,4 +160,4 @@ async def test_basic_gcs_logger(): # Delete Object from GCS print("deleting object from GCS") - await gcs_logger.delete_gcs_object(object_name=response.id) + # await gcs_logger.delete_gcs_object(object_name=response.id) From 6589666028548528c5441a3d1db1930cb3a4bfd2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 12 Aug 2024 16:34:27 -0700 Subject: [PATCH 5/9] fix gcs test --- litellm/tests/test_gcs_bucket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_gcs_bucket.py b/litellm/tests/test_gcs_bucket.py index 607599d903..b30978bad5 100644 --- a/litellm/tests/test_gcs_bucket.py +++ b/litellm/tests/test_gcs_bucket.py @@ -160,4 +160,4 @@ async def test_basic_gcs_logger(): # Delete Object from GCS print("deleting object from GCS") - # await gcs_logger.delete_gcs_object(object_name=response.id) + await gcs_logger.delete_gcs_object(object_name=object_name) From ef8fb23334cb3a65554f8ea81ce53812db011f8c Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 12 Aug 2024 16:44:44 -0700 Subject: [PATCH 6/9] fix(cost_calculator.py): fix cost calc --- litellm/cost_calculator.py | 14 +++++++++++--- litellm/tests/test_custom_logger.py | 16 +++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 6eec8d3cd5..a3cb847a4f 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -490,10 +490,18 @@ def completion_cost( isinstance(completion_response, BaseModel) or isinstance(completion_response, dict) ): # tts returns a custom class - if isinstance(completion_response, BaseModel) and not isinstance( - completion_response, litellm.Usage + + usage_obj: Optional[Union[dict, litellm.Usage]] = completion_response.get( + "usage", {} + ) + if isinstance(usage_obj, BaseModel) and not isinstance( + usage_obj, litellm.Usage ): - completion_response = litellm.Usage(**completion_response.model_dump()) + setattr( + completion_response, + "usage", + litellm.Usage(**usage_obj.model_dump()), + ) # get input/output tokens from completion_response prompt_tokens = completion_response.get("usage", {}).get("prompt_tokens", 0) completion_tokens = completion_response.get("usage", {}).get( diff --git a/litellm/tests/test_custom_logger.py b/litellm/tests/test_custom_logger.py index e3407c9e11..465012bffb 100644 --- a/litellm/tests/test_custom_logger.py +++ b/litellm/tests/test_custom_logger.py @@ -1,11 +1,17 @@ ### What this tests #### -import sys, os, time, inspect, asyncio, traceback +import asyncio +import inspect +import os +import sys +import time +import traceback + import pytest sys.path.insert(0, os.path.abspath("../..")) -from litellm import completion, embedding import litellm +from litellm import completion, embedding from litellm.integrations.custom_logger import CustomLogger @@ -201,7 +207,7 @@ def test_async_custom_handler_stream(): print("complete_streaming_response: ", complete_streaming_response) assert response_in_success_handler == complete_streaming_response except Exception as e: - pytest.fail(f"Error occurred: {e}") + pytest.fail(f"Error occurred: {e}\n{traceback.format_exc()}") # test_async_custom_handler_stream() @@ -457,11 +463,11 @@ async def test_cost_tracking_with_caching(): def test_redis_cache_completion_stream(): - from litellm import Cache - # Important Test - This tests if we can add to streaming cache, when custom callbacks are set import random + from litellm import Cache + try: print("\nrunning test_redis_cache_completion_stream") litellm.set_verbose = True From 66c0d32b1d0ee0dd06181df55f1e95f0453f8151 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 12 Aug 2024 17:42:04 -0700 Subject: [PATCH 7/9] fix gcs logging test --- litellm/tests/test_gcs_bucket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_gcs_bucket.py b/litellm/tests/test_gcs_bucket.py index b30978bad5..c21988c73d 100644 --- a/litellm/tests/test_gcs_bucket.py +++ b/litellm/tests/test_gcs_bucket.py @@ -64,7 +64,7 @@ def load_vertex_ai_credentials(): @pytest.mark.asyncio async def test_basic_gcs_logger(): - # load_vertex_ai_credentials() + load_vertex_ai_credentials() gcs_logger = GCSBucketLogger() print("GCSBucketLogger", gcs_logger) From d1d28487f7f4f6cfd5f0a8f5b2ff0b9f3cf434c4 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 12 Aug 2024 18:47:25 -0700 Subject: [PATCH 8/9] refactor(test_users.py): refactor test for user info to use mock endpoints --- .../internal_user_endpoints.py | 11 +++++- litellm/tests/test_proxy_server.py | 38 +++++++++++++++++++ tests/test_users.py | 7 ---- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 8e2358c992..a0e020b11f 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -312,7 +312,7 @@ async def user_info( try: if prisma_client is None: raise Exception( - f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" + "Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" ) ## GET USER ROW ## if user_id is not None: @@ -365,7 +365,14 @@ async def user_info( getattr(caller_user_info, "user_role", None) == LitellmUserRoles.PROXY_ADMIN ): - teams_2 = await prisma_client.db.litellm_teamtable.find_many() + from litellm.proxy.management_endpoints.team_endpoints import list_team + + teams_2 = await list_team( + http_request=Request( + scope={"type": "http", "path": "/user/info"}, + ), + user_api_key_dict=user_api_key_dict, + ) else: teams_2 = await prisma_client.get_data( team_id_list=caller_user_info.teams, diff --git a/litellm/tests/test_proxy_server.py b/litellm/tests/test_proxy_server.py index dee20a273c..757eef6d62 100644 --- a/litellm/tests/test_proxy_server.py +++ b/litellm/tests/test_proxy_server.py @@ -928,3 +928,41 @@ async def test_create_team_member_add(prisma_client, new_member_method): mock_client.call_args.kwargs["data"]["create"]["budget_duration"] == litellm.internal_user_budget_duration ) + + +@pytest.mark.asyncio +async def test_user_info_team_list(prisma_client): + """Assert user_info for admin calls team_list function""" + from litellm.proxy._types import LiteLLM_UserTable + + setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) + setattr(litellm.proxy.proxy_server, "master_key", "sk-1234") + await litellm.proxy.proxy_server.prisma_client.connect() + + from litellm.proxy.management_endpoints.internal_user_endpoints import user_info + + with patch( + "litellm.proxy.management_endpoints.team_endpoints.list_team", + new_callable=AsyncMock, + ) as mock_client: + + prisma_client.get_data = AsyncMock( + return_value=LiteLLM_UserTable( + user_role="proxy_admin", + user_id="default_user_id", + max_budget=None, + user_email="", + ) + ) + + try: + await user_info( + user_id=None, + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-1234", user_id="default_user_id" + ), + ) + except Exception: + pass + + mock_client.assert_called() diff --git a/tests/test_users.py b/tests/test_users.py index 632dd8f36c..8113fd0801 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -99,13 +99,6 @@ async def test_user_info(): ) assert status == 403 - ## check if returned teams as admin == all teams ## - admin_info = await get_user_info( - session=session, get_user="", call_user="sk-1234", view_all=True - ) - all_teams = await list_teams(session=session, i=0) - assert len(admin_info["teams"]) == len(all_teams) - @pytest.mark.asyncio async def test_user_update(): From 718c2cfa4ee4d38ff3bd554d563d6ddfa8a6eb51 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 12 Aug 2024 19:53:05 -0700 Subject: [PATCH 9/9] docs(team_logging.md): cleanup docs --- .../docs/proxy/team_based_routing.md | 38 ------------ docs/my-website/docs/proxy/team_logging.md | 62 +++++++++++++++---- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/docs/my-website/docs/proxy/team_based_routing.md b/docs/my-website/docs/proxy/team_based_routing.md index ad7e8b977d..89b18ec63d 100644 --- a/docs/my-website/docs/proxy/team_based_routing.md +++ b/docs/my-website/docs/proxy/team_based_routing.md @@ -71,41 +71,3 @@ curl --location 'http://0.0.0.0:4000/v1/chat/completions' \ }' ``` -## Team Based Logging - -[👉 Tutorial - Allow each team to use their own Langfuse Project / custom callbacks](team_logging.md) - - - - diff --git a/docs/my-website/docs/proxy/team_logging.md b/docs/my-website/docs/proxy/team_logging.md index bc9f19f5b2..1cc91c2dfe 100644 --- a/docs/my-website/docs/proxy/team_logging.md +++ b/docs/my-website/docs/proxy/team_logging.md @@ -2,7 +2,7 @@ import Image from '@theme/IdealImage'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# 👥📊 [BETA] Team Based Logging +# 👥📊 Team Based Logging Allow each team to use their own Langfuse Project / custom callbacks @@ -13,6 +13,46 @@ Team 2 -> Logs to Langfuse Project 2 Team 3 -> Disabled Logging (for GDPR compliance) ``` + +## Team Based Logging + +[👉 Tutorial - Allow each team to use their own Langfuse Project / custom callbacks](team_logging.md) + + +## Logging / Caching + +Turn on/off logging and caching for a specific team id. + +**Example:** + +This config would send langfuse logs to 2 different langfuse projects, based on the team id + +```yaml +litellm_settings: + default_team_settings: + - team_id: my-secret-project + success_callback: ["langfuse"] + langfuse_public_key: os.environ/LANGFUSE_PUB_KEY_1 # Project 1 + langfuse_secret: os.environ/LANGFUSE_PRIVATE_KEY_1 # Project 1 + - team_id: ishaans-secret-project + success_callback: ["langfuse"] + langfuse_public_key: os.environ/LANGFUSE_PUB_KEY_2 # Project 2 + langfuse_secret: os.environ/LANGFUSE_SECRET_2 # Project 2 +``` + +Now, when you [generate keys](./virtual_keys.md) for this team-id + +```bash +curl -X POST 'http://0.0.0.0:4000/key/generate' \ +-H 'Authorization: Bearer sk-1234' \ +-H 'Content-Type: application/json' \ +-d '{"team_id": "ishaans-secret-project"}' +``` + +All requests made with these keys will log data to their team-specific logging. --> + +## [BETA] Team Logging via API + :::info ✨ This is an Enterprise only feature [Get Started with Enterprise here](https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat) @@ -20,9 +60,9 @@ Team 3 -> Disabled Logging (for GDPR compliance) ::: -## Set Callbacks Per Team +### Set Callbacks Per Team -### 1. Set callback for team +#### 1. Set callback for team We make a request to `POST /team/{team_id}/callback` to add a callback for @@ -42,7 +82,7 @@ curl -X POST 'http:/localhost:4000/team/dbe2f686-a686-4896-864a-4c3924458709/cal }' ``` -#### Supported Values +##### Supported Values | Field | Supported Values | Notes | |-------|------------------|-------| @@ -53,7 +93,7 @@ curl -X POST 'http:/localhost:4000/team/dbe2f686-a686-4896-864a-4c3924458709/cal |     `langfuse_secret_key` | string | Required | |     `langfuse_host` | string | Optional (defaults to https://cloud.langfuse.com) | -### 2. Create key for team +#### 2. Create key for team All keys created for team `dbe2f686-a686-4896-864a-4c3924458709` will log to langfuse project specified on [Step 1. Set callback for team](#1-set-callback-for-team) @@ -68,7 +108,7 @@ curl --location 'http://0.0.0.0:4000/key/generate' \ ``` -### 3. Make `/chat/completion` request for team +#### 3. Make `/chat/completion` request for team ```shell curl -i http://localhost:4000/v1/chat/completions \ @@ -85,7 +125,7 @@ curl -i http://localhost:4000/v1/chat/completions \ Expect this to be logged on the langfuse project specified on [Step 1. Set callback for team](#1-set-callback-for-team) -## Disable Logging for a Team +### Disable Logging for a Team To disable logging for a specific team, you can use the following endpoint: @@ -93,7 +133,7 @@ To disable logging for a specific team, you can use the following endpoint: This endpoint removes all success and failure callbacks for the specified team, effectively disabling logging. -### Step 1. Disable logging for team +#### Step 1. Disable logging for team ```shell curl -X POST 'http://localhost:4000/team/YOUR_TEAM_ID/disable_logging' \ @@ -115,7 +155,7 @@ A successful request will return a response similar to this: } ``` -### Step 2. Test it - `/chat/completions` +#### Step 2. Test it - `/chat/completions` Use a key generated for team = `team_id` - you should see no logs on your configured success callback (eg. Langfuse) @@ -131,7 +171,7 @@ curl -i http://localhost:4000/v1/chat/completions \ }' ``` -### Debugging / Troubleshooting +#### Debugging / Troubleshooting - Check active callbacks for team using `GET /team/{team_id}/callback` @@ -142,7 +182,7 @@ curl -X GET 'http://localhost:4000/team/dbe2f686-a686-4896-864a-4c3924458709/cal -H 'Authorization: Bearer sk-1234' ``` -## Team Logging Endpoints +### Team Logging Endpoints - [`POST /team/{team_id}/callback` Add a success/failure callback to a team](https://litellm-api.up.railway.app/#/team%20management/add_team_callbacks_team__team_id__callback_post) - [`GET /team/{team_id}/callback` - Get the success/failure callbacks and variables for a team](https://litellm-api.up.railway.app/#/team%20management/get_team_callbacks_team__team_id__callback_get)