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
This commit is contained in:
derhornspieler 2026-08-23 17:20:42 -04:00
parent 0a88371a3a
commit 0a69790bb9
3 changed files with 25 additions and 14 deletions

View file

@ -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):

View file

@ -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:

View file

@ -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