diff --git a/litellm/llms/anthropic/files/handler.py b/litellm/llms/anthropic/files/handler.py index 2c678310032..fbe50a15aa8 100644 --- a/litellm/llms/anthropic/files/handler.py +++ b/litellm/llms/anthropic/files/handler.py @@ -43,6 +43,7 @@ class AnthropicFilesHandler: api_key: str | None = None, timeout: float | httpx.Timeout = 600.0, max_retries: int | None = None, + litellm_params: dict | None = None, # mutable-ok: handed straight to aget_auth_header ) -> HttpxBinaryResponseContent: """ Async: Retrieve file content from Anthropic. @@ -56,6 +57,10 @@ class AnthropicFilesHandler: api_key: Anthropic API key timeout: Request timeout max_retries: Max retry attempts (unused for now) + litellm_params: Optional deployment/credential params carrying the + workload-identity federation fields (rule id, org id, identity + token file, etc.). Without these a credential-backed federated + deployment has no static api_key and no way to mint one. Returns: HttpxBinaryResponseContent: Binary content wrapped in compatible response format @@ -74,7 +79,7 @@ class AnthropicFilesHandler: # Get Anthropic API credentials api_base = self.anthropic_model_info.get_api_base(api_base) auth_header: Final = await self.anthropic_model_info.aget_auth_header( - api_key, api_base, allow_workload_identity=True + api_key, api_base, litellm_params=litellm_params, allow_workload_identity=True ) if auth_header is None: @@ -118,6 +123,7 @@ class AnthropicFilesHandler: api_key: str | None = None, timeout: float | httpx.Timeout = 600.0, max_retries: int | None = None, + litellm_params: dict | None = None, # mutable-ok: handed straight to aget_auth_header ) -> HttpxBinaryResponseContent | Coroutine[object, object, HttpxBinaryResponseContent]: """ Retrieve file content from Anthropic. @@ -132,6 +138,8 @@ class AnthropicFilesHandler: api_key: Anthropic API key timeout: Request timeout max_retries: Max retry attempts (unused for now) + litellm_params: Optional deployment/credential params carrying the + workload-identity federation fields, forwarded to aget_auth_header. Returns: HttpxBinaryResponseContent or Coroutine: Binary content wrapped in compatible response format @@ -142,6 +150,7 @@ class AnthropicFilesHandler: api_base=api_base, api_key=api_key, max_retries=max_retries, + litellm_params=litellm_params, ) else: return asyncio.run( @@ -151,6 +160,7 @@ class AnthropicFilesHandler: api_key=api_key, timeout=timeout, max_retries=max_retries, + litellm_params=litellm_params, ) ) diff --git a/litellm/llms/anthropic/wif.py b/litellm/llms/anthropic/wif.py index 2c28aec0413..679ed515a39 100644 --- a/litellm/llms/anthropic/wif.py +++ b/litellm/llms/anthropic/wif.py @@ -345,17 +345,29 @@ def resolve_anthropic_base(api_base: str | None) -> str: def _trusted_exchange_hosts() -> frozenset[str]: """Hostnames a federated exchange may reach: Anthropic's own, plus whatever the operator put in - the environment. Comma separated, case folded, entries given as a URL reduced to their host.""" + the environment. Comma separated, case folded, each entry reduced to its bare hostname whether + it was written as a URL, a plain host, or a ``host:port`` (a bare ``host:port`` would otherwise + parse as scheme-only and match nothing, since the compared exchange base carries no port).""" configured: Final = os.getenv(_TRUSTED_EXCHANGE_HOSTS_ENV) or "" extra: Final = (entry.strip() for entry in configured.split(",") if entry.strip()) return frozenset( chain( (_DEFAULT_TRUSTED_EXCHANGE_HOST,), - ((urlsplit(entry).hostname or entry.split("/")[0]).lower() for entry in extra), + (_normalize_trusted_host(entry) for entry in extra), ) ) +def _normalize_trusted_host(entry: str) -> str: + """Reduce one allowlist entry to its lowercased hostname. A missing scheme is added as ``//`` so + ``host:port`` is parsed as an authority rather than as a scheme.""" + to_parse: Final = entry if "://" in entry else f"//{entry}" + host: Final = urlsplit(to_parse).hostname + if host is not None: + return host.lower() + return entry.split("/", 1)[0].split(":", 1)[0].lower() + + def _raise_if_exchange_host_untrusted(exchange_base: str, model: str) -> None: """The federated exchange refuses any host the operator has not vouched for, whatever wrote the deployment's api_base. Exact hostname match, never a substring: ``api.anthropic.com.evil.test``