From aa5ed0f83e8674aac693432193ff43380e82015c Mon Sep 17 00:00:00 2001 From: Mihidum Hettiyahandi <55163074+mihidumh@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:37:25 +1000 Subject: [PATCH 1/3] fix(router): don't log 'Could not identify azure model' when the deployment name resolves from the cost map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_router_model_info already falls back to resolving the azure deployment's model name against the model cost map when base_model is unset — and for deployments named after real azure models (e.g. azure/gpt-4o) that resolution returns correct max tokens and costs. The unconditional ERROR was therefore spurious for exactly the deployments that need no operator action, and on busy proxies it logs thousands of times per day per multi-deployment group. Log at debug when the fallback entry carries usable limits/costs (membership alone is not enough: Router init auto-registers every deployment name as a zeroed stub), keep the ERROR otherwise. Fixes #33172 Co-Authored-By: Claude Fable 5 --- litellm/router.py | 30 +++++++++-- tests/test_litellm/test_router.py | 89 +++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index e9d4dd0a482b..40421473f9e1 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -9154,10 +9154,32 @@ def get_router_model_info( ## SET MODEL TO 'model=' - if base_model is None + not azure if custom_llm_provider == "azure" and base_model is None: - verbose_router_logger.error( - "Could not identify azure model '%s'. Set azure 'base_model' for accurate max tokens, cost tracking, etc.- https://docs.litellm.ai/docs/proxy/cost_tracking#spend-tracking-for-azure-openai-models", - _model, - ) + # the `if model is None` fallback below resolves the deployment's + # model name against the model cost map — when the name is a known + # azure key (e.g. deployment model "azure/gpt-4o"), that resolution + # gives correct max tokens / costs and there is nothing for the + # operator to fix, so don't spam an ERROR on every request. + # membership alone isn't enough: Router init auto-registers every + # deployment name into litellm.model_cost as a zeroed stub, so + # require the entry to carry usable limits/costs. + _azure_fallback_key = _model if _model.startswith("azure/") else f"azure/{_model}" + _fallback_entry = litellm.model_cost.get(_azure_fallback_key) or {} + _fallback_resolves = ( + _fallback_entry.get("max_input_tokens") is not None + or _fallback_entry.get("max_tokens") is not None + or (_fallback_entry.get("input_cost_per_token") or 0) > 0 + ) + if _fallback_resolves: + verbose_router_logger.debug( + "Azure deployment '%s' has no base_model set; using '%s' from the model cost map for max tokens, cost tracking, etc.", + _model, + _azure_fallback_key, + ) + else: + verbose_router_logger.error( + "Could not identify azure model '%s'. Set azure 'base_model' for accurate max tokens, cost tracking, etc.- https://docs.litellm.ai/docs/proxy/cost_tracking#spend-tracking-for-azure-openai-models", + _model, + ) elif custom_llm_provider != "azure": model = _model diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index 1d6eb1bc5902..91e0a90a29c7 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -8639,3 +8639,92 @@ async def test_fallback_reentry_with_a_plain_group_clears_the_stale_marker(self) await router.async_pre_routing_hook(model="gemini-flash", request_kwargs=request_kwargs) assert AUTO_ROUTED_REQUEST_METADATA_KEY not in request_kwargs["metadata"] + + +class TestAzureBaseModelFallbackLogging: + """When an azure deployment has no base_model but its model name is a known + azure key in the cost map, get_router_model_info resolves it via the + fallback — so it must not log the per-request 'Could not identify azure + model' ERROR. The ERROR must remain for genuinely unmappable deployment + names. Issue #33172.""" + + @pytest.fixture(autouse=True) + def _use_local_model_cost_map(self, monkeypatch): + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + original_model_cost = litellm.model_cost + litellm.model_cost = litellm.get_model_cost_map(url="") + yield + litellm.model_cost = original_model_cost + + def _router_with_azure_deployment(self, deployment_model: str): + return litellm.Router( + model_list=[ + { + "model_name": "my-group", + "litellm_params": { + "model": deployment_model, + "api_key": "fake-key", + "api_base": "https://fake.openai.azure.com", + }, + "model_info": {"id": "azure-base-model-test-id"}, + } + ] + ) + + def test_map_known_deployment_name_resolves_without_error_log(self): + router = self._router_with_azure_deployment("azure/gpt-4o") + + with patch( + "litellm.router.verbose_router_logger.error" + ) as mock_error: + model_info = router.get_router_model_info( + deployment=None, received_model_name="my-group", id="azure-base-model-test-id" + ) + + assert not any( + "Could not identify azure model" in str(call) + for call in mock_error.call_args_list + ), f"unexpected error log: {mock_error.call_args_list}" + # the fallback resolution must actually surface the map values + assert model_info["max_input_tokens"] == litellm.model_cost["azure/gpt-4o"]["max_input_tokens"] + assert model_info["input_cost_per_token"] == litellm.model_cost["azure/gpt-4o"]["input_cost_per_token"] + + def test_unmappable_deployment_name_still_logs_error(self): + router = self._router_with_azure_deployment("azure/my-custom-deployment-name") + + with patch( + "litellm.router.verbose_router_logger.error" + ) as mock_error: + model_info = router.get_router_model_info( + deployment=None, received_model_name="my-group", id="azure-base-model-test-id" + ) + + assert any( + "Could not identify azure model" in str(call) + for call in mock_error.call_args_list + ), "expected the error log for an unmappable azure deployment name" + # unmappable names resolve to a zeroed stub — unchanged behavior + assert model_info.get("max_input_tokens") is None + + def test_explicit_base_model_still_wins(self): + router = litellm.Router( + model_list=[ + { + "model_name": "my-group", + "litellm_params": { + "model": "azure/some-deployment", + "api_key": "fake-key", + "api_base": "https://fake.openai.azure.com", + }, + "model_info": { + "id": "azure-base-model-test-id", + "base_model": "azure/gpt-4o-mini", + }, + } + ] + ) + + model_info = router.get_router_model_info( + deployment=None, received_model_name="my-group", id="azure-base-model-test-id" + ) + assert model_info["max_input_tokens"] == litellm.model_cost["azure/gpt-4o-mini"]["max_input_tokens"] From 43f6a5f463cdd824c2e2109539144b25da3d550b Mon Sep 17 00:00:00 2001 From: Mihidum Hettiyahandi <55163074+mihidumh@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:32:05 +1000 Subject: [PATCH 2/3] fix(router): use consistent positive checks in azure base_model fallback gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up: token-limit fields used 'is not None' while the cost field used '> 0' — a cost-map entry explicitly storing 0 limits could suppress the error log without carrying usable resolution data. All three checks now require a positive value. Co-Authored-By: Claude Fable 5 --- litellm/router.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index 40421473f9e1..bbe9e034700e 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -9163,10 +9163,10 @@ def get_router_model_info( # deployment name into litellm.model_cost as a zeroed stub, so # require the entry to carry usable limits/costs. _azure_fallback_key = _model if _model.startswith("azure/") else f"azure/{_model}" - _fallback_entry = litellm.model_cost.get(_azure_fallback_key) or {} - _fallback_resolves = ( - _fallback_entry.get("max_input_tokens") is not None - or _fallback_entry.get("max_tokens") is not None + _fallback_entry = litellm.model_cost.get(_azure_fallback_key) + _fallback_resolves = _fallback_entry is not None and ( + (_fallback_entry.get("max_input_tokens") or 0) > 0 + or (_fallback_entry.get("max_tokens") or 0) > 0 or (_fallback_entry.get("input_cost_per_token") or 0) > 0 ) if _fallback_resolves: From 419d3068448f4b4078efb6b04605f0341bc6eab0 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Fri, 21 Aug 2026 11:13:30 -0700 Subject: [PATCH 3/3] refactor(router): trim fallback gate comment and reuse the shared local_model_cost_map fixture --- litellm/router.py | 11 +++-------- tests/test_litellm/test_router.py | 11 ++--------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index bbe9e034700e..5acf4f446144 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -9154,14 +9154,9 @@ def get_router_model_info( ## SET MODEL TO 'model=' - if base_model is None + not azure if custom_llm_provider == "azure" and base_model is None: - # the `if model is None` fallback below resolves the deployment's - # model name against the model cost map — when the name is a known - # azure key (e.g. deployment model "azure/gpt-4o"), that resolution - # gives correct max tokens / costs and there is nothing for the - # operator to fix, so don't spam an ERROR on every request. - # membership alone isn't enough: Router init auto-registers every - # deployment name into litellm.model_cost as a zeroed stub, so - # require the entry to carry usable limits/costs. + # Router init auto-registers every deployment name into + # litellm.model_cost as a zeroed stub, so membership alone can't + # tell a resolvable name apart; require usable limits/costs. _azure_fallback_key = _model if _model.startswith("azure/") else f"azure/{_model}" _fallback_entry = litellm.model_cost.get(_azure_fallback_key) _fallback_resolves = _fallback_entry is not None and ( diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index 91e0a90a29c7..58a500def8e5 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -8641,21 +8641,14 @@ async def test_fallback_reentry_with_a_plain_group_clears_the_stale_marker(self) assert AUTO_ROUTED_REQUEST_METADATA_KEY not in request_kwargs["metadata"] +@pytest.mark.usefixtures("local_model_cost_map") class TestAzureBaseModelFallbackLogging: """When an azure deployment has no base_model but its model name is a known azure key in the cost map, get_router_model_info resolves it via the - fallback — so it must not log the per-request 'Could not identify azure + fallback, so it must not log the per-request 'Could not identify azure model' ERROR. The ERROR must remain for genuinely unmappable deployment names. Issue #33172.""" - @pytest.fixture(autouse=True) - def _use_local_model_cost_map(self, monkeypatch): - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - original_model_cost = litellm.model_cost - litellm.model_cost = litellm.get_model_cost_map(url="") - yield - litellm.model_cost = original_model_cost - def _router_with_azure_deployment(self, deployment_model: str): return litellm.Router( model_list=[