mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
Merge 1a3c30b0ab into f2118fc767
This commit is contained in:
commit
8cf19f0b4b
4 changed files with 106 additions and 21 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"""Utils for accessing credentials."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
|
|
@ -30,18 +31,15 @@ class CredentialAccessor:
|
|||
return {} if credential is None else dict(credential_api_key_secret_reference(credential.credential_values))
|
||||
|
||||
@staticmethod
|
||||
def upsert_credentials(credentials: list[CredentialItem]):
|
||||
"""Add a credential to the list of credentials."""
|
||||
|
||||
credential_names: Final = [cred.credential_name for cred in litellm.credential_list]
|
||||
def upsert_credentials(credentials: Sequence[CredentialItem]) -> None:
|
||||
"""Add credentials to the list, replacing the first existing entry with the same name in place."""
|
||||
first_index_by_name: Final = MappingProxyType(
|
||||
{cred.credential_name: i for i, cred in reversed(tuple(enumerate(litellm.credential_list)))}
|
||||
)
|
||||
|
||||
for credential in credentials:
|
||||
if credential.credential_name in credential_names:
|
||||
# Find and replace the existing credential in the list
|
||||
for i, existing_cred in enumerate(litellm.credential_list):
|
||||
if existing_cred.credential_name == credential.credential_name:
|
||||
litellm.credential_list[i] = credential
|
||||
break
|
||||
if credential.credential_name in first_index_by_name:
|
||||
litellm.credential_list[first_index_by_name[credential.credential_name]] = credential
|
||||
else:
|
||||
litellm.credential_list.append(credential)
|
||||
|
||||
|
|
|
|||
|
|
@ -9545,7 +9545,7 @@ class ProxyConfig:
|
|||
credential_object.credential_values = decrypted_credential_values
|
||||
return credential_object
|
||||
|
||||
async def delete_credentials(self, db_credentials: list[CredentialItem]):
|
||||
async def delete_credentials(self, db_credentials: Sequence[CredentialItem]) -> None:
|
||||
"""
|
||||
Create all-up list of db credentials + local credentials
|
||||
Compare to the litellm.credential_list
|
||||
|
|
@ -9555,16 +9555,9 @@ class ProxyConfig:
|
|||
config: Final = await self.get_config(config_file_path=user_config_file_path)
|
||||
credential_list: Final = self.load_credential_list(config=config)
|
||||
|
||||
## COMBINED LIST ##
|
||||
combined_list: Final = db_credentials + credential_list
|
||||
|
||||
## DELETE ##
|
||||
idx_to_delete: Final = []
|
||||
for idx, credential in enumerate(litellm.credential_list):
|
||||
if credential.credential_name not in [cred.credential_name for cred in combined_list]:
|
||||
idx_to_delete.append(idx)
|
||||
for idx in sorted(idx_to_delete, reverse=True):
|
||||
litellm.credential_list.pop(idx)
|
||||
names_to_keep: Final = frozenset(cred.credential_name for cred in (*db_credentials, *credential_list))
|
||||
litellm.credential_list[:] = (cred for cred in litellm.credential_list if cred.credential_name in names_to_keep)
|
||||
|
||||
async def get_credentials(self, prisma_client: PrismaClient):
|
||||
try:
|
||||
|
|
|
|||
51
tests/unit/litellm_core_utils/test_credential_accessor.py
Normal file
51
tests/unit/litellm_core_utils/test_credential_accessor.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
||||
from litellm.types.utils import CredentialItem
|
||||
|
||||
|
||||
def _credential(name: str, api_key: str) -> CredentialItem:
|
||||
return CredentialItem(credential_name=name, credential_values={"api_key": api_key}, credential_info={})
|
||||
|
||||
|
||||
def _names_and_keys() -> list[tuple[str, str]]:
|
||||
return [(cred.credential_name, cred.credential_values["api_key"]) for cred in litellm.credential_list]
|
||||
|
||||
|
||||
def test_upsert_credentials_replaces_by_name_in_place_and_appends_new(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
litellm, "credential_list", [_credential("a", "old-a"), _credential("b", "old-b"), _credential("c", "old-c")]
|
||||
)
|
||||
|
||||
CredentialAccessor.upsert_credentials(
|
||||
[_credential("b", "new-b"), _credential("d", "new-d"), _credential("a", "new-a")]
|
||||
)
|
||||
|
||||
assert _names_and_keys() == [("a", "new-a"), ("b", "new-b"), ("c", "old-c"), ("d", "new-d")]
|
||||
|
||||
|
||||
def test_upsert_credentials_replaces_the_first_duplicate_that_find_credential_reads(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "credential_list", [_credential("a", "first"), _credential("a", "second")])
|
||||
|
||||
CredentialAccessor.upsert_credentials([_credential("a", "from-db")])
|
||||
|
||||
assert _names_and_keys() == [("a", "from-db"), ("a", "second")]
|
||||
assert CredentialAccessor.get_credential_values("a") == {"api_key": "from-db"}
|
||||
|
||||
|
||||
def test_upsert_credentials_scales_linearly_with_credential_count(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
n = 20_000
|
||||
monkeypatch.setattr(litellm, "credential_list", [_credential(f"cred-{i}", f"k{i}") for i in range(n)])
|
||||
refreshed = [_credential(f"cred-{i}", f"k{i}-v2") for i in range(n)]
|
||||
|
||||
started = time.perf_counter()
|
||||
CredentialAccessor.upsert_credentials(refreshed)
|
||||
elapsed = time.perf_counter() - started
|
||||
|
||||
assert litellm.credential_list == refreshed
|
||||
assert elapsed < 2.0, f"upsert of {n} credentials took {elapsed:.1f}s; the per-reload sync must stay linear"
|
||||
|
|
@ -13,6 +13,7 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -29,6 +30,7 @@ from pydantic import JsonValue, TypeAdapter, ValidationError
|
|||
|
||||
import litellm
|
||||
from litellm.proxy._types import CommonProxyErrors, ConfigGeneralSettings
|
||||
from litellm.types.utils import CredentialItem
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
|
||||
from litellm.types.router import Deployment, LiteLLM_Params
|
||||
from litellm.proxy.proxy_server import (
|
||||
|
|
@ -2059,6 +2061,47 @@ def test_ProxyConfig_load_credential_list_invalid_entry_raises():
|
|||
pc.load_credential_list({"credential_list": [{"missing_required": True}]})
|
||||
|
||||
|
||||
def _credential(name: str) -> CredentialItem:
|
||||
return CredentialItem(credential_name=name, credential_values={"api_key": f"key-{name}"}, credential_info={})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ProxyConfig_delete_credentials_drops_only_names_missing_from_db_and_config(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pc = ProxyConfig()
|
||||
loaded = [_credential("db-kept"), _credential("stale"), _credential("from-config")]
|
||||
monkeypatch.setattr(litellm, "credential_list", loaded)
|
||||
monkeypatch.setattr(
|
||||
pc, "get_config", AsyncMock(return_value={"credential_list": [_credential("from-config").model_dump()]})
|
||||
)
|
||||
|
||||
await pc.delete_credentials([_credential("db-kept")])
|
||||
|
||||
assert litellm.credential_list is loaded
|
||||
assert [cred.credential_name for cred in loaded] == ["db-kept", "from-config"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ProxyConfig_delete_credentials_scales_linearly_with_credential_count(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
n = 20_000
|
||||
pc = ProxyConfig()
|
||||
monkeypatch.setattr(litellm, "credential_list", [_credential(f"cred-{i}") for i in range(n)])
|
||||
monkeypatch.setattr(pc, "get_config", AsyncMock(return_value={}))
|
||||
db_credentials = [cred for cred in litellm.credential_list if cred.credential_name != "cred-7"]
|
||||
|
||||
started = time.perf_counter()
|
||||
await pc.delete_credentials(db_credentials)
|
||||
elapsed = time.perf_counter() - started
|
||||
|
||||
assert litellm.credential_list == db_credentials
|
||||
assert elapsed < 2.0, (
|
||||
f"delete_credentials over {n} credentials took {elapsed:.1f}s; the per-reload sync must stay linear"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ProxyConfig.parse_search_tools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue