fix: route IBM WatsonX selections to model_id endpoint in unified get_llm (#13671) (#13684)

* 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 <id> 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 <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Eric Hare
2026-06-18 07:50:35 -07:00
committed by GitHub
parent b796f55c9c
commit d68e4bbe08
2 changed files with 138 additions and 9 deletions

View File

@ -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 <id> 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)

View File

@ -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 <id> 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"