From 6f1bffddb7b0dcafb6190bcfea3e7f0399d66ad3 Mon Sep 17 00:00:00 2001 From: AUTHENSOR Date: Wed, 16 Sep 2026 13:44:54 -0500 Subject: [PATCH 1/2] Resolve watsonx model_id body key for virtual-key scope checks (fixes #41467) Add the /watsonx branch to get_model_from_request reading the native model_id body key, so the models[] allowlist check runs on the watsonx pass-through route instead of being skipped when resolution returns None. --- litellm/proxy/auth/auth_utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 3372145e66c..7c15fd1f33c 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -2036,6 +2036,16 @@ def get_model_from_request( if vertex_match: model = vertex_match.group(1) + # If still not found, read the Watsonx body key. Watsonx generation and + # chat requests carry the model under "model_id" (the native Watsonx ML + # API shape, and the key litellm's own Watsonx client writes), so without + # this branch the model resolves to None and the key's model allowlist is + # never checked on /watsonx routes. + if model is None and route.lower().startswith("/watsonx"): + watsonx_model: Final = request_data.get("model_id") + if isinstance(watsonx_model, str) and watsonx_model: + model = watsonx_model + if route.lower().startswith("/bedrock"): bedrock_model: Final = _model_from_bedrock_route(route) return model if bedrock_model is None else bedrock_model From 8bca92b6447f7096eddc86fbab3df14e1ece553a Mon Sep 17 00:00:00 2001 From: AUTHENSOR <250658088+AUTHENSOR@users.noreply.github.com> Date: Sun, 20 Sep 2026 03:41:57 -0700 Subject: [PATCH 2/2] address review: always return watsonx model_id when present The generic extractor resolves the 'model' body key first; a caller can set model to an allowed name while Watsonx executes the disallowed model_id. This makes model_id authoritative whenever present on /watsonx routes, so the allowlist check runs on the model that will actually serve the request. --- litellm/proxy/auth/auth_utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 7c15fd1f33c..72d2dd31896 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -2036,15 +2036,15 @@ def get_model_from_request( if vertex_match: model = vertex_match.group(1) - # If still not found, read the Watsonx body key. Watsonx generation and - # chat requests carry the model under "model_id" (the native Watsonx ML - # API shape, and the key litellm's own Watsonx client writes), so without - # this branch the model resolves to None and the key's model allowlist is - # never checked on /watsonx routes. - if model is None and route.lower().startswith("/watsonx"): + # Watsonx routes carry the deployment name under "model_id" (the native + # Watsonx ML API shape). The generic "model" body key can also be set by + # the caller, but Watsonx executes model_id; both must be authorized, + # so model_id is checked whenever present on a /watsonx route, + # regardless of whether "model" already resolved. + if route.lower().startswith("/watsonx"): watsonx_model: Final = request_data.get("model_id") if isinstance(watsonx_model, str) and watsonx_model: - model = watsonx_model + return watsonx_model if route.lower().startswith("/bedrock"): bedrock_model: Final = _model_from_bedrock_route(route)