From d68e4bbe08221e58b20b4b024def471d400a2184 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 18 Jun 2026 07:50:35 -0700 Subject: [PATCH] fix: route IBM WatsonX selections to model_id endpoint in unified get_llm (#13671) (#13684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: route IBM WatsonX selections to model_id endpoint in unified get_llm A model selection sourced from the GET /api/v1/models catalog (which the frontend uses to augment the Language Model / Agent dropdown right after a provider is configured) carries only raw create_model_metadata fields — none of the enriched *_param keys that get_language_model_options injects. get_llm already derived model_class from the provider mapping for this case but still let model_name_param / api_key_param / url_param / project_id_param fall back to the generic defaults. For IBM WatsonX this is load-bearing: langchain_ibm.ChatWatsonx exposes both a `model` field (routes to the OpenAI-style Model Gateway, a different catalog) and a `model_id` field (routes to ModelInference, the foundation-models endpoint the dropdown is populated from). Falling back to the generic `model` sent the selected foundation-model id to the gateway, failing with "model not found" / IAM "Provided user not found or active" — even though the dropdown, connection test, and standalone IBM watsonx.ai component all work. Derive all param names from get_provider_param_mapping(provider) when the selection metadata lacks them. Only WatsonX (model_id/apikey) differs from the generic defaults, so OpenAI/Anthropic/etc. are unaffected. Fixes #13671 Co-Authored-By: Claude Opus 4.8 * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../models/unified_models/instantiation.py | 36 ++++-- .../models/test_get_llm_watsonx_params.py | 111 ++++++++++++++++++ 2 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 src/lfx/tests/unit/base/models/test_get_llm_watsonx_params.py diff --git a/src/lfx/src/lfx/base/models/unified_models/instantiation.py b/src/lfx/src/lfx/base/models/unified_models/instantiation.py index 658a55af36..921523ada5 100644 --- a/src/lfx/src/lfx/base/models/unified_models/instantiation.py +++ b/src/lfx/src/lfx/base/models/unified_models/instantiation.py @@ -71,8 +71,24 @@ def get_llm( provider = model.get("provider") metadata = model.get("metadata", {}) + # Stored selections sourced from ``get_unified_models_detailed`` (e.g. the + # ``GET /api/v1/models`` catalog the frontend uses to augment its dropdown + # right after a provider is configured, before the backend repopulates + # ``template[model]["options"]``) carry only the raw ``create_model_metadata`` + # fields — none of the enriched ``*_param`` keys. Derive those param names + # from the provider mapping (the same source ``get_language_model_options`` + # uses) so provider-specific names are honored instead of the generic + # ``model`` / ``api_key`` defaults. This matters for IBM WatsonX: passing a + # foundation-model id under the generic ``model`` kwarg routes ChatWatsonx to + # the Model Gateway (a different, OpenAI-style catalog), surfacing as + # "model not found" / IAM "Provided user not found or active" even + # though the dropdown, connection test, and standalone component all work. + from lfx.base.models.model_metadata import get_provider_param_mapping + + provider_param_mapping = get_provider_param_mapping(provider) if provider else {} + # Get model class and parameter names from metadata - api_key_param = metadata.get("api_key_param", "api_key") + api_key_param = metadata.get("api_key_param") or provider_param_mapping.get("api_key_param", "api_key") # Capture the user-supplied api_key BEFORE resolution so we can name # it back in the error message if it was a Global Variable reference @@ -136,16 +152,16 @@ def get_llm( # carries the raw ``create_model_metadata`` fields, so we have to derive # ``model_class`` from the provider mapping that # ``get_language_model_options`` would have used. - model_class_name = metadata.get("model_class") - if not model_class_name and provider: - from lfx.base.models.model_metadata import get_provider_param_mapping - - model_class_name = get_provider_param_mapping(provider).get("model_class") + model_class_name = metadata.get("model_class") or provider_param_mapping.get("model_class") if not model_class_name: msg = f"No model class defined for {model_name}" raise ValueError(msg) model_class = unified_models_module.get_model_class(model_class_name) - model_name_param = metadata.get("model_name_param", "model") + # The provider mapping stores the model-name param under ``model_param`` + # (``get_language_model_options`` re-keys it to ``model_name_param``); fall + # back to it so e.g. IBM WatsonX resolves to ``model_id`` rather than the + # generic ``model``. + model_name_param = metadata.get("model_name_param") or provider_param_mapping.get("model_param", "model") # Check if this is a reasoning model that doesn't support temperature reasoning_models = metadata.get("reasoning_models", []) @@ -185,8 +201,10 @@ def get_llm( if provider in {"IBM WatsonX", "IBM watsonx.ai"}: # For watsonx, url and project_id are required parameters # Try database first, then component values, then environment variables - url_param = metadata.get("url_param", "url") - project_id_param = metadata.get("project_id_param", "project_id") + url_param = metadata.get("url_param") or provider_param_mapping.get("url_param", "url") + project_id_param = metadata.get("project_id_param") or provider_param_mapping.get( + "project_id_param", "project_id" + ) # Get all provider variables from database provider_vars = unified_models_module.get_all_variables_for_provider(user_id, provider) diff --git a/src/lfx/tests/unit/base/models/test_get_llm_watsonx_params.py b/src/lfx/tests/unit/base/models/test_get_llm_watsonx_params.py new file mode 100644 index 0000000000..7aebb23ba1 --- /dev/null +++ b/src/lfx/tests/unit/base/models/test_get_llm_watsonx_params.py @@ -0,0 +1,111 @@ +"""Regression guards for IBM WatsonX param-name resolution in unified ``get_llm``. + +A stored model selection sourced from the ``GET /api/v1/models`` catalog (which +the frontend uses to augment the Language Model / Agent dropdown right after a +provider is configured) carries only the raw ``create_model_metadata`` fields — +none of the enriched ``*_param`` keys that ``get_language_model_options`` adds. + +For IBM WatsonX this is load-bearing: ``langchain_ibm.ChatWatsonx`` exposes both +a ``model`` field (routes to the OpenAI-style **Model Gateway**, a different +catalog) and a ``model_id`` field (routes to **ModelInference**, the foundation +models endpoint the dropdown is populated from). If ``get_llm`` falls back to the +generic ``model`` param name, the selected foundation-model id is sent to the +gateway and fails with "model not found" / IAM "Provided user not found or +active" — even though the dropdown, connection test, and standalone +``IBM watsonx.ai`` component all work. See langflow-ai/langflow#13671. + +These tests pin that ``get_llm`` resolves the provider-specific param names +(``model_id``, ``apikey``, ``url``, ``project_id``) from the provider mapping +even when the selection metadata is the raw catalog shape. +""" + +from __future__ import annotations + +from unittest.mock import patch + + +def _capture_factory(): + captured: dict = {} + + class FakeChatModel: + def __init__(self, **kwargs): + captured.update(kwargs) + + return FakeChatModel, captured + + +def _raw_watsonx_selection() -> list[dict]: + """Selection as persisted from the raw ``/api/v1/models`` catalog. + + Matches ``create_model_metadata`` output: provider/icon/flags only, with + none of the enriched ``model_class`` / ``model_name_param`` / ``api_key_param`` + keys that ``get_language_model_options`` injects. + """ + return [ + { + "name": "meta-llama/llama-3-3-70b-instruct", + "provider": "IBM WatsonX", + "metadata": { + "provider": "IBM WatsonX", + "icon": "IBM", + "tool_calling": True, + "model_type": "llm", + "default": True, + }, + } + ] + + +def _build_llm_from_raw_selection() -> dict: + from lfx.base.models import unified_models as unified_models_module + from lfx.base.models.unified_models.instantiation import get_llm + + fake_cls, captured = _capture_factory() + + with ( + patch.object( + unified_models_module, + "get_api_key_for_provider", + return_value="ibm-apikey-xyz", # pragma: allowlist secret + ), + patch.object(unified_models_module, "get_model_class", return_value=fake_cls), + patch.object( + unified_models_module, + "get_all_variables_for_provider", + return_value={ + "WATSONX_URL": "https://us-south.ml.cloud.ibm.com", + "WATSONX_PROJECT_ID": "proj-123", + }, + ), + ): + get_llm(_raw_watsonx_selection(), user_id=None, stream=True) + + return captured + + +def test_watsonx_raw_metadata_sends_model_id_not_model(): + """The foundation-model id must be sent as ``model_id``, never the generic ``model``. + + ``model`` routes ChatWatsonx to the Model Gateway (different catalog) and is + the root cause of the "model not found" failure in #13671. + """ + captured = _build_llm_from_raw_selection() + + assert captured.get("model_id") == "meta-llama/llama-3-3-70b-instruct", ( + "get_llm must resolve IBM WatsonX's model param to 'model_id' even when the " + f"selection carries only raw catalog metadata. Got kwargs keys: {sorted(captured)}." + ) + assert "model" not in captured, ( + "Passing the foundation-model id under the generic 'model' kwarg routes " + "ChatWatsonx to the Model Gateway (a different catalog), which fails with " + f"'model not found'. Got kwargs keys: {sorted(captured)}." + ) + + +def test_watsonx_raw_metadata_sends_apikey_and_connection_params(): + """The api key, url, and project id must use WatsonX's native param names.""" + captured = _build_llm_from_raw_selection() + + assert captured.get("apikey") == "ibm-apikey-xyz" # pragma: allowlist secret + assert captured.get("url") == "https://us-south.ml.cloud.ibm.com" + assert captured.get("project_id") == "proj-123"