fix(proxy): raise on provider count_tokens errors instead of falling back to tiktoken (#46121)

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
This commit is contained in:
Krrish Dholakia 2026-10-13 12:08:01 -07:00 • committed by GitHub
parent 784581bace
commit 719c44db3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 110 additions and 113 deletions

View file

@ -14116,19 +14116,12 @@ async def _try_provider_token_count(
code=status_code,
)
if result is not None and result.error is True:
if litellm.disable_token_counter is True:
raise ProxyException(
message=result.error_message or "Token counting failed",
type="token_counting_error",
param="model",
code=result.status_code or 500,
)
verbose_proxy_logger.warning(
"Provider token counting failed (%s): %s. Falling back to local tokenizer.",
result.status_code,
result.error_message,
raise ProxyException(
message=result.error_message or "Token counting failed",
type="token_counting_error",
param="model",
code=result.status_code or 500,
)
return None
return result

View file

@ -398,24 +398,24 @@ def test_messages_count_tokens_keeps_a_mid_conversation_system_in_place(
)
def test_messages_count_tokens_falls_back_locally_when_every_message_is_system(
def test_messages_count_tokens_surfaces_the_peer_error_when_every_message_is_system(
deployment: _Deployment, gateway: Gateway
) -> None:
with _listening(deployment) as peer:
body: Final[dict[str, JsonValue]] = {"model": deployment.model, "messages": [LEADING]}
local: Final = _local_count(gateway, body)
response: Final = _count(gateway, body)
assert _payload(response) == {"input_tokens": local}, response.text
assert response.status_code == 400, response.text
assert "input_tokens" not in response.text, response.text
assert _bodies(peer, deployment.provider) == ({"model": _MODEL, "messages": [], "system": [LIFTED]},)
def test_messages_count_tokens_leaves_the_request_untouched_for_a_non_text_system(
def test_messages_count_tokens_surfaces_the_peer_error_for_a_non_text_system(
deployment: _Deployment, gateway: Gateway
) -> None:
with _listening(deployment) as peer:
local: Final = _local_count(gateway, count_request(deployment.model, STRING_CASE))
response: Final = _count(gateway, {**count_request(deployment.model, STRING_CASE), "system": 5})
assert _payload(response) == {"input_tokens": local}, response.text
assert response.status_code == 400, response.text
assert "input_tokens" not in response.text, response.text
assert _bodies(peer, deployment.provider) == ({"model": _MODEL, "messages": [LEADING, USER], "system": 5},)
@ -428,14 +428,14 @@ def test_messages_count_tokens_repeated_request_lifts_each_time(deployment: _Dep
assert _bodies(peer, deployment.provider) == (_STRING_BODY,) * 2
def test_messages_count_tokens_answers_a_leading_system_without_content_before_any_peer_call(
def test_messages_count_tokens_surfaces_the_counting_error_for_a_leading_system_without_content(
deployment: _Deployment, gateway: Gateway
) -> None:
with _listening(deployment) as peer:
body: Final[dict[str, JsonValue]] = {"model": deployment.model, "messages": [{"role": "system"}, USER]}
local: Final = _local_count(gateway, body)
response: Final = _count(gateway, body)
assert _payload(response) == {"input_tokens": local}, response.text
assert 500 <= response.status_code < 600, response.text
assert "input_tokens" not in response.text, response.text
assert peer.drain() == ()
follow_up: Final = _count(gateway, count_request(deployment.model, STRING_CASE))
assert _payload(follow_up) == {"input_tokens": _COUNT}, follow_up.text
@ -458,14 +458,14 @@ def test_messages_count_tokens_duplicate_messages_key_lifts_the_last_value(
@pytest.mark.parametrize("status", [400, 403, 404, 500, 503])
def test_messages_count_tokens_falls_back_locally_when_the_peer_rejects_the_lifted_body(
def test_messages_count_tokens_surfaces_the_peer_status_when_the_peer_rejects_the_lifted_body(
deployment: _Deployment, gateway: Gateway, status: int
) -> None:
with _listening(deployment, _rejecting(status)) as peer:
body: Final = count_request(deployment.model, STRING_CASE)
local: Final = _local_count(gateway, body)
response: Final = _count(gateway, body)
assert _payload(response) == {"input_tokens": local}, response.text
assert response.status_code == status, response.text
assert "input_tokens" not in response.text, response.text
assert _bodies(peer, deployment.provider) == (_STRING_BODY,)
@ -478,14 +478,13 @@ def test_messages_count_tokens_unauthenticated_request_never_reaches_the_peer(
assert peer.drain() == ()
def test_peer_outage_between_concurrent_waves_falls_back_then_recovers(
def test_peer_outage_between_concurrent_waves_errors_then_recovers(
deployment: _Deployment, gateway: Gateway
) -> None:
with ExitStack() as stack:
clients: Final = _clients(stack, str(gateway.client.base_url), 8)
pool: Final = stack.enter_context(ThreadPoolExecutor(max_workers=len(clients)))
body: Final = count_request(deployment.model, STRING_CASE)
local: Final = _local_count(gateway, body)
def count(client: httpx.Client) -> tuple[int, JsonValue]:
return _counted_on(client, gateway.key, body)
@ -493,7 +492,9 @@ def test_peer_outage_between_concurrent_waves_falls_back_then_recovers(
with _listening(deployment) as peer:
assert tuple(pool.map(count, clients)) == ((200, _COUNT),) * len(clients)
assert _bodies(peer, deployment.provider) == (_STRING_BODY,) * len(clients)
assert tuple(pool.map(count, clients)) == ((200, local),) * len(clients)
outage: Final = tuple(pool.map(count, clients))
for status, tokens in outage:
assert 500 <= status < 600 and tokens is None, outage
with _listening(deployment) as revived:
assert tuple(pool.map(count, clients)) == ((200, _COUNT),) * len(clients)
assert _bodies(revived, deployment.provider) == (_STRING_BODY,) * len(clients)

View file

@ -522,7 +522,7 @@ def test_model_the_runtime_counts_never_reaches_mantle(counting_proxy: OwnedProx
assert mantle.drain() == ()
def test_non_claude_model_the_runtime_cannot_count_falls_back_locally_without_mantle(
def test_non_claude_model_the_runtime_cannot_count_surfaces_the_error_without_mantle(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -533,7 +533,8 @@ def test_non_claude_model_the_runtime_cannot_count_falls_back_locally_without_ma
):
model: Final = _deployment(scenario, runtime.url, _NOVA)
response: Final = _count(gateway, _bare(model))
assert _payload(response) == {"input_tokens": _local_count(gateway, _bare(model))}, response.text
assert response.status_code == 400, response.text
assert _UNSUPPORTED in response.text and "input_tokens" not in response.text, response.text
assert _runtime_count_targets(runtime) == (f"/model/{_NOVA}/count-tokens",)
assert mantle.drain() == ()
@ -551,17 +552,19 @@ def test_runtime_403_on_a_claude_model_never_reaches_mantle(counting_proxy: Owne
"messages": [{"role": "user", "content": "status=403 Count this message"}],
}
response: Final = _count(gateway, body)
assert _payload(response) == {"input_tokens": _local_count(gateway, body)}, response.text
assert response.status_code == 403, response.text
assert "input_tokens" not in response.text, response.text
assert len(_runtime_count_targets(runtime)) == 1
assert mantle.drain() == ()
def test_unreachable_runtime_falls_back_locally_without_mantle(counting_proxy: OwnedProxy, mantle_port: int) -> None:
def test_unreachable_runtime_surfaces_the_error_without_mantle(counting_proxy: OwnedProxy, mantle_port: int) -> None:
gateway: Final = counting_proxy.gateway
with wire_server(_mantle(), port=mantle_port) as mantle, gateway.scenario() as scenario:
model: Final = _deployment(scenario, _closed_port_url())
response: Final = _count(gateway, _full(model))
assert _payload(response) == {"input_tokens": _local_count(gateway, _full(model))}, response.text
assert 500 <= response.status_code < 600, response.text
assert "input_tokens" not in response.text, response.text
assert mantle.drain() == ()
@ -770,7 +773,7 @@ def test_messages_count_tokens_keeps_a_mid_conversation_system_in_place_for_mant
)
def test_messages_count_tokens_falls_back_locally_when_every_message_is_system_for_mantle(
def test_messages_count_tokens_surfaces_the_mantle_error_when_every_message_is_system(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -781,14 +784,14 @@ def test_messages_count_tokens_falls_back_locally_when_every_message_is_system_f
):
model: Final = _deployment(scenario, runtime.url)
body: Final[dict[str, JsonValue]] = {"model": model, "messages": [LEADING]}
local: Final = _local_count(gateway, body)
response: Final = _count(gateway, body)
assert _payload(response) == {"input_tokens": local}, response.text
assert response.status_code == 400, response.text
assert "input_tokens" not in response.text, response.text
assert len(_runtime_count_targets(runtime)) == 1
assert _mantle_bodies(mantle) == (_mantle_body(messages=[], system=[LIFTED]),)
def test_messages_count_tokens_leaves_a_leading_system_in_place_beside_a_non_text_system_for_mantle(
def test_messages_count_tokens_surfaces_the_mantle_error_beside_a_non_text_system(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -798,9 +801,9 @@ def test_messages_count_tokens_leaves_a_leading_system_in_place_beside_a_non_tex
gateway.scenario() as scenario,
):
model: Final = _deployment(scenario, runtime.url)
local: Final = _local_count(gateway, count_request(model, STRING_CASE))
response: Final = _count(gateway, {**count_request(model, STRING_CASE), "system": 5})
assert _payload(response) == {"input_tokens": local}, response.text
assert response.status_code == 400, response.text
assert "input_tokens" not in response.text, response.text
assert len(_runtime_count_targets(runtime)) == 1
assert _mantle_bodies(mantle) == (_mantle_body(messages=[LEADING, USER], system=5),)
@ -821,7 +824,7 @@ def test_messages_count_tokens_repeated_request_lifts_the_leading_system_each_ti
assert _mantle_bodies(mantle) == (_MANTLE_LIFTED,) * 2
def test_messages_count_tokens_answers_a_leading_system_without_content_before_any_mantle_call(
def test_messages_count_tokens_surfaces_the_counting_error_for_a_leading_system_without_content(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -832,9 +835,9 @@ def test_messages_count_tokens_answers_a_leading_system_without_content_before_a
):
model: Final = _deployment(scenario, runtime.url)
body: Final[dict[str, JsonValue]] = {"model": model, "messages": [{"role": "system"}, USER]}
local: Final = _local_count(gateway, body)
response: Final = _count(gateway, body)
assert _payload(response) == {"input_tokens": local}, response.text
assert 500 <= response.status_code < 600, response.text
assert "input_tokens" not in response.text, response.text
assert mantle.drain() == ()
follow_up: Final = _count(gateway, count_request(model, STRING_CASE))
assert _payload(follow_up) == {"input_tokens": _MANTLE_COUNT}, follow_up.text
@ -889,7 +892,7 @@ def test_disabled_token_counter_counts_a_leading_system_through_mantle(
assert len(_runtime_count_targets(runtime)) == 1
def test_mantle_outage_between_concurrent_lifted_waves_falls_back_then_recovers(
def test_mantle_outage_between_concurrent_lifted_waves_errors_then_recovers(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -900,7 +903,6 @@ def test_mantle_outage_between_concurrent_lifted_waves_falls_back_then_recovers(
scenario: Final = stack.enter_context(gateway.scenario())
model: Final = _deployment(scenario, runtime.url)
body: Final = count_request(model, STRING_CASE)
local: Final = _local_count(gateway, body)
def count(client: httpx.Client) -> tuple[int, JsonValue]:
return _counted_on(client, gateway.key, body)
@ -908,7 +910,9 @@ def test_mantle_outage_between_concurrent_lifted_waves_falls_back_then_recovers(
with wire_server(_mantle(_strict), port=mantle_port) as mantle:
assert tuple(pool.map(count, clients)) == ((200, _MANTLE_COUNT),) * len(clients)
assert _mantle_bodies(mantle) == (_MANTLE_LIFTED,) * len(clients)
assert tuple(pool.map(count, clients)) == ((200, local),) * len(clients)
outage: Final = tuple(pool.map(count, clients))
for status, tokens in outage:
assert 500 <= status < 600 and tokens is None, outage
with wire_server(_mantle(_strict), port=mantle_port) as revived:
assert tuple(pool.map(count, clients)) == ((200, _MANTLE_COUNT),) * len(clients)
assert _mantle_bodies(revived) == (_MANTLE_LIFTED,) * len(clients)
@ -916,7 +920,7 @@ def test_mantle_outage_between_concurrent_lifted_waves_falls_back_then_recovers(
@pytest.mark.parametrize("status", [400, 403, 404, 500, 503])
def test_messages_count_tokens_falls_back_locally_when_mantle_errors(
def test_messages_count_tokens_surfaces_the_mantle_status_when_mantle_errors(
counting_proxy: OwnedProxy, mantle_port: int, status: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -927,7 +931,8 @@ def test_messages_count_tokens_falls_back_locally_when_mantle_errors(
):
model: Final = _deployment(scenario, runtime.url)
response: Final = _count(gateway, _full(model))
assert _payload(response) == {"input_tokens": _local_count(gateway, _full(model))}, response.text
assert response.status_code == status, response.text
assert "input_tokens" not in response.text, response.text
assert len(_runtime_count_targets(runtime)) == 1
assert _mantle_bodies(mantle) == (_MANTLE_FULL,)
@ -937,7 +942,7 @@ def test_messages_count_tokens_falls_back_locally_when_mantle_errors(
[b'{"inputTokens": 7}', b"not json at all", b"{}"],
ids=["runtime_key", "not_json", "empty_object"],
)
def test_messages_count_tokens_falls_back_locally_when_mantle_answers_without_input_tokens(
def test_messages_count_tokens_errors_when_mantle_answers_without_input_tokens(
counting_proxy: OwnedProxy, mantle_port: int, body: bytes
) -> None:
gateway: Final = counting_proxy.gateway
@ -948,21 +953,22 @@ def test_messages_count_tokens_falls_back_locally_when_mantle_answers_without_in
):
model: Final = _deployment(scenario, runtime.url)
response: Final = _count(gateway, _bare(model))
assert _payload(response) == {"input_tokens": _local_count(gateway, _bare(model))}, response.text
assert 500 <= response.status_code < 600, response.text
assert len(_runtime_count_targets(runtime)) == 1
assert _mantle_bodies(mantle) == (_MANTLE_BARE,)
def test_messages_count_tokens_falls_back_locally_when_mantle_is_unreachable(counting_proxy: OwnedProxy) -> None:
def test_messages_count_tokens_errors_when_mantle_is_unreachable(counting_proxy: OwnedProxy) -> None:
gateway: Final = counting_proxy.gateway
with wire_server(_runtime) as runtime, gateway.scenario() as scenario:
model: Final = _deployment(scenario, runtime.url)
response: Final = _count(gateway, _full(model))
assert _payload(response) == {"input_tokens": _local_count(gateway, _full(model))}, response.text
assert 500 <= response.status_code < 600, response.text
assert "input_tokens" not in response.text, response.text
assert len(_runtime_count_targets(runtime)) == 1
def test_messages_count_tokens_falls_back_locally_when_mantle_never_answers(
def test_messages_count_tokens_errors_when_mantle_never_answers(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -983,18 +989,18 @@ def test_messages_count_tokens_falls_back_locally_when_mantle_never_answers(
scenario: Final = stack.enter_context(gateway.scenario())
model: Final = _deployment(scenario, runtime.url)
body: Final = _full(model)
local: Final = _local_count(gateway, body)
future: Final = pool.submit(_counted_on, client, gateway.key, body)
eventually(held.qsize, lambda size: size == 1, seconds=30)
assert gateway.request("GET", "/health/liveliness").status_code == 200
assert _local_count(gateway, body) == local
assert _local_count(gateway, body) > 0
assert not future.done()
assert future.result(timeout=120) == (200, local)
status, tokens = future.result(timeout=120)
assert 500 <= status < 600 and tokens is None, (status, tokens)
assert len(_runtime_count_targets(runtime)) == 1
assert len(mantle.drain()) == 1
def test_messages_count_tokens_falls_back_locally_when_mantle_rejects_a_non_text_system(
def test_messages_count_tokens_surfaces_the_mantle_error_for_a_non_text_system(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -1005,7 +1011,8 @@ def test_messages_count_tokens_falls_back_locally_when_mantle_rejects_a_non_text
):
model: Final = _deployment(scenario, runtime.url)
response: Final = _count(gateway, {**_bare(model), "system": 5})
assert _payload(response) == {"input_tokens": _local_count(gateway, _bare(model))}, response.text
assert response.status_code == 400, response.text
assert "input_tokens" not in response.text, response.text
assert len(_runtime_count_targets(runtime)) == 1
assert _mantle_bodies(mantle) == (_mantle_body(system=5),)
@ -1174,7 +1181,7 @@ def test_disabled_token_counter_surfaces_the_mantle_error_instead_of_counting_lo
assert len(_runtime_count_targets(runtime)) == 4
def test_mantle_outage_between_concurrent_waves_falls_back_then_recovers(
def test_mantle_outage_between_concurrent_waves_errors_then_recovers(
counting_proxy: OwnedProxy, mantle_port: int
) -> None:
gateway: Final = counting_proxy.gateway
@ -1185,7 +1192,6 @@ def test_mantle_outage_between_concurrent_waves_falls_back_then_recovers(
scenario: Final = stack.enter_context(gateway.scenario())
model: Final = _deployment(scenario, runtime.url)
body: Final = _full(model)
local: Final = _local_count(gateway, body)
def generate_then_count(client: httpx.Client) -> tuple[int, int, JsonValue]:
return _generated_then_counted(client, gateway.key, model, body)
@ -1197,7 +1203,8 @@ def test_mantle_outage_between_concurrent_waves_falls_back_then_recovers(
assert tuple(pool.map(generate_then_count, clients)) == ((200, 200, _MANTLE_COUNT),) * len(clients)
assert _mantle_bodies(mantle) == (_MANTLE_FULL,) * len(clients)
outage: Final = tuple(pool.map(count_only, clients))
assert outage == ((200, local),) * len(clients)
for status, tokens in outage:
assert 500 <= status < 600 and tokens is None, outage
with wire_server(_mantle(), port=mantle_port) as revived:
assert tuple(pool.map(generate_then_count, clients)) == ((200, 200, _MANTLE_COUNT),) * len(clients)
assert _mantle_bodies(revived) == (_MANTLE_FULL,) * len(clients)

View file

@ -344,15 +344,14 @@ def test_utils_token_counter_local_mode_never_calls_the_peer(gateway: Gateway) -
assert wire.drain() == ()
def test_utils_token_counter_falls_back_locally_when_peer_rejects_openai_tools(gateway: Gateway) -> None:
def test_utils_token_counter_surfaces_the_peer_error_for_openai_tools(gateway: Gateway) -> None:
with wire_server(_peer(_strict)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(gateway, scenario, wire.url)
body: Final = {**_bare(model), "tools": _OPENAI_TOOLS}
response: Final = gateway.request("POST", "/utils/token_counter", body, params={"call_endpoint": "true"})
payload: Final = _payload(response)
assert response.status_code == 400, response.text
assert "total_tokens" not in response.text, response.text
assert _counted_bodies(wire) == ({**_PEER_BARE, "tools": _OPENAI_TOOLS},)
assert payload["total_tokens"] == _local_count(gateway, body), response.text
assert payload["tokenizer_type"] != "vertex_ai_partner_models", response.text
def test_responses_input_tokens_counts_through_the_partner_peer(gateway: Gateway) -> None:
@ -365,7 +364,7 @@ def test_responses_input_tokens_counts_through_the_partner_peer(gateway: Gateway
assert _counted_bodies(wire) == (_PEER_BARE,)
def test_responses_input_tokens_falls_back_locally_when_peer_rejects(gateway: Gateway) -> None:
def test_responses_input_tokens_surfaces_the_peer_error(gateway: Gateway) -> None:
with wire_server(_peer(_strict)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(gateway, scenario, wire.url)
response: Final = gateway.request(
@ -373,11 +372,10 @@ def test_responses_input_tokens_falls_back_locally_when_peer_rejects(gateway: Ga
"/v1/responses/input_tokens",
{"model": model, "input": "Count this message", "instructions": "Be terse", "tools": _RESPONSES_TOOLS},
)
payload: Final = _payload(response)
assert response.status_code == 400, response.text
assert "input_tokens" not in response.text, response.text
(sent,) = _counted_bodies(wire)
assert sent["tools"] == _RESPONSES_TOOLS, sent
local: Final = _local_count(gateway, {"model": model, "messages": sent["messages"], "tools": _RESPONSES_TOOLS})
assert payload == {"object": "response.input_tokens", "input_tokens": local}, response.text
def test_gemini_count_tokens_route_reaches_the_partner_peer(gateway: Gateway) -> None:
@ -388,26 +386,26 @@ def test_gemini_count_tokens_route_reaches_the_partner_peer(gateway: Gateway) ->
assert _counted_bodies(wire) == ({"model": _BACKEND, "messages": _GEMINI_MESSAGES},)
def test_gemini_count_tokens_route_falls_back_locally_when_peer_rejects(gateway: Gateway) -> None:
def test_gemini_count_tokens_route_surfaces_the_peer_error(gateway: Gateway) -> None:
with wire_server(_peer(_rejecting(400))) as wire, gateway.scenario() as scenario:
model: Final = _deployment(gateway, scenario, wire.url)
response: Final = gateway.request("POST", f"/v1beta/models/{model}:countTokens", _GEMINI_BODY)
payload: Final = _payload(response)
assert response.status_code == 400, response.text
assert "totalTokens" not in response.text, response.text
assert _counted_bodies(wire) == ({"model": _BACKEND, "messages": _GEMINI_MESSAGES},)
assert payload["totalTokens"] == _local_count(gateway, {"model": model, "messages": _GEMINI_MESSAGES})
@pytest.mark.parametrize("status", [400, 500, 503])
def test_messages_count_tokens_falls_back_locally_when_peer_errors(gateway: Gateway, status: int) -> None:
def test_messages_count_tokens_surfaces_the_peer_error_when_peer_errors(gateway: Gateway, status: int) -> None:
with wire_server(_peer(_rejecting(status))) as wire, gateway.scenario() as scenario:
model: Final = _deployment(gateway, scenario, wire.url)
response: Final = _count(gateway, _full(model))
payload: Final = _payload(response)
assert response.status_code == status, response.text
assert "input_tokens" not in response.text, response.text
assert _counted_bodies(wire) == (_PEER_FULL,)
assert payload == {"input_tokens": _local_count(gateway, _full(model))}, response.text
def test_messages_count_tokens_falls_back_locally_when_token_endpoint_rejects(gateway: Gateway) -> None:
def test_messages_count_tokens_surfaces_the_error_when_token_endpoint_rejects(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
if request.target == "/_oauth/token":
return Reply(
@ -421,17 +419,18 @@ def test_messages_count_tokens_falls_back_locally_when_token_endpoint_rejects(ga
gateway, scenario, wire.url, vertex_credentials=service_account_json(_PROJECT, wire.url)
)
response: Final = _count(gateway, _full(model))
payload: Final = _payload(response)
assert 500 <= response.status_code < 600, response.text
assert "input_tokens" not in response.text, response.text
targets: Final = frozenset(request.target for request in wire.drain())
assert targets == {"/_oauth/token"}, targets
assert payload == {"input_tokens": _local_count(gateway, _full(model))}, response.text
def test_messages_count_tokens_falls_back_locally_when_peer_is_unreachable(gateway: Gateway) -> None:
def test_messages_count_tokens_errors_when_peer_is_unreachable(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
model: Final = _deployment(gateway, scenario, _closed_port_url())
response: Final = _count(gateway, _full(model))
assert _payload(response) == {"input_tokens": _local_count(gateway, _full(model))}, response.text
assert 500 <= response.status_code < 600, response.text
assert "input_tokens" not in response.text, response.text
@pytest.mark.parametrize(
@ -476,13 +475,13 @@ def test_messages_count_tokens_leaves_null_system_and_tools_out(gateway: Gateway
assert _counted_bodies(wire) == (_PEER_BARE,)
def test_messages_count_tokens_falls_back_locally_when_peer_rejects_a_non_text_system(gateway: Gateway) -> None:
def test_messages_count_tokens_surfaces_the_peer_error_for_a_non_text_system(gateway: Gateway) -> None:
with wire_server(_peer(_strict)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(gateway, scenario, wire.url)
response: Final = _count(gateway, {**_bare(model), "system": 5})
payload: Final = _payload(response)
assert response.status_code == 400, response.text
assert "input_tokens" not in response.text, response.text
assert _counted_bodies(wire) == ({**_PEER_BARE, "system": 5},)
assert payload == {"input_tokens": _local_count(gateway, _bare(model))}, response.text
def test_messages_count_tokens_forwards_a_5kb_system_verbatim(gateway: Gateway) -> None:
@ -578,7 +577,7 @@ def test_disabled_token_counter_surfaces_provider_failures_instead_of_counting_l
assert len(_counted_bodies(wire)) == 2
def test_peer_outage_between_concurrent_waves_falls_back_then_recovers(gateway: Gateway) -> None:
def test_peer_outage_between_concurrent_waves_errors_then_recovers(gateway: Gateway) -> None:
with ExitStack() as stack:
clients: Final = _clients(stack, str(gateway.client.base_url), 8)
pool: Final = stack.enter_context(ThreadPoolExecutor(max_workers=len(clients)))
@ -587,7 +586,6 @@ def test_peer_outage_between_concurrent_waves_falls_back_then_recovers(gateway:
port: Final = int(wire.url.rsplit(":", 1)[1])
model: Final = _deployment(gateway, scenario, wire.url)
body: Final = _full(model)
local: Final = _local_count(gateway, body)
def generate_then_count(client: httpx.Client) -> tuple[int, int, JsonValue]:
return _generated_then_counted(client, gateway.key, model, body)
@ -595,7 +593,8 @@ def test_peer_outage_between_concurrent_waves_falls_back_then_recovers(gateway:
assert tuple(pool.map(generate_then_count, clients)) == ((200, 200, _PEER_COUNT),) * len(clients)
assert _counted_bodies(wire) == (_PEER_FULL,) * len(clients)
outage: Final = tuple(pool.map(lambda client: _counted_on(client, gateway.key, body), clients))
assert outage == ((200, local),) * len(clients)
for status, tokens in outage:
assert 500 <= status < 600 and tokens is None, outage
with wire_server(_peer(), port=port) as revived:
assert tuple(pool.map(generate_then_count, clients)) == ((200, 200, _PEER_COUNT),) * len(clients)
assert _counted_bodies(revived) == (_PEER_FULL,) * len(clients)

View file

@ -310,20 +310,16 @@ async def test_internal_token_counter_anthropic_provider_detection():
setattr(litellm.proxy.proxy_server, "llm_router", llm_router)
# Test with is_direct_request=False (simulating call from Anthropic endpoint)
response = await token_counter(
request=TokenCountRequest(
model="claude-test",
messages=[{"role": "user", "content": "hello"}],
),
call_endpoint=True,
)
print("Anthropic provider test response:", response)
# Verify response structure
assert response.request_model == "claude-test"
assert response.model_used == "claude-3-sonnet-20240229"
assert response.total_tokens > 0
# The provider counter is engaged: its API error surfaces instead of a local count
with pytest.raises(ProxyException) as exc_info:
await token_counter(
request=TokenCountRequest(
model="claude-test",
messages=[{"role": "user", "content": "hello"}],
),
call_endpoint=True,
)
assert exc_info.value.type == "token_counting_error"
# Test with non-Anthropic provider
llm_router = Router(
@ -1018,10 +1014,10 @@ async def test_proxy_token_counter_error_raises_exception_when_disabled():
@pytest.mark.asyncio
async def test_proxy_token_counter_error_falls_back_when_enabled():
async def test_proxy_token_counter_error_raises_when_enabled():
"""
Test that proxy token_counter falls back to local tokenizer when disable_token_counter=False
and provider returns an error response.
Test that proxy token_counter raises ProxyException when disable_token_counter=False
and the provider returns an error response, never falling back to the local tokenizer.
"""
# Create error response
error_response = TokenCountResponse(
@ -1069,19 +1065,20 @@ async def test_proxy_token_counter_error_falls_back_when_enabled():
mock_get_provider_token_counter
)
# Should not raise, should fall back to local tokenizer
result = await token_counter(
request=TokenCountRequest(
model="claude-bedrock",
messages=[{"role": "user", "content": "hello"}],
),
call_endpoint=True,
)
with patch("litellm.token_counter") as mock_local_counter:
with pytest.raises(ProxyException) as exc_info:
await token_counter(
request=TokenCountRequest(
model="claude-bedrock",
messages=[{"role": "user", "content": "hello"}],
),
call_endpoint=True,
)
# Should have used the fallback tokenizer
assert result.error is False
assert result.total_tokens > 0
assert result.tokenizer_type != "bedrock_api"
assert exc_info.value.code == "429"
assert "Rate limit exceeded" in exc_info.value.message
assert exc_info.value.type == "token_counting_error"
mock_local_counter.assert_not_called()
finally:
litellm.disable_token_counter = original_disable
litellm.proxy.proxy_server._get_provider_token_counter = (