mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(anthropic): thread WIF params through files handler and normalize host:port allowlist entries
- AnthropicFilesHandler.afile_content/file_content now accept litellm_params and forward them to aget_auth_header. Without this a credential-backed federated deployment (no static api_key) has no federation fields on the token exchange, so batch-result downloads fail with 'Missing Anthropic API Key'. - LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS entries written as bare 'host:port' now reduce to just their hostname. urlsplit was parsing 'gateway.example.com:8443' as a scheme with hostname=None, so the fallback stored 'host:port' while the exchange comparison uses the bare hostname and the entry never matched.
This commit is contained in:
parent
6e44b5b626
commit
d3a1256a18
2 changed files with 25 additions and 3 deletions
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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``
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue