diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 735a001e343..cb50df64301 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -186,13 +186,15 @@ def _fetch_anthropic_models_page( def _fetch_anthropic_model_ids( api_base: str, headers: Mapping[str, str], after_id: str | None, pages_left: int ) -> tuple[str, ...]: - if pages_left <= 0: - raise Exception(f"Anthropic /v1/models did not terminate within {_MODEL_LIST_PAGE_CAP} pages.") - page: Final = _fetch_anthropic_models_page(api_base, headers, after_id) - page_ids: Final = tuple(entry.id for entry in page.data) - if not page.has_more or page.last_id is None: - return page_ids - return page_ids + _fetch_anthropic_model_ids(api_base, headers, page.last_id, pages_left - 1) + collected: tuple[str, ...] = () # rebind-ok: accumulates one page of ids per iteration + cursor: str | None = after_id # rebind-ok: advances to each page's last_id + for _ in range(max(pages_left, 0)): + page: Final = _fetch_anthropic_models_page(api_base, headers, cursor) + collected += tuple(entry.id for entry in page.data) + if not page.has_more or page.last_id is None: + return collected + cursor = page.last_id + raise Exception(f"Anthropic /v1/models did not terminate within {_MODEL_LIST_PAGE_CAP} pages.") class AnthropicModelInfo(BaseLLMModelInfo): diff --git a/litellm/llms/anthropic/wif.py b/litellm/llms/anthropic/wif.py index 03340c0e084..4be1a1e7ae1 100644 --- a/litellm/llms/anthropic/wif.py +++ b/litellm/llms/anthropic/wif.py @@ -275,13 +275,17 @@ def _strip_chat_suffix(base: str) -> str: def _strip_path_suffixes(path: str) -> str: """Drop the chat-surface suffixes a deployment base may carry, so every tier derives the same - token URL. Recursion depth is bounded by the path's own segment count.""" - trimmed: Final = path.rstrip("/") - shortened: Final = next( - (trimmed.removesuffix(suffix) for suffix in _CHAT_BASE_SUFFIXES if trimmed.endswith(suffix)), - trimmed, - ) - return trimmed if shortened == trimmed else _strip_path_suffixes(shortened) + token URL. Each pass removes at most one suffix, so the loop is bounded by the segment count.""" + trimmed = path.rstrip("/") # rebind-ok: fixed-point strip, one suffix per pass + while True: + shortened: Final = next( + (trimmed.removesuffix(suffix) for suffix in _CHAT_BASE_SUFFIXES if trimmed.endswith(suffix)), + trimmed, + ) + if shortened == trimmed: + return trimmed + # Re-strip: a doubled suffix leaves a trailing slash that would stop the next match. + trimmed = shortened.rstrip("/") def _config_value(litellm_params: Mapping[str, object] | None, param_key: str, env_name: str) -> str | None: diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 7e48a1563e5..6e54bf49a3b 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -3967,6 +3967,11 @@ class TestAnthropicProxyRouteCallerAuthHeaders: ): monkeypatch.delenv(name, raising=False) + # A sibling test leaving SERVER_ROOT_PATH set re-prefixes the passthrough route, so + # /anthropic/... stops resolving and the request 404s before any header is built. + # Pin it so this class asserts on headers rather than on ambient state. + monkeypatch.delenv("SERVER_ROOT_PATH", raising=False) + def _enable_wif(self, monkeypatch) -> None: from litellm.llms.anthropic import common_utils as anthropic_common_utils from litellm.llms.anthropic.wif import aget_anthropic_wif_token