diff --git a/.github/workflows/test-unit-proxy-endpoints.yml b/.github/workflows/test-unit-proxy-endpoints.yml index 20b89c72440..645996f779d 100644 --- a/.github/workflows/test-unit-proxy-endpoints.yml +++ b/.github/workflows/test-unit-proxy-endpoints.yml @@ -44,6 +44,7 @@ jobs: tests/test_litellm/proxy/vector_store_endpoints tests/test_litellm/proxy/agent_endpoints tests/test_litellm/proxy/a2a + tests/test_litellm/proxy/credential_endpoints tests/test_litellm/proxy/discovery_endpoints tests/test_litellm/proxy/health_endpoints tests/test_litellm/proxy/shutdown diff --git a/litellm/integrations/azure_sentinel/azure_sentinel.py b/litellm/integrations/azure_sentinel/azure_sentinel.py index 563f815b582..24328549094 100644 --- a/litellm/integrations/azure_sentinel/azure_sentinel.py +++ b/litellm/integrations/azure_sentinel/azure_sentinel.py @@ -78,8 +78,8 @@ class AzureSentinelLogger(CustomBatchLogger): If not provided, will use AZURE_SENTINEL_AUDIT_STREAM_NAME env var or the standard stream name. authority_host (str, optional): Microsoft Entra authority host that issues the OAuth2 token, e.g. "https://login.microsoftonline.us" for Azure Government. If not provided, will use - AZURE_AUTHORITY_HOST env var or default to the Azure Public Cloud authority. The Azure - Monitor audience is derived from it. + AZURE_SENTINEL_AUTHORITY_HOST or AZURE_AUTHORITY_HOST env vars, or default to the Azure + Public Cloud authority. The Azure Monitor audience is derived from it. """ self.async_httpx_client = get_async_httpx_client(llm_provider=httpxSpecialProvider.LoggingCallback) @@ -95,7 +95,10 @@ class AzureSentinelLogger(CustomBatchLogger): client_secret or os.getenv("AZURE_SENTINEL_CLIENT_SECRET") or os.getenv("AZURE_CLIENT_SECRET") ) resolved_authority_host: Final = self._normalize_authority_host( - authority_host or os.getenv("AZURE_AUTHORITY_HOST") or DEFAULT_AZURE_AUTHORITY_HOST + authority_host + or os.getenv("AZURE_SENTINEL_AUTHORITY_HOST") + or os.getenv("AZURE_AUTHORITY_HOST") + or DEFAULT_AZURE_AUTHORITY_HOST ) if not resolved_dcr_immutable_id: diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index a5078c50fc0..f2cb1124fa0 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -3528,11 +3528,34 @@ async def info_key_fn( ): """ Retrieve information about a key. + Parameters: - key: Optional[str] = Query parameter representing the key in the request - user_api_key_dict: UserAPIKeyAuth = Dependency representing the user's API key + - key: str | None (query parameter) - The key to look up. Accepts the plaintext key or its hash. + Defaults to the key in the Authorization header. + Returns: - Dict containing the key and its associated information + - key: str - The key that was looked up, echoed back as it was passed in + - info: dict - The key's row, minus the hashed token + - key_alias: str | None - User-friendly key alias + - spend: float - Amount spent by the key. When budget_duration is set this covers only the + current budget window, not the key's lifetime + - max_budget: float | None - Max budget for the key, enforced against spend + - budget_duration: str | None - Budget reset period ("30d", "1h", etc.) + - budget_reset_at: datetime | None - When the current budget window ends and spend is next + reset to 0, not when it was last reset. Reset times snap to standard boundaries in the + configured timezone (30d and 1mo land on the 1st of the month, 7d on Monday, 1h on the + hour), so subtracting budget_duration from it does not give the window's start + - model_max_budget: dict - Per-model budgets, e.g. {"gpt-4": {"budget_limit": 0.0005, "time_period": "30d"}} + - model_max_budget_usage: dict | None - Current-window spend per model, present only when + the key has per-model budgets + - models: list - Model_name's the key is allowed to call + - tpm_limit / rpm_limit: int | None - Tokens and requests per minute limits + - metadata: dict - Metadata for the key, e.g. {"team": "core-infra"} + - blocked: bool | None - Whether the key is blocked + - expires: datetime | None - When the key stops authenticating requests + - last_active: datetime | None - When the key was last used + - object_permission: dict | None - Resolved vector store / MCP permissions when the key has + an object_permission_id Example Curl: ``` diff --git a/tests/test_litellm/integrations/test_azure_sentinel.py b/tests/test_litellm/integrations/test_azure_sentinel.py index 56662eea633..f48f5cb1784 100644 --- a/tests/test_litellm/integrations/test_azure_sentinel.py +++ b/tests/test_litellm/integrations/test_azure_sentinel.py @@ -313,6 +313,7 @@ def _build_logger(**overrides): @pytest.fixture def _no_authority_host_env(monkeypatch): + monkeypatch.delenv("AZURE_SENTINEL_AUTHORITY_HOST", raising=False) monkeypatch.delenv("AZURE_AUTHORITY_HOST", raising=False) @@ -389,3 +390,38 @@ async def test_azure_sentinel_token_request_uses_sovereign_authority_and_audienc assert len(token_calls) == 1 assert token_calls[0].kwargs["url"] == "https://login.microsoftonline.us/test-tenant-id/oauth2/v2.0/token" assert token_calls[0].kwargs["data"]["scope"] == "https://monitor.azure.us/.default" + + +def test_azure_sentinel_authority_host_prefers_the_sentinel_scoped_env_var(_no_authority_host_env, monkeypatch): + """AZURE_AUTHORITY_HOST is shared with Azure OpenAI and the azure_storage callback, so a deployment + whose Sentinel workspace lives in a different cloud than the rest of its Azure resources needs a + Sentinel-scoped override. This mirrors how tenant, client id and secret already resolve.""" + monkeypatch.setenv("AZURE_AUTHORITY_HOST", "https://login.microsoftonline.com") + monkeypatch.setenv("AZURE_SENTINEL_AUTHORITY_HOST", "https://login.microsoftonline.us") + + logger = _build_logger() + + assert logger.authority_host == "https://login.microsoftonline.us" + assert logger.oauth_scope == "https://monitor.azure.us/.default" + + +def test_azure_sentinel_falls_back_to_the_shared_authority_host(_no_authority_host_env, monkeypatch): + """With no Sentinel-scoped override the shared variable still applies, which is the behavior + shipped in the original fix.""" + monkeypatch.setenv("AZURE_AUTHORITY_HOST", "https://login.microsoftonline.us") + + logger = _build_logger() + + assert logger.authority_host == "https://login.microsoftonline.us" + assert logger.oauth_scope == "https://monitor.azure.us/.default" + + +def test_azure_sentinel_authority_host_argument_outranks_the_scoped_env_var(_no_authority_host_env, monkeypatch): + """An explicit constructor argument is the most specific source and has to win, otherwise a + deployment that exports the scoped variable silently overrides an SDK caller.""" + monkeypatch.setenv("AZURE_SENTINEL_AUTHORITY_HOST", "https://login.microsoftonline.us") + + logger = _build_logger(authority_host="https://login.microsoftonline.com") + + assert logger.authority_host == "https://login.microsoftonline.com" + assert logger.oauth_scope == "https://monitor.azure.com/.default"