litellm/tests/integration/_support/sigv4.py
devin-ai-integration[bot] 43f07d0bd8
fix(bedrock): list the account's invocable models behind bedrock/* when check_provider_endpoint is on (#43981)
* fix(bedrock): list the account's invocable models behind bedrock/* when check_provider_endpoint is on

* fix(bedrock): sign the listing query RFC 3986 style and raise BedrockError on a failed listing

* test(utils): keep test_utils.py at main's formatting, only the Bedrock discovery test is new

* fix(bedrock): chain BedrockModelInfo's constructor so the passthrough config keeps its event parser

* fix(bedrock): list discovered ids without the provider prefix so partial wildcards filter

get_valid_models returns bare vendor ids for every other provider and for the static bedrock catalog, and the proxy's wildcard expansion adds the provider prefix itself. The lister prefixed its ids, so a partial wildcard like bedrock/anthropic.* never matched the proxy's filter and listed uncallable bedrock/anthropic.bedrock/<id> entries

* test(bedrock): integration cells for wildcard discovery through the proxy and the SDK

* fix(proxy): keep a partial wildcard a filter when its deployment repeats the prefix

The wildcard expansion guessed filter-or-alias by whether any provider id carried the prefix. A deployment such as model_name bedrock/anthropic.* over model bedrock/anthropic.* can only ever route names that share the prefix, so when the account has no on-demand anthropic.* id (every Anthropic model behind an inference profile) the guess fell into the alias branch and listed bedrock/anthropic.<every id>, none of them callable. A deployment whose model repeats the suffix now always filters, and lists nothing when nothing matches

* fix(proxy): prefix every discovered id under a custom wildcard prefix that starts a vendor id

* test(bedrock): Final and read-only annotations in the wildcard discovery tests

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-09 14:14:17 -07:00

48 lines
1.8 KiB
Python

import hashlib
import hmac
from collections.abc import Mapping
from typing import Final
from urllib.parse import parse_qsl
_UNRESERVED: Final = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
def _encoded(value: str, safe: bytes) -> str:
return "".join(chr(byte) if byte in safe else f"%{byte:02X}" for byte in value.encode("utf-8"))
def encoded_path(value: str) -> str:
return _encoded(value, _UNRESERVED + b"/")
def canonical_query(query: str) -> str:
"""The SigV4 canonical query string: pairs sorted by name, each name and value URI-encoded."""
return "&".join(
_encoded(name, _UNRESERVED) + "=" + _encoded(value, _UNRESERVED)
for name, value in sorted(parse_qsl(query, keep_blank_values=True))
)
def signature(
method: str,
path: str,
headers: Mapping[str, str],
signed: str,
body: bytes,
secret: str,
scope: str,
query: str = "",
) -> tuple[str, str]:
"""AWS SigV4 equations, independent of botocore and LiteLLM's signer; `query` is already canonical."""
canonical_headers: Final = "".join(
name + ":" + " ".join(headers[name].split()) + "\n" for name in signed.split(";")
)
canonical: Final = "\n".join((method, path, query, canonical_headers, signed, hashlib.sha256(body).hexdigest()))
canonical_hash: Final = hashlib.sha256(canonical.encode()).hexdigest()
date, region, service, terminator = scope.split("/")
assert terminator == "aws4_request"
key = ("AWS4" + secret).encode()
for part in (date, region, service, terminator):
key = hmac.new(key, part.encode(), hashlib.sha256).digest()
to_sign: Final = "\n".join(("AWS4-HMAC-SHA256", headers["x-amz-date"], scope, canonical_hash))
return canonical_hash, hmac.new(key, to_sign.encode(), hashlib.sha256).hexdigest()