Skip to content

Commit c69efe5

Browse files
authored
feat(compat): transitional compat layer to migrate from 0.21 to 0.22+ (#1841)
Two-layer safeguard for users carrying 0.21 LangChain-style configs into the 0.22 default framework
1 parent 6a8fdf3 commit c69efe5

9 files changed

Lines changed: 688 additions & 2 deletions

File tree

nemoguardrails/_compat/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Transitional compatibility shims and migration helpers.
17+
18+
Modules under this package exist to bridge specific upgrade paths and are
19+
expected to be removed in a later release. Each module documents its own
20+
sunset version in its docstring.
21+
"""
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""0.21 -> 0.22 LangChain config migration helper.
17+
18+
Detects LangChain Python-side flags in ``model.parameters`` when the
19+
default framework is active and raises a clear error at LLMRails
20+
construction, so a stale 0.21 LangChain config surfaces during init
21+
rather than as an opaque HTTP 400 deep in a guardrail call.
22+
23+
Remove in 0.23.0. After 0.23 any unrecognized parameter is forwarded
24+
verbatim to the OpenAI-compatible HTTP client; the wire's HTTP 400 is
25+
the user's signal to clean up.
26+
"""
27+
28+
# TODO(0.23): delete this module along with its call site in
29+
# nemoguardrails.rails.llm.llmrails.LLMRails._init_llms.
30+
31+
import re
32+
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
33+
34+
if TYPE_CHECKING:
35+
from nemoguardrails.rails.llm.config import Model
36+
37+
_LANGCHAIN_BASE_FLAGS = frozenset(
38+
{
39+
"streaming",
40+
"disable_streaming",
41+
"verbose",
42+
"cache",
43+
"callbacks",
44+
"tags",
45+
"metadata",
46+
"name",
47+
"model_kwargs",
48+
}
49+
)
50+
51+
_PROVIDER_PREFIXED_ALIAS = re.compile(r"^(?P<prefix>[a-zA-Z]\w*?)_(?P<canonical>api_key|base_url|api_base|endpoint)$")
52+
53+
54+
def _canonical_name_for(matched_canonical: str) -> str:
55+
if matched_canonical == "api_key":
56+
return "api_key"
57+
return "base_url"
58+
59+
60+
def _detect_provider_alias(name: str) -> Optional[str]:
61+
match = _PROVIDER_PREFIXED_ALIAS.fullmatch(name)
62+
if match is None:
63+
return None
64+
return _canonical_name_for(match.group("canonical"))
65+
66+
67+
def _violations_for(model_type: str, parameters: dict) -> List[Tuple[str, str]]:
68+
"""Return a list of (model_type, action) tuples for one model."""
69+
out: List[Tuple[str, str]] = []
70+
for flag in sorted(_LANGCHAIN_BASE_FLAGS & set(parameters)):
71+
if flag == "model_kwargs":
72+
out.append((model_type, "unpack `model_kwargs` contents directly into `parameters`"))
73+
else:
74+
out.append((model_type, f"remove `{flag}`"))
75+
for name in sorted(parameters):
76+
if name in _LANGCHAIN_BASE_FLAGS:
77+
continue
78+
canonical = _detect_provider_alias(name)
79+
if canonical is None:
80+
continue
81+
out.append((model_type, f"rename `{name}` to `{canonical}`"))
82+
return out
83+
84+
85+
def check_langchain_kwargs(models: "Iterable[Model]", active_framework: str) -> None:
86+
"""Raise ValueError if any model carries LangChain Python-side flags.
87+
88+
No-op when the active framework is anything other than ``default``;
89+
LangChain-flavored kwargs are valid on the LangChain framework.
90+
"""
91+
if active_framework != "default":
92+
return
93+
violations: List[Tuple[str, str]] = []
94+
for model in models:
95+
if not model.parameters:
96+
continue
97+
violations.extend(_violations_for(model.type, model.parameters))
98+
if not violations:
99+
return
100+
body = "\n".join(f" models[{model_type}]: {action}" for model_type, action in violations)
101+
raise ValueError(
102+
"Your config uses 0.21-style LangChain conventions that the default framework\n"
103+
"doesn't forward:\n\n"
104+
f"{body}\n\n"
105+
"Two paths:\n"
106+
" - Adapt to the default framework: apply the renames/removals above.\n"
107+
" Only do this if your endpoint is OpenAI-compatible.\n"
108+
" - Keep 0.21 LangChain behavior: set NEMOGUARDRAILS_LLM_FRAMEWORK=langchain.\n\n"
109+
"(Migration check; removed in 0.23.0.)"
110+
)

nemoguardrails/llm/clients/_errors.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,37 @@
4141
"token limit",
4242
]
4343

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

61+
_UNKNOWN_PARAM_HINT_TOKENS = (
62+
"unrecognized request argument",
63+
"unsupported parameter",
64+
"' is unsupported",
65+
"extra inputs are not permitted",
66+
)
67+
68+
_MIGRATION_HINT_021 = (
69+
"(If you upgraded from 0.21: the default framework forwards `parameters` "
70+
"verbatim to the OpenAI-compatible endpoint, which rejected the field above. "
71+
"LangChain-only flags must be removed for the default framework. To keep "
72+
"0.21 LangChain behavior, set NEMOGUARDRAILS_LLM_FRAMEWORK=langchain.)"
73+
)
74+
5275
_SECRET_PATTERN = re.compile(r"(sk-|nvapi-|AIza|bearer\s+)\S+", re.IGNORECASE)
5376

5477

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

131154

155+
def _looks_like_unknown_param_400(error_message: str) -> bool:
156+
msg_lower = error_message.lower()
157+
return any(token in msg_lower for token in _UNKNOWN_PARAM_HINT_TOKENS)
158+
159+
160+
def _maybe_append_migration_hint(error_message: str) -> str:
161+
if not _looks_like_unknown_param_400(error_message):
162+
return error_message
163+
return f"{error_message}\n\n{_MIGRATION_HINT_021}"
164+
165+
132166
def _classify_bad_request(status_code: int, error_message: str, kwargs: Dict[str, Any]) -> LLMClientError:
133167
msg_lower = error_message.lower()
134168
if any(kw in msg_lower for kw in _CONTEXT_WINDOW_KEYWORDS):
@@ -139,6 +173,8 @@ def _classify_bad_request(status_code: int, error_message: str, kwargs: Dict[str
139173
f"{error_message} (set include_usage_in_stream=False on the model "
140174
"or in config.yml parameters to remove this field from streaming requests)"
141175
)
176+
else:
177+
error_message = _maybe_append_migration_hint(error_message)
142178
return LLMUnsupportedParamsError(status_code, error_message, **kwargs)
143179
return LLMBadRequestError(status_code, error_message, **kwargs)
144180

nemoguardrails/rails/llm/llmrails.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,14 @@ def _init_llms(self):
428428
Raises:
429429
ModelInitializationError: If any model initialization fails
430430
"""
431+
from nemoguardrails._compat.langchain_kwargs import check_langchain_kwargs
432+
from nemoguardrails.llm.frameworks import get_default_framework
433+
434+
models_to_check = (
435+
[model for model in self.config.models if model.type != "main"] if self.llm else self.config.models
436+
)
437+
check_langchain_kwargs(models_to_check, get_default_framework())
438+
431439
# If the user supplied an already-constructed LLM via the constructor we
432440
# treat it as the *main* model, but **still** iterate through the
433441
# configuration to load any additional models (e.g. `content_safety`).

tests/_compat/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.

0 commit comments

Comments
 (0)