Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Litellm dev 01 13 2025 p2 #7758

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 23 additions & 41 deletions litellm/litellm_core_utils/prompt_templates/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2185,12 +2185,7 @@ def get_image_details(image_url) -> Tuple[str, str]:
# Convert the image content to base64 bytes
base64_bytes = base64.b64encode(response.content).decode("utf-8")

# Get mime-type
mime_type = content_type.split("/")[
1
] # Extract mime-type from content-type header

return base64_bytes, mime_type
return base64_bytes, content_type

except Exception as e:
raise e
Expand All @@ -2216,50 +2211,37 @@ def _process_bedrock_converse_image_block(
mime_type = "image/jpeg"
image_format = "jpeg"
_blob = BedrockSourceBlock(bytes=img_without_base_64)
supported_image_formats = (
litellm.AmazonConverseConfig().get_supported_image_types()
)
supported_document_types = (
litellm.AmazonConverseConfig().get_supported_document_types()
)
if image_format in supported_image_formats:
return BedrockContentBlock(image=BedrockImageBlock(source=_blob, format=image_format)) # type: ignore
elif image_format in supported_document_types:
return BedrockContentBlock(document=BedrockDocumentBlock(source=_blob, format=image_format, name="DocumentPDFmessages_{}".format(str(uuid.uuid4())))) # type: ignore
else:
# Handle the case when the image format is not supported
raise ValueError(
"Unsupported image format: {}. Supported formats: {}".format(
image_format, supported_image_formats
)
)

elif "https:/" in image_url:
# Case 2: Images with direct links
image_bytes, image_format = get_image_details(image_url)
image_bytes, mime_type = get_image_details(image_url)
image_format = mime_type.split("/")[1]
_blob = BedrockSourceBlock(bytes=image_bytes)
supported_image_formats = (
litellm.AmazonConverseConfig().get_supported_image_types()
)
supported_document_types = (
litellm.AmazonConverseConfig().get_supported_document_types()
)
if image_format in supported_image_formats:
return BedrockContentBlock(image=BedrockImageBlock(source=_blob, format=image_format)) # type: ignore
elif image_format in supported_document_types:
return BedrockContentBlock(document=BedrockDocumentBlock(source=_blob, format=image_format, name="DocumentPDFmessages_{}".format(str(uuid.uuid4())))) # type: ignore
else:
# Handle the case when the image format is not supported
raise ValueError(
"Unsupported image format: {}. Supported formats: {}".format(
image_format, supported_image_formats
)
)
else:
raise ValueError(
"Unsupported image type. Expected either image url or base64 encoded string - \
e.g. 'data:image/jpeg;base64,<base64-encoded-string>'"
)

supported_image_formats = litellm.AmazonConverseConfig().get_supported_image_types()

document_types = ["application", "text"]
is_document = any(
mime_type.startswith(document_type) for document_type in document_types
)

if image_format in supported_image_formats:
return BedrockContentBlock(image=BedrockImageBlock(source=_blob, format=image_format)) # type: ignore
elif is_document:
return BedrockContentBlock(document=BedrockDocumentBlock(source=_blob, format=image_format, name="DocumentPDFmessages_{}".format(str(uuid.uuid4())))) # type: ignore
else:
# Handle the case when the image format is not supported
raise ValueError(
"Unsupported image format: {}. Supported formats: {}".format(
image_format, supported_image_formats
)
)


def _convert_to_bedrock_tool_call_invoke(
tool_calls: list,
Expand Down
2 changes: 1 addition & 1 deletion litellm/proxy/_new_secret_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ model_list:
model: "azure/gpt-4o"
api_key: os.environ/AZURE_API_KEY
api_base: os.environ/AZURE_API_BASE

1 change: 1 addition & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ class GenerateKeyResponse(KeyRequestBase):
user_id: Optional[str] = None
token_id: Optional[str] = None
litellm_budget_table: Optional[Any] = None
token: Optional[str] = None

@model_validator(mode="before")
@classmethod
Expand Down
65 changes: 58 additions & 7 deletions litellm/proxy/hooks/key_management_event_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import (
GenerateKeyRequest,
GenerateKeyResponse,
KeyRequest,
LiteLLM_AuditLogs,
LiteLLM_VerificationToken,
LitellmTableNames,
ProxyErrorTypes,
ProxyException,
RegenerateKeyRequest,
UpdateKeyRequest,
UserAPIKeyAuth,
WebhookEvent,
Expand All @@ -30,7 +32,7 @@ class KeyManagementEventHooks:
@staticmethod
async def async_key_generated_hook(
data: GenerateKeyRequest,
response: dict,
response: GenerateKeyResponse,
user_api_key_dict: UserAPIKeyAuth,
litellm_changed_by: Optional[str] = None,
):
Expand All @@ -48,11 +50,13 @@ async def async_key_generated_hook(
from litellm.proxy.proxy_server import litellm_proxy_admin_name

if data.send_invite_email is True:
await KeyManagementEventHooks._send_key_created_email(response)
await KeyManagementEventHooks._send_key_created_email(
response.model_dump(exclude_none=True)
)

# Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True
if litellm.store_audit_logs is True:
_updated_values = json.dumps(response, default=str)
_updated_values = response.model_dump_json(exclude_none=True)
asyncio.create_task(
create_audit_log_for_update(
request_data=LiteLLM_AuditLogs(
Expand All @@ -63,7 +67,7 @@ async def async_key_generated_hook(
or litellm_proxy_admin_name,
changed_by_api_key=user_api_key_dict.api_key,
table_name=LitellmTableNames.KEY_TABLE_NAME,
object_id=response.get("token_id", ""),
object_id=response.token_id or "",
action="created",
updated_values=_updated_values,
before_value=None,
Expand All @@ -72,8 +76,8 @@ async def async_key_generated_hook(
)
# store the generated key in the secret manager
await KeyManagementEventHooks._store_virtual_key_in_secret_manager(
secret_name=data.key_alias or f"virtual-key-{uuid.uuid4()}",
secret_token=response.get("token", ""),
secret_name=data.key_alias or f"virtual-key-{response.token_id}",
secret_token=response.key,
)

@staticmethod
Expand Down Expand Up @@ -119,7 +123,25 @@ async def async_key_updated_hook(
)
)
)
pass

@staticmethod
async def async_key_rotated_hook(
data: Optional[RegenerateKeyRequest],
existing_key_row: Any,
response: GenerateKeyResponse,
user_api_key_dict: UserAPIKeyAuth,
litellm_changed_by: Optional[str] = None,
):
# store the generated key in the secret manager
if data is not None and response.token_id is not None:
initial_secret_name = (
existing_key_row.key_alias or f"virtual-key-{existing_key_row.token}"
)
await KeyManagementEventHooks._rotate_virtual_key_in_secret_manager(
current_secret_name=initial_secret_name,
new_secret_name=data.key_alias or f"virtual-key-{response.token_id}",
new_secret_value=response.key,
)

@staticmethod
async def async_key_deleted_hook(
Expand Down Expand Up @@ -207,6 +229,35 @@ async def _store_virtual_key_in_secret_manager(secret_name: str, secret_token: s
secret_value=secret_token,
)

@staticmethod
async def _rotate_virtual_key_in_secret_manager(
current_secret_name: str, new_secret_name: str, new_secret_value: str
):
"""
Update a virtual key in the secret manager

Args:
secret_name: Name of the virtual key
secret_token: Value of the virtual key (example: sk-1234)
"""
if litellm._key_management_settings is not None:
if litellm._key_management_settings.store_virtual_keys is True:
from litellm.secret_managers.base_secret_manager import (
BaseSecretManager,
)

# store the key in the secret manager
if isinstance(litellm.secret_manager_client, BaseSecretManager):
await litellm.secret_manager_client.async_rotate_secret(
current_secret_name=KeyManagementEventHooks._get_secret_name(
current_secret_name
),
new_secret_name=KeyManagementEventHooks._get_secret_name(
new_secret_name
),
new_secret_value=new_secret_value,
)

@staticmethod
def _get_secret_name(secret_name: str) -> str:
if litellm._key_management_settings.prefix_for_stored_virtual_keys.endswith(
Expand Down
20 changes: 17 additions & 3 deletions litellm/proxy/management_endpoints/key_management_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@ async def generate_key_fn( # noqa: PLR0915
data.soft_budget
) # include the user-input soft budget in the response

response = GenerateKeyResponse(**response)

asyncio.create_task(
KeyManagementEventHooks.async_key_generated_hook(
data=data,
Expand All @@ -532,7 +534,7 @@ async def generate_key_fn( # noqa: PLR0915
)
)

return GenerateKeyResponse(**response)
return response
except Exception as e:
verbose_proxy_logger.exception(
"litellm.proxy.proxy_server.generate_key_fn(): Exception occured - {}".format(
Expand Down Expand Up @@ -1517,7 +1519,7 @@ async def regenerate_key_fn(
updated_token_dict = dict(updated_token)

updated_token_dict["key"] = new_token
updated_token_dict.pop("token")
updated_token_dict["token_id"] = updated_token_dict.pop("token")

### 3. remove existing key entry from cache
######################################################################
Expand All @@ -1535,9 +1537,21 @@ async def regenerate_key_fn(
proxy_logging_obj=proxy_logging_obj,
)

return GenerateKeyResponse(
response = GenerateKeyResponse(
**updated_token_dict,
)

asyncio.create_task(
KeyManagementEventHooks.async_key_rotated_hook(
data=data,
existing_key_row=_key_in_db,
response=response,
user_api_key_dict=user_api_key_dict,
litellm_changed_by=litellm_changed_by,
)
)

return response
except Exception as e:
raise handle_exception_on_proxy(e)

Expand Down
81 changes: 81 additions & 0 deletions litellm/secret_managers/base_secret_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import httpx

from litellm import verbose_logger


class BaseSecretManager(ABC):
"""
Expand Down Expand Up @@ -93,3 +95,82 @@ async def async_delete_secret(
dict: Response from the secret manager containing deletion details
"""
pass

async def async_rotate_secret(
self,
current_secret_name: str,
new_secret_name: str,
new_secret_value: str,
optional_params: Optional[dict] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
) -> dict:
"""
Async function to rotate a secret by creating a new one and deleting the old one.
This allows for both value and name changes during rotation.

Args:
current_secret_name: Current name of the secret
new_secret_name: New name for the secret
new_secret_value: New value for the secret
optional_params: Additional AWS parameters
timeout: Request timeout

Returns:
dict: Response containing the new secret details

Raises:
ValueError: If the secret doesn't exist or if there's an HTTP error
"""
try:
# First verify the old secret exists
old_secret = await self.async_read_secret(
secret_name=current_secret_name,
optional_params=optional_params,
timeout=timeout,
)

if old_secret is None:
raise ValueError(f"Current secret {current_secret_name} not found")

# Create new secret with new name and value
create_response = await self.async_write_secret(
secret_name=new_secret_name,
secret_value=new_secret_value,
description=f"Rotated from {current_secret_name}",
optional_params=optional_params,
timeout=timeout,
)

# Verify new secret was created successfully
new_secret = await self.async_read_secret(
secret_name=new_secret_name,
optional_params=optional_params,
timeout=timeout,
)

if new_secret is None:
raise ValueError(f"Failed to verify new secret {new_secret_name}")

# If everything is successful, delete the old secret
await self.async_delete_secret(
secret_name=current_secret_name,
recovery_window_in_days=7, # Keep for recovery if needed
optional_params=optional_params,
timeout=timeout,
)

return create_response

except httpx.HTTPStatusError as err:
verbose_logger.exception(
"Error rotating secret in AWS Secrets Manager: %s",
str(err.response.text),
)
raise ValueError(f"HTTP error occurred: {err.response.text}")
except httpx.TimeoutException:
raise ValueError("Timeout error occurred")
except Exception as e:
verbose_logger.exception(
"Error rotating secret in AWS Secrets Manager: %s", str(e)
)
raise
10 changes: 10 additions & 0 deletions litellm/secret_managers/hashicorp_secret_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ async def async_write_secret(
verbose_logger.exception(f"Error writing secret to Hashicorp Vault: {e}")
return {"status": "error", "message": str(e)}

async def async_rotate_secret(
self,
current_secret_name: str,
new_secret_name: str,
new_secret_value: str,
optional_params: Dict | None = None,
timeout: float | httpx.Timeout | None = None,
) -> Dict:
raise NotImplementedError("Hashicorp does not support secret rotation")

async def async_delete_secret(
self,
secret_name: str,
Expand Down
Loading
Loading