Skip to content
21 changes: 21 additions & 0 deletions nemoguardrails/_compat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Transitional compatibility shims and migration helpers.

Modules under this package exist to bridge specific upgrade paths and are
expected to be removed in a later release. Each module documents its own
sunset version in its docstring.
"""
110 changes: 110 additions & 0 deletions nemoguardrails/_compat/langchain_kwargs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""0.21 -> 0.22 LangChain config migration helper.

Detects LangChain Python-side flags in ``model.parameters`` when the
default framework is active and raises a clear error at LLMRails
construction, so a stale 0.21 LangChain config surfaces during init
rather than as an opaque HTTP 400 deep in a guardrail call.

Remove in 0.23.0. After 0.23 any unrecognized parameter is forwarded
verbatim to the OpenAI-compatible HTTP client; the wire's HTTP 400 is
the user's signal to clean up.
"""

# TODO(0.23): delete this module along with its call site in
# nemoguardrails.rails.llm.llmrails.LLMRails._init_llms.

import re
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple

if TYPE_CHECKING:
from nemoguardrails.rails.llm.config import Model

_LANGCHAIN_BASE_FLAGS = frozenset(
{
"streaming",
"disable_streaming",
"verbose",
"cache",
"callbacks",
"tags",
"metadata",
"name",
"model_kwargs",
}
)

_PROVIDER_PREFIXED_ALIAS = re.compile(r"^(?P<prefix>[a-zA-Z]\w*?)_(?P<canonical>api_key|base_url|api_base|endpoint)$")


def _canonical_name_for(matched_canonical: str) -> str:
if matched_canonical == "api_key":
return "api_key"
return "base_url"


def _detect_provider_alias(name: str) -> Optional[str]:
match = _PROVIDER_PREFIXED_ALIAS.fullmatch(name)
if match is None:
return None
return _canonical_name_for(match.group("canonical"))


def _violations_for(model_type: str, parameters: dict) -> List[Tuple[str, str]]:
"""Return a list of (model_type, action) tuples for one model."""
out: List[Tuple[str, str]] = []
for flag in sorted(_LANGCHAIN_BASE_FLAGS & set(parameters)):
if flag == "model_kwargs":
out.append((model_type, "unpack `model_kwargs` contents directly into `parameters`"))
else:
out.append((model_type, f"remove `{flag}`"))
for name in sorted(parameters):
if name in _LANGCHAIN_BASE_FLAGS:
continue
canonical = _detect_provider_alias(name)
if canonical is None:
continue
out.append((model_type, f"rename `{name}` to `{canonical}`"))
return out


def check_langchain_kwargs(models: "Iterable[Model]", active_framework: str) -> None:
"""Raise ValueError if any model carries LangChain Python-side flags.

No-op when the active framework is anything other than ``default``;
LangChain-flavored kwargs are valid on the LangChain framework.
"""
if active_framework != "default":
return
violations: List[Tuple[str, str]] = []
for model in models:
if not model.parameters:
continue
violations.extend(_violations_for(model.type, model.parameters))
if not violations:
return
body = "\n".join(f" models[{model_type}]: {action}" for model_type, action in violations)
raise ValueError(
"Your config uses 0.21-style LangChain conventions that the default framework\n"
"doesn't forward:\n\n"
f"{body}\n\n"
"Two paths:\n"
" - Adapt to the default framework: apply the renames/removals above.\n"
" Only do this if your endpoint is OpenAI-compatible.\n"
" - Keep 0.21 LangChain behavior: set NEMOGUARDRAILS_LLM_FRAMEWORK=langchain.\n\n"
"(Migration check; removed in 0.23.0.)"
)
38 changes: 37 additions & 1 deletion nemoguardrails/llm/clients/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,37 @@
"token limit",
]

# Bare "is not supported" was deliberately removed: it false-positives on
# non-param 400s ("model is not supported in your region", "image input is not
# supported for this model"). Real OpenAI param rejections always carry the
# "Unsupported parameter:" prefix matched above. A provider emitting bare
# "X is not supported" without that prefix will classify as LLMBadRequestError
# instead of LLMUnsupportedParamsError; if observed in the wild, add a tighter
# phrase here (e.g. "is not a supported parameter").
_UNSUPPORTED_PARAMS_KEYWORDS = [
"unsupported parameter",
"is not supported",
"parameter not allowed",
"unknown parameter",
"unrecognized parameter",
"unrecognized request argument",
"' is unsupported",
"extra inputs are not permitted",
]

_UNKNOWN_PARAM_HINT_TOKENS = (
"unrecognized request argument",
"unsupported parameter",
"' is unsupported",
"extra inputs are not permitted",
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

_MIGRATION_HINT_021 = (
"(If you upgraded from 0.21: the default framework forwards `parameters` "
"verbatim to the OpenAI-compatible endpoint, which rejected the field above. "
"LangChain-only flags must be removed for the default framework. To keep "
"0.21 LangChain behavior, set NEMOGUARDRAILS_LLM_FRAMEWORK=langchain.)"
)

_SECRET_PATTERN = re.compile(r"(sk-|nvapi-|AIza|bearer\s+)\S+", re.IGNORECASE)


Expand Down Expand Up @@ -129,6 +152,17 @@ def _build_error_fields(parsed_body: Any, raw_body: str, headers: Any, ctx: Erro
return error_message, kwargs


def _looks_like_unknown_param_400(error_message: str) -> bool:
msg_lower = error_message.lower()
return any(token in msg_lower for token in _UNKNOWN_PARAM_HINT_TOKENS)


def _maybe_append_migration_hint(error_message: str) -> str:
if not _looks_like_unknown_param_400(error_message):
return error_message
return f"{error_message}\n\n{_MIGRATION_HINT_021}"


def _classify_bad_request(status_code: int, error_message: str, kwargs: Dict[str, Any]) -> LLMClientError:
msg_lower = error_message.lower()
if any(kw in msg_lower for kw in _CONTEXT_WINDOW_KEYWORDS):
Expand All @@ -139,6 +173,8 @@ def _classify_bad_request(status_code: int, error_message: str, kwargs: Dict[str
f"{error_message} (set include_usage_in_stream=False on the model "
"or in config.yml parameters to remove this field from streaming requests)"
)
else:
error_message = _maybe_append_migration_hint(error_message)
return LLMUnsupportedParamsError(status_code, error_message, **kwargs)
return LLMBadRequestError(status_code, error_message, **kwargs)

Expand Down
8 changes: 8 additions & 0 deletions nemoguardrails/rails/llm/llmrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,14 @@ def _init_llms(self):
Raises:
ModelInitializationError: If any model initialization fails
"""
from nemoguardrails._compat.langchain_kwargs import check_langchain_kwargs
from nemoguardrails.llm.frameworks import get_default_framework

models_to_check = (
[model for model in self.config.models if model.type != "main"] if self.llm else self.config.models
)
check_langchain_kwargs(models_to_check, get_default_framework())

# If the user supplied an already-constructed LLM via the constructor we
# treat it as the *main* model, but **still** iterate through the
# configuration to load any additional models (e.g. `content_safety`).
Expand Down
14 changes: 14 additions & 0 deletions tests/_compat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Loading
Loading