mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(anthropic): resolve the count-tokens base like chat when the deployment has none
This commit is contained in:
parent
412890ff52
commit
272ccfdd22
4 changed files with 44 additions and 13 deletions
|
|
@ -7,7 +7,7 @@ This module handles the transformation of requests to Anthropic's CountTokens AP
|
|||
from typing import Any, Final
|
||||
|
||||
from litellm.constants import ANTHROPIC_TOKEN_COUNTING_BETA_VERSION
|
||||
from litellm.llms.anthropic.wif import anthropic_base_without_chat_suffix
|
||||
from litellm.llms.anthropic.wif import resolve_anthropic_base
|
||||
|
||||
|
||||
class AnthropicCountTokensConfig:
|
||||
|
|
@ -27,14 +27,14 @@ class AnthropicCountTokensConfig:
|
|||
Args:
|
||||
api_base: The deployment's api_base, which names the chat surface (a host, or a
|
||||
base already carrying ``/v1`` or ``/v1/messages``); the count-tokens path is
|
||||
appended to it, so it is never the full count-tokens URL
|
||||
appended to it, so it is never the full count-tokens URL. Unset or empty falls
|
||||
back to ``ANTHROPIC_API_BASE`` / ``ANTHROPIC_BASE_URL`` and then Anthropic's
|
||||
host, the same resolution chat and the federated exchange use
|
||||
|
||||
Returns:
|
||||
The endpoint URL for the CountTokens API
|
||||
"""
|
||||
if api_base is None:
|
||||
return "https://api.anthropic.com/v1/messages/count_tokens"
|
||||
return anthropic_base_without_chat_suffix(api_base) + "/v1/messages/count_tokens"
|
||||
return resolve_anthropic_base(api_base) + "/v1/messages/count_tokens"
|
||||
|
||||
def transform_request_to_count_tokens(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ def get_anthropic_wif_token(
|
|||
params: Final = resolve_anthropic_wif_params(litellm_params)
|
||||
if params is None:
|
||||
return None
|
||||
exchange_base: Final = _token_exchange_base(api_base)
|
||||
exchange_base: Final = resolve_anthropic_base(api_base)
|
||||
_raise_if_exchange_host_untrusted(exchange_base, model)
|
||||
result: Final = engine.get_token(build_anthropic_wif_spec(params, exchange_base))
|
||||
return _token_from_result(result, model, params)
|
||||
|
|
@ -269,7 +269,7 @@ async def aget_anthropic_wif_token(
|
|||
params: Final = resolve_anthropic_wif_params(litellm_params)
|
||||
if params is None:
|
||||
return None
|
||||
exchange_base: Final = _token_exchange_base(api_base)
|
||||
exchange_base: Final = resolve_anthropic_base(api_base)
|
||||
_raise_if_exchange_host_untrusted(exchange_base, model)
|
||||
result: Final = await engine.aget_token(build_anthropic_wif_spec(params, exchange_base))
|
||||
return _token_from_result(result, model, params)
|
||||
|
|
@ -283,11 +283,12 @@ def _token_from_result(result: ExchangeResult, model: str, params: AnthropicWifP
|
|||
_raise_anthropic_wif_error(result, model=model, workspace_id_set=params.workspace_id is not None)
|
||||
|
||||
|
||||
def _token_exchange_base(api_base: str | None) -> str:
|
||||
"""Exchange base for any caller-supplied form of the deployment base: trailing
|
||||
slashes and chat-appended ``/v1/messages`` suffixes stripped, so every tier
|
||||
derives the same token URL (and cache key) for the same deployment."""
|
||||
return anthropic_base_without_chat_suffix(api_base if api_base is not None else _resolve_default_api_base())
|
||||
def resolve_anthropic_base(api_base: str | None) -> str:
|
||||
"""The base every Anthropic tier derives its URLs from: the deployment api_base when set,
|
||||
else ``ANTHROPIC_API_BASE`` / ``ANTHROPIC_BASE_URL``, else Anthropic's host, with trailing
|
||||
slashes and chat-appended ``/v1/messages`` suffixes stripped, so the token URL, the cache key
|
||||
and the count-tokens URL all agree for the same deployment."""
|
||||
return anthropic_base_without_chat_suffix(api_base or _resolve_default_api_base())
|
||||
|
||||
|
||||
def _trusted_exchange_hosts() -> frozenset[str]:
|
||||
|
|
|
|||
|
|
@ -99,16 +99,41 @@ def test_transform_no_system_no_tools():
|
|||
("api_base", "expected"),
|
||||
[
|
||||
(None, "https://api.anthropic.com/v1/messages/count_tokens"),
|
||||
("", "https://api.anthropic.com/v1/messages/count_tokens"),
|
||||
("https://gateway.example", "https://gateway.example/v1/messages/count_tokens"),
|
||||
("https://gateway.example/", "https://gateway.example/v1/messages/count_tokens"),
|
||||
("https://gateway.example/v1", "https://gateway.example/v1/messages/count_tokens"),
|
||||
("https://gateway.example/anthropic/v1/messages", "https://gateway.example/anthropic/v1/messages/count_tokens"),
|
||||
],
|
||||
)
|
||||
def test_endpoint_appends_count_tokens_path_to_deployment_api_base(api_base, expected):
|
||||
def test_endpoint_appends_count_tokens_path_to_deployment_api_base(api_base, expected, monkeypatch):
|
||||
monkeypatch.delenv("ANTHROPIC_API_BASE", raising=False)
|
||||
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
|
||||
assert AnthropicCountTokensConfig().get_anthropic_count_tokens_endpoint(api_base) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("env_name", ["ANTHROPIC_API_BASE", "ANTHROPIC_BASE_URL"])
|
||||
@pytest.mark.parametrize("api_base", [None, ""])
|
||||
def test_endpoint_without_deployment_api_base_follows_env_base(env_name, api_base, monkeypatch):
|
||||
"""Chat and the federated exchange resolve an unset deployment base through the environment,
|
||||
so an env-only gateway must receive the count too, never Anthropic's public host."""
|
||||
monkeypatch.delenv("ANTHROPIC_API_BASE", raising=False)
|
||||
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
|
||||
monkeypatch.setenv(env_name, "https://env-gateway.example/v1/messages/")
|
||||
assert (
|
||||
AnthropicCountTokensConfig().get_anthropic_count_tokens_endpoint(api_base)
|
||||
== "https://env-gateway.example/v1/messages/count_tokens"
|
||||
)
|
||||
|
||||
|
||||
def test_endpoint_prefers_deployment_api_base_over_env_base(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_BASE", "https://env-gateway.example")
|
||||
assert (
|
||||
AnthropicCountTokensConfig().get_anthropic_count_tokens_endpoint("https://gateway.example/v1")
|
||||
== "https://gateway.example/v1/messages/count_tokens"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def httpx_transport_clients(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
|
|
|
|||
|
|
@ -303,6 +303,11 @@ class TestBaseUrlDerivation:
|
|||
monkeypatch.setenv("ANTHROPIC_API_BASE", "https://env.example.com")
|
||||
assert self._mint(None, monkeypatch) == "https://env.example.com/v1/oauth/token"
|
||||
|
||||
def test_empty_api_base_falls_back_like_unset(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Chat treats an empty deployment api_base as unset; the exchange must not refuse host ''."""
|
||||
monkeypatch.setenv("ANTHROPIC_API_BASE", "https://env.example.com")
|
||||
assert self._mint("", monkeypatch) == "https://env.example.com/v1/oauth/token"
|
||||
|
||||
def test_env_base_url(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://base.example.com")
|
||||
assert self._mint(None, monkeypatch) == "https://base.example.com/v1/oauth/token"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue