diff --git a/litellm/router_python_core.py b/litellm/router_python_core.py index c0c5d74a88a..d1d75665618 100644 --- a/litellm/router_python_core.py +++ b/litellm/router_python_core.py @@ -3570,6 +3570,8 @@ class PythonCore(DeprecatedRouterMembers): else: model_client = potential_model_client + self.total_calls[model_name] += 1 + ### DEPLOYMENT-SPECIFIC PRE-CALL CHECKS ### (e.g. update rpm pre-call. Raise error, if deployment over limit) ## only run if model group given, not model id if model in self._core_model_names or not self._core_has_model_id(model): @@ -3604,6 +3606,8 @@ class PythonCore(DeprecatedRouterMembers): ): response.fetch_sync_stream() + self.success_calls[model_name] += 1 + # Wrap streaming responses so MidStreamFallbackError (raised # during iteration) triggers the Router's fallback chain. if isinstance(response, CustomStreamWrapper): @@ -3617,6 +3621,8 @@ class PythonCore(DeprecatedRouterMembers): return response except Exception as e: verbose_router_logger.info("litellm.completion(model=%s)\x1b[31m Exception %s\x1b[0m", model_name, e) + if model_name is not None: + self.fail_calls[model_name] += 1 # Set per-deployment num_retries on exception for retry logic if deployment is not None: self._set_deployment_num_retries_on_exception(e, deployment) diff --git a/tests/unit/test_router/test_router.py b/tests/unit/test_router/test_router.py index c16c1c62202..f31ed552057 100644 --- a/tests/unit/test_router/test_router.py +++ b/tests/unit/test_router/test_router.py @@ -28173,6 +28173,7 @@ class TestRouterIndexManagement: f"{'=' * 70}\n" ) + def test_model_names_is_set(self): """Verify that model_names uses a set for O(1) lookups, not a list (O(n))""" router = Router(model_list=[]) @@ -32831,3 +32832,45 @@ def test_image_routing_inspects_native_request_content_without_translation(reque original: Final = copy.deepcopy(request_kwargs) assert _candidate_ids(router, request_kwargs=request_kwargs) == ["vision"] assert request_kwargs == original + + +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. + + Before the fix, _completion() (the sync entry point that completion() + reaches through function_with_fallbacks -> async_function_with_retries) + never touched any of the three counters, so the router metrics stayed at + zero for every sync call. + """ + 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