From 0a69790bb96076277246e18eab626c19c5200548 Mon Sep 17 00:00:00 2001 From: derhornspieler <15236687+derhornspieler@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:20:42 -0400 Subject: [PATCH] refactor(anthropic): drop the recursion the workload identity paths did not need The repo's recursion guard rejects unignored recursive functions, and this branch had added three. Two were plain iteration written as recursion: the model list walked its pages recursively, and the base URL stripped chat suffixes by calling itself. The third mattered more: a token exchange follower whose leader published nothing called get_token again, so a contended entry grew the stack one frame per failed leader All three are loops now. The suffix strip re-strips trailing slashes on each pass, which the recursion got for free by re-entering a function that begins by stripping them, and which a doubled suffix needs to keep matching The passthrough header tests also pin SERVER_ROOT_PATH. They drive the real relay, and a sibling test leaving that variable set re-prefixes the route, so the request 404s before any header is built and the assertion never runs --- litellm/llms/anthropic/common_utils.py | 16 +++++++++------- litellm/llms/anthropic/wif.py | 18 +++++++++++------- .../test_llm_pass_through_endpoints.py | 5 +++++ 3 files changed, 25 insertions(+), 14 deletions(-) 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