diff --git a/litellm/router_rust/invoke/endpoints.py b/litellm/router_rust/invoke/endpoints.py index e2c621ccb7d..4be2cf7d3a8 100644 --- a/litellm/router_rust/invoke/endpoints.py +++ b/litellm/router_rust/invoke/endpoints.py @@ -358,9 +358,7 @@ PLANS: Final[Mapping[str, Plan]] = MappingProxyType( sync=True, splits_stream_timeout=True, stamp="deployment", - total_key=None, - success_key=None, - fail_key=None, + # The sync path updates the same deployment counters as ``acompletion``. named=("messages",), ), "atext_completion": Plan( diff --git a/tests/unit/test_router/core/test_gaps_l2800.py b/tests/unit/test_router/core/test_gaps_l2800.py index 78e237f0696..6ef195a384c 100644 --- a/tests/unit/test_router/core/test_gaps_l2800.py +++ b/tests/unit/test_router/core/test_gaps_l2800.py @@ -424,8 +424,17 @@ async def test_acompletion_counts_total_success_and_failed_calls_per_deployment_ assert counts == (2, 1, 1) -def test_sync_completion_does_not_count_calls(make_core: MakeCore, host: ScriptedHost) -> None: - """Oddity: only the async path updates ``total_calls``/``success_calls``/``fail_calls``.""" - core: Final = make_core(model_list=[deployment("g", "d1")]) +def test_sync_completion_counts_total_success_and_failed_calls_per_deployment_model( + make_core: MakeCore, host: ScriptedHost +) -> None: + core: Final = make_core(model_list=[deployment("g", "d1")], num_retries=0) core.completion(model="g", messages=MESSAGES) # pyright: ignore[reportAttributeAccessIssue] - assert core.total_calls["openai/d1"] == 0 # pyright: ignore[reportAttributeAccessIssue] + host.answer("openai/d1", rate_limit()) + with pytest.raises(litellm.RateLimitError): + core.completion(model="g", messages=MESSAGES) # pyright: ignore[reportAttributeAccessIssue] + counts: Final = ( + core.total_calls["openai/d1"], # pyright: ignore[reportAttributeAccessIssue] + core.success_calls["openai/d1"], # pyright: ignore[reportAttributeAccessIssue] + core.fail_calls["openai/d1"], # pyright: ignore[reportAttributeAccessIssue] + ) + assert counts == (2, 1, 1) diff --git a/tests/unit/test_router/test_router.py b/tests/unit/test_router/test_router.py index ed97e8214d0..a2c02f364e8 100644 --- a/tests/unit/test_router/test_router.py +++ b/tests/unit/test_router/test_router.py @@ -19900,3 +19900,40 @@ def test_router_warns_when_a_deployment_enforces_endpoints_without_a_list(caplog assert "Deployment 'embed-from-config' sets enforce_supported_endpoints without" in caplog.text assert "Deployment 'embed-from-api' sets enforce_supported_endpoints without" in caplog.text + + +def test_sync_completion_tracks_router_metrics(): + """ + Regression: sync router.completion() must update router.total_calls / + router.success_calls / router.fail_calls, same as router.acompletion() does. + """ + router = Router( + model_list=[ + { + "model_name": "gpt-4o", + "litellm_params": {"model": "openai/gpt-4o", "num_retries": 0}, + } + ], + num_retries=0, + ) + + ## success: total/success track the deployment model + router.completion( + model="gpt-4o", + messages=[{"role": "user", "content": "hi"}], + mock_response="Hello", + ) + assert router.total_calls["openai/gpt-4o"] == 1 + assert router.success_calls["openai/gpt-4o"] == 1 + assert router.fail_calls.get("openai/gpt-4o", 0) == 0 + + ## failure: fail tracks the deployment model + with pytest.raises(Exception): + router.completion( + model="gpt-4o", + messages=[{"role": "user", "content": "hi"}], + mock_response="litellm.InternalServerError", + ) + assert router.fail_calls["openai/gpt-4o"] == 1 + assert router.total_calls["openai/gpt-4o"] == 2 + assert router.success_calls["openai/gpt-4o"] == 1