From 3520696cacc71988160548e2e885567ac55e7f49 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:34:31 -0700 Subject: [PATCH] fix(bedrock): keep Mantle token counts on the deployment's own endpoint The Mantle count endpoint ignored the deployment's api_base and aws_bedrock_runtime_endpoint, so a bedrock/mantle deployment counted on BEDROCK_MANTLE_API_BASE or the public host, and a runtime deployment pinned to a private endpoint retried its counts on the public Mantle host. A bedrock/mantle deployment now counts where its inference goes. Other deployments retry on BEDROCK_MANTLE_API_BASE when it is set, on the public host only when they use the default runtime endpoint, and otherwise keep the runtime's own answer with no Mantle retry. --- litellm/llms/bedrock/common_utils.py | 4 +- .../count_tokens/bedrock_token_counter.py | 19 ++- .../bedrock/count_tokens/mantle_handler.py | 58 ++++++- ...est_bedrock_mantle_count_tokens_handler.py | 56 ++++++- .../test_bedrock_token_counter.py | 144 +++++++++++++++++- 5 files changed, 268 insertions(+), 13 deletions(-) diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 4175479f068..576a1b34cfc 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -1323,7 +1323,7 @@ MANTLE_MESSAGES_PATH: Final = "/anthropic/v1/messages" _MANTLE_OPENAI_BASE_SUFFIXES: Final = ("/openai/v1", "/v1") -def _mantle_api_base_from_env() -> str | None: +def mantle_api_base_from_env() -> str | None: env_base: Final = get_secret_str("BEDROCK_MANTLE_API_BASE") if env_base is None: return None @@ -1383,7 +1383,7 @@ def build_mantle_messages_url( shared with the OpenAI-surface ``bedrock_mantle/*`` routes, which need it to carry their ``/v1`` or ``/openai/v1`` base, so that suffix is dropped first. """ - override: Final = api_base or aws_bedrock_runtime_endpoint or _mantle_api_base_from_env() + override: Final = api_base or aws_bedrock_runtime_endpoint or mantle_api_base_from_env() if override: return ensure_api_route(override, MANTLE_MESSAGES_PATH) return f"https://bedrock-mantle.{region}.api.aws{MANTLE_MESSAGES_PATH}" diff --git a/litellm/llms/bedrock/count_tokens/bedrock_token_counter.py b/litellm/llms/bedrock/count_tokens/bedrock_token_counter.py index 26bc5904e06..38512d99631 100644 --- a/litellm/llms/bedrock/count_tokens/bedrock_token_counter.py +++ b/litellm/llms/bedrock/count_tokens/bedrock_token_counter.py @@ -15,9 +15,13 @@ from litellm._logging import verbose_logger from litellm.llms.anthropic.chat.transformation import AnthropicConfig from litellm.llms.anthropic.count_tokens.transformation import ChatCountTranslationError, anthropic_count_fields from litellm.llms.base_llm.base_utils import BaseTokenCounter +from litellm.llms.bedrock.base_aws_llm import run_aws_signing from litellm.llms.bedrock.common_utils import BedrockError, get_bedrock_base_model from litellm.llms.bedrock.count_tokens.handler import BedrockCountTokensHandler -from litellm.llms.bedrock.count_tokens.mantle_handler import BedrockMantleCountTokensHandler +from litellm.llms.bedrock.count_tokens.mantle_handler import ( + BedrockMantleCountTokensHandler, + deployment_mantle_endpoint, +) from litellm.llms.bedrock_mantle.common_utils import is_mantle_claude_model from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler from litellm.types.utils import ( @@ -74,6 +78,10 @@ def _runtime_rejected_claude_model(outcome: CountTokensOutcome, resolved_model: ) +async def _has_mantle_endpoint(litellm_params: Mapping[str, object]) -> bool: + return await run_aws_signing(deployment_mantle_endpoint, litellm_params) is not None + + def _request_data( model: str, messages: Sequence[Mapping[str, object]], tools: Sequence[Mapping[str, object]] | None, system: object ) -> Mapping[str, object]: @@ -150,8 +158,9 @@ class BedrockTokenCounter(BaseTokenCounter): use_mantle: Final = model_to_use.startswith("bedrock_mantle/") counted_model: Final = model_to_use.removeprefix("bedrock_mantle/") resolved_model: Final = get_bedrock_base_model(counted_model) - primary_tokenizer: Final = MANTLE_TOKENIZER_TYPE if use_mantle else RUNTIME_TOKENIZER_TYPE litellm_params: Final = (deployment or {}).get("litellm_params", {}) + counts_on_mantle: Final = use_mantle and await _has_mantle_endpoint(litellm_params) + primary_tokenizer: Final = MANTLE_TOKENIZER_TYPE if counts_on_mantle else RUNTIME_TOKENIZER_TYPE translated: Final = ( await anthropic_count_fields( counted_model, @@ -178,7 +187,7 @@ class BedrockTokenCounter(BaseTokenCounter): await self._count_with( self._mantle_handler, primary_tokenizer, mantle_request, litellm_params, resolved_model ) - if use_mantle + if counts_on_mantle else await self._count_with( self._runtime_handler, primary_tokenizer, runtime_request, litellm_params, resolved_model ) @@ -187,7 +196,9 @@ class BedrockTokenCounter(BaseTokenCounter): await self._count_with( self._mantle_handler, MANTLE_TOKENIZER_TYPE, mantle_request, litellm_params, resolved_model ) - if not use_mantle and _runtime_rejected_claude_model(primary, resolved_model) + if not use_mantle + and _runtime_rejected_claude_model(primary, resolved_model) + and await _has_mantle_endpoint(litellm_params) else primary ) match outcome: diff --git a/litellm/llms/bedrock/count_tokens/mantle_handler.py b/litellm/llms/bedrock/count_tokens/mantle_handler.py index 5cd26235ba2..ad82604815c 100644 --- a/litellm/llms/bedrock/count_tokens/mantle_handler.py +++ b/litellm/llms/bedrock/count_tokens/mantle_handler.py @@ -1,4 +1,5 @@ from collections.abc import Mapping +from dataclasses import dataclass from typing import Final import httpx @@ -11,11 +12,16 @@ from litellm.litellm_core_utils.url_utils import join_api_base from litellm.llms.anthropic.common_utils import tools_without_custom_field from litellm.llms.anthropic.count_tokens.transformation import COUNT_TOKEN_OPTION_NAMES, AnthropicCountTokensConfig from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM, run_aws_signing -from litellm.llms.bedrock.common_utils import BedrockError, build_mantle_messages_url +from litellm.llms.bedrock.common_utils import BedrockError, build_mantle_messages_url, mantle_api_base_from_env from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_async_httpx_client +from litellm.secret_managers.main import get_secret_str MANTLE_COUNT_TOKENS_SUFFIX: Final = "/count_tokens" MANTLE_ANTHROPIC_VERSION: Final = "2023-06-01" +NO_MANTLE_ENDPOINT_MESSAGE: Final = ( + "This deployment pins its own bedrock-runtime endpoint, so its prompt is not counted on the public " + "bedrock-mantle host. Set BEDROCK_MANTLE_API_BASE to the bedrock-mantle endpoint it may use." +) class MantleCountTokensRequest(TypedDict): @@ -30,6 +36,39 @@ _COUNT_OPTIONS: Final[TypeAdapter[Mapping[str, JsonValue]]] = TypeAdapter(Mappin _TOOLS: Final = TypeAdapter(list[dict[str, JsonValue]]) +@dataclass(frozen=True, slots=True) +class MantleEndpointSettings: + api_base: str | None + aws_bedrock_runtime_endpoint: str | None + + +_OPERATOR_MANTLE_ENDPOINT: Final = MantleEndpointSettings(api_base=None, aws_bedrock_runtime_endpoint=None) + + +def _endpoint_setting(litellm_params: Mapping[str, object], name: str) -> str | None: + value: Final = litellm_params.get(name) + return value if isinstance(value, str) and value else None + + +def _deploys_on_mantle(litellm_params: Mapping[str, object]) -> bool: + from litellm.llms.bedrock.common_utils import BedrockModelInfo + + model: Final = litellm_params.get("model") + return isinstance(model, str) and BedrockModelInfo.get_bedrock_route(model) == "mantle" + + +def deployment_mantle_endpoint(litellm_params: Mapping[str, object]) -> MantleEndpointSettings | None: + api_base: Final = _endpoint_setting(litellm_params, "api_base") + runtime_endpoint: Final = _endpoint_setting(litellm_params, "aws_bedrock_runtime_endpoint") + if _deploys_on_mantle(litellm_params): + return MantleEndpointSettings(api_base=api_base, aws_bedrock_runtime_endpoint=runtime_endpoint) + if mantle_api_base_from_env() or not ( + api_base or runtime_endpoint or get_secret_str("AWS_BEDROCK_RUNTIME_ENDPOINT") + ): + return _OPERATOR_MANTLE_ENDPOINT + return None + + class BedrockMantleCountTokensHandler(BaseAWSLLM): """Counts tokens through Anthropic's count_tokens on the bedrock-mantle endpoint. @@ -42,9 +81,16 @@ class BedrockMantleCountTokensHandler(BaseAWSLLM): super().__init__() self._anthropic_config: Final = anthropic_config or AnthropicCountTokensConfig() - def get_mantle_count_tokens_endpoint(self, aws_region_name: str) -> str: + def get_mantle_count_tokens_endpoint( + self, litellm_params: Mapping[str, object], aws_region_name: str + ) -> str | None: + endpoint: Final = deployment_mantle_endpoint(litellm_params) + if endpoint is None: + return None messages_url: Final = build_mantle_messages_url( - api_base=None, aws_bedrock_runtime_endpoint=None, region=aws_region_name + api_base=endpoint.api_base, + aws_bedrock_runtime_endpoint=endpoint.aws_bedrock_runtime_endpoint, + region=aws_region_name, ) return join_api_base(messages_url, MANTLE_COUNT_TOKENS_SUFFIX) @@ -60,7 +106,11 @@ class BedrockMantleCountTokensHandler(BaseAWSLLM): aws_region_name: Final = await run_aws_signing( self.get_aws_region_name, optional_params=litellm_params, model=resolved_model, model_id=None ) - endpoint_url: Final = await run_aws_signing(self.get_mantle_count_tokens_endpoint, aws_region_name) + endpoint_url: Final = await run_aws_signing( + self.get_mantle_count_tokens_endpoint, litellm_params, aws_region_name + ) + if endpoint_url is None: + raise BedrockError(status_code=400, message=NO_MANTLE_ENDPOINT_MESSAGE) tools: Final = request.get("tools") body: Final = self._anthropic_config.transform_request_to_count_tokens( model=resolved_model, diff --git a/tests/unit/llms/bedrock/count_tokens/test_bedrock_mantle_count_tokens_handler.py b/tests/unit/llms/bedrock/count_tokens/test_bedrock_mantle_count_tokens_handler.py index 930a290098f..6710cf9a862 100644 --- a/tests/unit/llms/bedrock/count_tokens/test_bedrock_mantle_count_tokens_handler.py +++ b/tests/unit/llms/bedrock/count_tokens/test_bedrock_mantle_count_tokens_handler.py @@ -11,6 +11,8 @@ from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler from tests.unit.llms.bedrock.event_loop_probe import host_in_loop_probing_vault MANTLE_COUNT_URL: Final = "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages/count_tokens" +PRIVATE_MANTLE: Final = "https://vpce-0e1f2a3b.bedrock-mantle.us-east-1.vpce.api.aws" +OPERATOR_MANTLE: Final = "https://vpce-9f8e7d6c.bedrock-mantle.us-east-1.vpce.api.aws" LITELLM_PARAMS: Final = { "aws_access_key_id": "AKIATESTACCESSKEY", "aws_secret_access_key": "test-secret", @@ -47,6 +49,7 @@ def _client(endpoint: _MantleEndpoint) -> AsyncHTTPHandler: def _sigv4_only(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False) monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_BEDROCK_RUNTIME_ENDPOINT", raising=False) @pytest.mark.asyncio @@ -141,7 +144,9 @@ async def test_non_200_answers_raise_bedrock_error_with_mantle_status() -> None: @pytest.mark.asyncio async def test_region_and_endpoint_lookups_leave_the_event_loop_free(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("AWS_REGION_NAME", "us-east-1") - vault: Final = host_in_loop_probing_vault(monkeypatch, "AWS_REGION_NAME", "BEDROCK_MANTLE_API_BASE") + vault: Final = host_in_loop_probing_vault( + monkeypatch, "AWS_REGION_NAME", "BEDROCK_MANTLE_API_BASE", "AWS_BEDROCK_RUNTIME_ENDPOINT" + ) mantle: Final = _MantleEndpoint(200, {"input_tokens": 2177}) result = await BedrockMantleCountTokensHandler().handle_count_tokens_request( @@ -155,3 +160,52 @@ async def test_region_and_endpoint_lookups_leave_the_event_loop_free(monkeypatch assert not vault.read_on_loop.is_set() assert result == {"input_tokens": 2177} assert str(mantle.only_request().url) == MANTLE_COUNT_URL + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("endpoint", "environment"), + ( + ({"api_base": PRIVATE_MANTLE}, {}), + ({"api_base": f"{PRIVATE_MANTLE}/anthropic/v1/messages"}, {}), + ({"aws_bedrock_runtime_endpoint": PRIVATE_MANTLE}, {}), + ({"api_base": PRIVATE_MANTLE}, {"BEDROCK_MANTLE_API_BASE": OPERATOR_MANTLE}), + ), + ids=("api_base", "api_base_with_messages_path", "aws_bedrock_runtime_endpoint", "api_base_over_env"), +) +async def test_mantle_deployment_counts_on_the_endpoint_its_inference_uses( + monkeypatch: pytest.MonkeyPatch, endpoint: Mapping[str, str], environment: Mapping[str, str] +) -> None: + for name, value in environment.items(): + monkeypatch.setenv(name, value) + mantle: Final = _MantleEndpoint(200, {"input_tokens": 3}) + + await BedrockMantleCountTokensHandler().handle_count_tokens_request( + request_data=dict(REQUEST), + litellm_params={**LITELLM_PARAMS, "model": "bedrock/mantle/anthropic.claude-opus-5-5", **endpoint}, + resolved_model="anthropic.claude-opus-5-5", + client=_client(mantle), + ) + + assert str(mantle.only_request().url) == f"{PRIVATE_MANTLE}/anthropic/v1/messages/count_tokens" + + +@pytest.mark.asyncio +async def test_runtime_deployment_with_its_own_endpoint_is_never_counted_on_public_mantle() -> None: + mantle: Final = _MantleEndpoint(200, {"input_tokens": 3}) + + with pytest.raises(BedrockError) as raised: + await BedrockMantleCountTokensHandler().handle_count_tokens_request( + request_data=dict(REQUEST), + litellm_params={ + **LITELLM_PARAMS, + "model": "bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + "api_base": "https://vpce-0a1b2c3d.bedrock-runtime.us-east-1.vpce.amazonaws.com", + }, + resolved_model="anthropic.claude-sonnet-4-5-20250929-v1:0", + client=_client(mantle), + ) + + assert raised.value.status_code == 400 + assert "BEDROCK_MANTLE_API_BASE" in raised.value.message + assert mantle.requests == () diff --git a/tests/unit/llms/bedrock/count_tokens/test_bedrock_token_counter.py b/tests/unit/llms/bedrock/count_tokens/test_bedrock_token_counter.py index cec7950b859..8f90a709ac4 100644 --- a/tests/unit/llms/bedrock/count_tokens/test_bedrock_token_counter.py +++ b/tests/unit/llms/bedrock/count_tokens/test_bedrock_token_counter.py @@ -9,6 +9,7 @@ from pydantic import JsonValue, TypeAdapter import litellm from litellm.llms.bedrock.count_tokens.bedrock_token_counter import BedrockTokenCounter from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler +from tests.unit.llms.bedrock.event_loop_probe import host_in_loop_probing_vault RUNTIME_HOST: Final = "bedrock-runtime.us-east-1.amazonaws.com" MANTLE_HOST: Final = "bedrock-mantle.us-east-1.api.aws" @@ -21,23 +22,37 @@ DEPLOYMENT: Final = { } } MESSAGES: Final = [{"role": "user", "content": "The quick brown fox jumps over the lazy dog."}] +PRIVATE_RUNTIME_HOST: Final = "vpce-0a1b2c3d.bedrock-runtime.us-east-1.vpce.amazonaws.com" +PRIVATE_RUNTIME: Final = f"https://{PRIVATE_RUNTIME_HOST}" +PRIVATE_MANTLE: Final = "https://vpce-0e1f2a3b.bedrock-mantle.us-east-1.vpce.api.aws" +SONNET_4_5: Final = "us.anthropic.claude-sonnet-4-5-20250929-v1:0" +SONNET_4_5_RUNTIME_COUNT: Final = f"{PRIVATE_RUNTIME}/model/anthropic.claude-sonnet-4-5-20250929-v1%3A0/count-tokens" _JSON_BODY: Final = TypeAdapter(dict[str, JsonValue]) class _Bedrock: - def __init__(self, runtime: tuple[int, Mapping[str, object]], mantle: tuple[int, Mapping[str, object]]) -> None: + def __init__( + self, + runtime: tuple[int, Mapping[str, object]], + mantle: tuple[int, Mapping[str, object]], + runtime_host: str = RUNTIME_HOST, + ) -> None: self.runtime: Final = runtime self.mantle: Final = mantle + self.runtime_host: Final = runtime_host self.requests: tuple[httpx.Request, ...] = () def __call__(self, request: httpx.Request) -> httpx.Response: self.requests = (*self.requests, request) - status_code, body = self.runtime if request.url.host == RUNTIME_HOST else self.mantle + status_code, body = self.runtime if request.url.host == self.runtime_host else self.mantle return httpx.Response(status_code, json=dict(body), request=request) def posted_hosts(self) -> tuple[str, ...]: return tuple(request.url.host for request in self.requests) + def posted_urls(self) -> tuple[str, ...]: + return tuple(str(request.url) for request in self.requests) + def _counter(bedrock: _Bedrock) -> BedrockTokenCounter: return BedrockTokenCounter(client=AsyncHTTPHandler(transport=httpx.MockTransport(bedrock))) @@ -47,6 +62,7 @@ def _counter(bedrock: _Bedrock) -> BedrockTokenCounter: def _sigv4_only(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False) monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_BEDROCK_RUNTIME_ENDPOINT", raising=False) @pytest.mark.asyncio @@ -420,3 +436,127 @@ async def test_explicit_mantle_failure_does_not_probe_runtime() -> None: assert result.status_code == 404 assert result.tokenizer_type == "bedrock_mantle_api" assert bedrock.posted_hosts() == (MANTLE_HOST,) + + +def _pinned_runtime_deployment(endpoint: Mapping[str, str]) -> dict[str, object]: + return {"litellm_params": {**DEPLOYMENT["litellm_params"], "model": f"bedrock/{SONNET_4_5}", **endpoint}} + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("endpoint", "environment"), + ( + ({"api_base": PRIVATE_RUNTIME}, {}), + ({"aws_bedrock_runtime_endpoint": PRIVATE_RUNTIME}, {}), + ({}, {"AWS_BEDROCK_RUNTIME_ENDPOINT": PRIVATE_RUNTIME}), + ), + ids=("api_base", "aws_bedrock_runtime_endpoint", "AWS_BEDROCK_RUNTIME_ENDPOINT"), +) +async def test_pinned_runtime_endpoint_keeps_its_own_rejection_and_sends_nothing_to_mantle( + monkeypatch: pytest.MonkeyPatch, endpoint: Mapping[str, str], environment: Mapping[str, str] +) -> None: + for name, value in environment.items(): + monkeypatch.setenv(name, value) + bedrock: Final = _Bedrock( + runtime=(400, UNSUPPORTED), mantle=(200, {"input_tokens": 2177}), runtime_host=PRIVATE_RUNTIME_HOST + ) + + result: Final = await _counter(bedrock).count_tokens( + model_to_use=SONNET_4_5, + messages=MESSAGES, + contents=None, + deployment=_pinned_runtime_deployment(endpoint), + request_model="claude-sonnet-4-5", + ) + + assert result is not None + assert (result.error, result.status_code, result.tokenizer_type) == (True, 400, "bedrock_api") + assert UNSUPPORTED["message"] in (result.error_message or "") + assert bedrock.posted_urls() == (SONNET_4_5_RUNTIME_COUNT,) + + +@pytest.mark.asyncio +async def test_pinned_runtime_endpoint_retries_on_the_mantle_host_the_operator_named( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("BEDROCK_MANTLE_API_BASE", PRIVATE_MANTLE) + bedrock: Final = _Bedrock( + runtime=(400, UNSUPPORTED), mantle=(200, {"input_tokens": 2177}), runtime_host=PRIVATE_RUNTIME_HOST + ) + + result: Final = await _counter(bedrock).count_tokens( + model_to_use=SONNET_4_5, + messages=MESSAGES, + contents=None, + deployment=_pinned_runtime_deployment({"api_base": PRIVATE_RUNTIME}), + request_model="claude-sonnet-4-5", + ) + + assert result is not None + assert (result.error, result.total_tokens, result.tokenizer_type) == (False, 2177, "bedrock_mantle_api") + assert bedrock.posted_urls() == ( + SONNET_4_5_RUNTIME_COUNT, + f"{PRIVATE_MANTLE}/anthropic/v1/messages/count_tokens", + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("runtime", "expected"), + ( + ((200, {"inputTokens": 193}), (False, 193, None)), + ((400, UNSUPPORTED), (True, 0, 400)), + ), + ids=("runtime_counts", "runtime_rejects"), +) +async def test_mantle_counting_model_on_a_pinned_runtime_deployment_counts_on_that_endpoint_only( + monkeypatch: pytest.MonkeyPatch, + runtime: tuple[int, Mapping[str, object]], + expected: tuple[bool, int, int | None], +) -> None: + vault: Final = host_in_loop_probing_vault(monkeypatch, "BEDROCK_MANTLE_API_BASE", "AWS_BEDROCK_RUNTIME_ENDPOINT") + bedrock: Final = _Bedrock(runtime=runtime, mantle=(200, {"input_tokens": 271}), runtime_host=PRIVATE_RUNTIME_HOST) + + result: Final = await _counter(bedrock).count_tokens( + model_to_use="bedrock_mantle/anthropic.claude-opus-5-5", + messages=MESSAGES, + contents=None, + deployment={ + "litellm_params": { + **DEPLOYMENT["litellm_params"], + "model": "bedrock/us.anthropic.claude-opus-5-5", + "api_base": PRIVATE_RUNTIME, + } + }, + request_model="claude-opus-5-5", + ) + + assert result is not None + assert (result.error, result.total_tokens, result.status_code) == expected + assert result.tokenizer_type == "bedrock_api" + assert bedrock.posted_urls() == (f"{PRIVATE_RUNTIME}/model/anthropic.claude-opus-5-5/count-tokens",) + assert vault.read.is_set() + assert not vault.read_on_loop.is_set() + + +@pytest.mark.asyncio +async def test_mantle_deployment_counts_on_its_own_api_base() -> None: + bedrock: Final = _Bedrock(runtime=(200, {"inputTokens": 193}), mantle=(200, {"input_tokens": 271})) + + result: Final = await _counter(bedrock).count_tokens( + model_to_use="bedrock_mantle/anthropic.claude-opus-5-5", + messages=MESSAGES, + contents=None, + deployment={ + "litellm_params": { + **DEPLOYMENT["litellm_params"], + "model": "bedrock/mantle/anthropic.claude-opus-5-5", + "api_base": PRIVATE_MANTLE, + } + }, + request_model="claude-opus-5-5", + ) + + assert result is not None + assert (result.error, result.total_tokens, result.tokenizer_type) == (False, 271, "bedrock_mantle_api") + assert bedrock.posted_urls() == (f"{PRIVATE_MANTLE}/anthropic/v1/messages/count_tokens",)