From 35e72e8c64df7e9d1f078ff418334859e274d0c7 Mon Sep 17 00:00:00 2001 From: leela8256 Date: Fri, 31 Jul 2026 05:01:53 -0700 Subject: [PATCH 1/5] feat(nodes): add NVIDIA Nemotron LLM node Adds llm_nemotron: NVIDIA's Nemotron reasoning models via the OpenAI-compatible build.nvidia.com API, with self-hosted NIM/vLLM profiles, filtered sync_models integration, CI key wiring, tests, and docs. Closes #1765 Co-Authored-By: Claude Fable 5 --- .github/workflows/sync-models.yml | 3 + docs/README-nodes.md | 1 + nodes/src/nodes/llm_nemotron/IGlobal.py | 151 +++++++++ nodes/src/nodes/llm_nemotron/IInstance.py | 28 ++ nodes/src/nodes/llm_nemotron/README.md | 93 ++++++ nodes/src/nodes/llm_nemotron/__init__.py | 41 +++ nodes/src/nodes/llm_nemotron/nemotron.py | 88 ++++++ nodes/src/nodes/llm_nemotron/nemotron.svg | 1 + nodes/src/nodes/llm_nemotron/requirements.txt | 7 + nodes/src/nodes/llm_nemotron/services.json | 294 ++++++++++++++++++ nodes/test/framework/pipeline.py | 1 + tools/sync_models/README.md | 1 + tools/sync_models/src/providers/nemotron.py | 61 ++++ tools/sync_models/src/sync_models.config.json | 50 ++- tools/sync_models/src/sync_models.py | 2 + tools/sync_models/test/markers.py | 5 + tools/sync_models/test/test_sync_live.py | 22 ++ 17 files changed, 848 insertions(+), 1 deletion(-) create mode 100644 nodes/src/nodes/llm_nemotron/IGlobal.py create mode 100644 nodes/src/nodes/llm_nemotron/IInstance.py create mode 100644 nodes/src/nodes/llm_nemotron/README.md create mode 100644 nodes/src/nodes/llm_nemotron/__init__.py create mode 100644 nodes/src/nodes/llm_nemotron/nemotron.py create mode 100644 nodes/src/nodes/llm_nemotron/nemotron.svg create mode 100644 nodes/src/nodes/llm_nemotron/requirements.txt create mode 100644 nodes/src/nodes/llm_nemotron/services.json create mode 100644 tools/sync_models/src/providers/nemotron.py diff --git a/.github/workflows/sync-models.yml b/.github/workflows/sync-models.yml index 1eb5eaab6..3d1ce1bb4 100644 --- a/.github/workflows/sync-models.yml +++ b/.github/workflows/sync-models.yml @@ -63,6 +63,7 @@ jobs: [llm_qwen]=ROCKETRIDE_QWEN_KEY [llm_minimax]=ROCKETRIDE_MINIMAX_KEY [llm_kimi]=ROCKETRIDE_KIMI_KEY + [llm_nemotron]=ROCKETRIDE_NVIDIA_KEY ) for PROVIDER in "${!PROVIDER_KEY[@]}"; do @@ -90,6 +91,7 @@ jobs: ROCKETRIDE_QWEN_KEY: ${{ secrets.ROCKETRIDE_QWEN_KEY }} ROCKETRIDE_MINIMAX_KEY: ${{ secrets.ROCKETRIDE_MINIMAX_KEY }} ROCKETRIDE_KIMI_KEY: ${{ secrets.ROCKETRIDE_KIMI_KEY }} + ROCKETRIDE_NVIDIA_KEY: ${{ secrets.ROCKETRIDE_NVIDIA_KEY }} # Strict sync: only providers whose API key is configured. Native API # discovers and smoke-tests new models. No OpenRouter pollution. @@ -110,6 +112,7 @@ jobs: ROCKETRIDE_QWEN_KEY: ${{ secrets.ROCKETRIDE_QWEN_KEY }} ROCKETRIDE_MINIMAX_KEY: ${{ secrets.ROCKETRIDE_MINIMAX_KEY }} ROCKETRIDE_KIMI_KEY: ${{ secrets.ROCKETRIDE_KIMI_KEY }} + ROCKETRIDE_NVIDIA_KEY: ${{ secrets.ROCKETRIDE_NVIDIA_KEY }} # Fallback sync: providers without an API key. OpenRouter / LiteLLM # are permitted as discovery sources for these. Smoke tests are skipped diff --git a/docs/README-nodes.md b/docs/README-nodes.md index 5153b42f4..0b32bf4db 100644 --- a/docs/README-nodes.md +++ b/docs/README-nodes.md @@ -81,6 +81,7 @@ the same `tool_github` or `tool_tavily` can attach to `agent_deepagent`, | `llm_perplexity` | questions → answers | Perplexity Sonar (web-search models) | | `llm_gmi_cloud` | questions → answers | GMI Cloud models | | `llm_baidu_qianfan` | questions → answers | Baidu Qianfan / ERNIE (OpenAI-compatible Qianfan API) | +| `llm_nemotron` | questions → answers | NVIDIA Nemotron reasoning models (build.nvidia.com API or self-hosted NIM) | | `llm_openai_api` | questions → answers | Any OpenAI-compatible endpoint (also ships a Nebius Token Factory preset) | > `nodes/src/nodes/llm_ibm_watson/` exists but currently ships **no service diff --git a/nodes/src/nodes/llm_nemotron/IGlobal.py b/nodes/src/nodes/llm_nemotron/IGlobal.py new file mode 100644 index 000000000..4eeba6f90 --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/IGlobal.py @@ -0,0 +1,151 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +import os +import re +from typing import Optional +from rocketlib import IGlobalBase, warning +from ai.common.config import Config +from ai.common.chat import ChatBase + + +VALIDATION_PROMPT = 'Hi' +NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1' + + +class IGlobal(IGlobalBase): + """Global handler for the NVIDIA Nemotron LLM node.""" + + chat: Optional[ChatBase] = None + + def validateConfig(self): + """Validate only cloud Nemotron models (NVIDIA API) at save time. + + Local/self-hosted OpenAI-compatible endpoints (NIM / vLLM / SGLang) + are not validated here, matching the llm_kimi precedent. + """ + # Load dependencies first + from depends import depends # type: ignore + + requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt' + depends(requirements) + + try: + # Import OpenAI SDK (the NVIDIA API is OpenAI-compatible) + from openai import OpenAI + + # Prefer provider-driven exception types over string parsing + from openai import APIStatusError, OpenAIError, AuthenticationError, RateLimitError, APIConnectionError + + # Get config + config = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) + apikey = config.get('apikey') + model = config.get('model') + serverbase = config.get('serverbase') or NVIDIA_BASE_URL + + # Only validate cloud NVIDIA endpoints; skip self-hosted/offline ones. + if 'api.nvidia.com' not in serverbase: + return + + # UI handles missing key prompts; skip the probe when the key is absent + if not apikey or not model: + return + + # Minimal probe; UI handles missing key prompts + try: + # Create client and make a 1-token probe + client = OpenAI(api_key=apikey, base_url=serverbase) + client.chat.completions.create( + model=model, messages=[{'role': 'user', 'content': VALIDATION_PROMPT}], max_tokens=1 + ) + except APIStatusError as e: + # HTTP error with structured body; pull code/type/message + status = getattr(e, 'status_code', None) or getattr(e, 'status', None) + try: + resp = getattr(e, 'response', None) + data = resp.json() if resp is not None else None + if isinstance(data, dict): + err = data.get('error') + if isinstance(err, dict): + etype = err.get('type') + emsg = err.get('message') or data.get('message') + else: + etype = None + emsg = data.get('message') + message = self._format_error(status, etype, emsg, str(e)) + else: + message = self._format_error(status, None, None, str(e)) + except Exception: + message = self._format_error(status, None, None, str(e)) + warning(message) + return + except (AuthenticationError, RateLimitError, APIConnectionError, OpenAIError) as e: + # Other OpenAI exceptions; format consistently + message = self._format_error(None, None, None, str(e)) + warning(message) + return + + except Exception as e: + # Generic fallback - do not alter provider message + warning(str(e)) + + def beginGlobal(self): + from depends import depends # type: ignore + + # Load the requirements + requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt' + depends(requirements) + + from .nemotron import Chat + + # Get our bag + bag = self.IEndpoint.endpoint.bag + + # Get this nodes config + config = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) + + # Get a chat to interface + self._chat = Chat(self.glb.logicalType, config, bag) + + def endGlobal(self): + self._chat = None + + def _format_error(self, status, etype, emsg, fallback: str) -> str: + """Compose a user-facing error string. + + - If a numeric HTTP status/code is available, prefix it as "Error :". + - Then include provider error type and message when present. + - If no structured fields are available, return the fallback message as-is. + - Whitespace is normalized to a single line; content is not truncated. + """ + parts: list[str] = [] + if status is not None: + parts.append(f'Error {status}:') + if etype: + parts.append(str(etype)) + if emsg: + if etype: + parts.append('-') + parts.append(str(emsg)) + message = ' '.join(parts) if parts else fallback + return re.sub(r'\s+', ' ', message).strip() diff --git a/nodes/src/nodes/llm_nemotron/IInstance.py b/nodes/src/nodes/llm_nemotron/IInstance.py new file mode 100644 index 000000000..77e200613 --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/IInstance.py @@ -0,0 +1,28 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +from ai.common.llm_base import LLMBase + + +class IInstance(LLMBase): + pass diff --git a/nodes/src/nodes/llm_nemotron/README.md b/nodes/src/nodes/llm_nemotron/README.md new file mode 100644 index 000000000..397e9c276 --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/README.md @@ -0,0 +1,93 @@ +# NVIDIA Nemotron LLM Node (`llm_nemotron`) + +Connects RocketRide pipelines to NVIDIA's **Nemotron** family of open-weight +reasoning models, served through NVIDIA's OpenAI-compatible cloud API +([build.nvidia.com](https://build.nvidia.com)) or self-hosted with NIM +containers, vLLM, or SGLang. + +- **Lane:** `questions → answers` +- **Endpoint (cloud):** `https://integrate.api.nvidia.com/v1` +- **Endpoint (local, default):** `http://localhost:8000/v1` + +## Models + +The Nemotron 3 generation are hybrid Mamba-Transformer MoE reasoning models +with configurable thinking budgets. + +| Profile | Model ID | Context | Notes | +| --- | --- | --- | --- | +| Nemotron 3 Super 120B *(default)* | `nvidia/nemotron-3-super-120b-a12b` | 1M | Best efficiency/accuracy balance | +| Nemotron 3 Ultra 550B | `nvidia/nemotron-3-ultra-550b-a55b` | 1M | Frontier reasoning, 55B active params | +| Nemotron 3 Nano 30B | `nvidia/nemotron-3-nano-30b-a3b` | 256K | Fast sub-agent tier, 3B active params | +| Custom Model | (user-defined) | (user-defined) | Any model on an OpenAI-compatible endpoint | + +The multimodal variants (Nano Omni, Nano VL) and the Retriever / Parse / +Speech / Safety lines are out of scope for this chat node. + +## Reasoning output + +Nemotron 3 models are reasoning models. When the OpenAI-compatible endpoint +returns chain-of-thought wrapped in `...` inside `content`, the +node strips it so downstream nodes only see the final answer (the +`llm_minimax` pattern). Budget generous output tokens for reasoning-heavy +prompts. + +## Local deploy + +Self-hosted **NIM containers serve the same model IDs as the hosted API**, so +the "(Local)" profiles reuse the cloud model strings against +`http://localhost:8000/v1`: + +```sh +docker run --gpus all -p 8000:8000 nvcr.io/nim/nvidia/nemotron-3-nano-30b-a3b:latest +``` + +vLLM / SGLang users can serve the open weights from HuggingFace instead; set +`--served-model-name` to the profile's model ID (or use a Custom profile with +your own name). Nemotron 3 weights, training data, and recipes are openly +published; Nano runs on a single high-memory GPU, Super and Ultra need +multi-GPU servers. + +## Authentication + +Cloud profiles require an NVIDIA API key (`nvapi-...`, from +[build.nvidia.com](https://build.nvidia.com)) in `apikey`. The key requirement +is enforced by base-URL match: if `serverbase` contains `api.nvidia.com` and +no key is set, the node raises `NVIDIA API key is required for cloud +profiles.` at startup. Key format is not validated beyond presence. + +Local profiles have no `apikey` field; local OpenAI-compatible servers accept +any token, so the node passes a dummy key (`sk-local-dummy-key`). + +## Model sync + +Profiles are maintained by the `sync_models` tooling (`llm_nemotron` +provider, `ROCKETRIDE_NVIDIA_KEY`). NVIDIA's `/v1/models` endpoint lists the +full multi-vendor build.nvidia.com catalog (Llama, GLM, Kimi, ...), so the +sync config filters to the `nvidia/*nemotron*` chat models only. + +--- + + + + +## Schema + +| Field | Type | Description | Default | +|---|---|---|---| +| `model` | `string` | **Model**
NVIDIA Nemotron model | | +| `modelTotalTokens` | `number` | **Tokens**
Total Tokens | | +| `nemotron.profile` | `string` | **Model**
NVIDIA Nemotron LLM model | `"nemotron-3-super"` | +| `nemotron.serverbase` | `string` | **Server base URL**
OpenAI-compatible base URL for the Nemotron endpoint (e.g. https://integrate.api.nvidia.com/v1 for NVIDIA's cloud API, http://localhost:8000/v1 for a self-hosted NIM / vLLM server). | `"https://integrate.api.nvidia.com/v1"` | + +## Dependencies + +- `openai` +- `langchain-openai` +- `langchain-core` +- `langchain` + +## Source + +[ View source](https://github.com/rocketride-org/rocketride-server/tree/develop/nodes/src/nodes/llm_nemotron) + diff --git a/nodes/src/nodes/llm_nemotron/__init__.py b/nodes/src/nodes/llm_nemotron/__init__.py new file mode 100644 index 000000000..a1d64b537 --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/__init__.py @@ -0,0 +1,41 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +from .IGlobal import IGlobal +from .IInstance import IInstance + + +def getChat(): + """ + Get the Chat class from the module. + """ + from .nemotron import Chat + + return Chat + + +__all__ = [ + 'IGlobal', + 'IInstance', + 'getChat', +] diff --git a/nodes/src/nodes/llm_nemotron/nemotron.py b/nodes/src/nodes/llm_nemotron/nemotron.py new file mode 100644 index 000000000..04c37fc07 --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/nemotron.py @@ -0,0 +1,88 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +""" +NVIDIA Nemotron binding for the ChatLLM. +""" + +import re +from typing import Any, Dict +from ai.common.chat import ChatBase +from ai.common.config import Config +from langchain_openai import ChatOpenAI + +# Nemotron reasoning models can return chain-of-thought wrapped in +# ... inside the `content` field on the OpenAI-compatible +# endpoint. The block is stripped here so downstream pipeline nodes only see +# the final answer. Mirrors the llm_minimax pattern. +_THINK_BLOCK_RE = re.compile(r'.*?\s*', re.DOTALL | re.IGNORECASE) + + +class Chat(ChatBase): + """ + Creates an NVIDIA Nemotron chat bot. + """ + + _llm: ChatOpenAI + + def __init__(self, provider: str, connConfig: Dict[str, Any], bag: Dict[str, Any]): + """Initialize the NVIDIA Nemotron chat bot. + + Args: + provider (str): Provider name + connConfig (Dict[str, Any]): Node configuration + bag (Dict[str, Any]): Bag to store data + """ + # Init the base + super().__init__(provider, connConfig, bag) + + # Get the nodes configuration + config = Config.getNodeConfig(provider, connConfig) + + # Get the serverbase url; fall back to the NVIDIA API default so runtime + # accepts the same "no explicit serverbase" config that validateConfig does + serverbase = config.get('serverbase') or 'https://integrate.api.nvidia.com/v1' + + # Get the api key, use a dummy key if not provided. Local profiles + # (NIM / vLLM / SGLang) intentionally have no apikey property — local + # OpenAI-compatible servers accept any token. Mirrors the llm_minimax pattern. + apikey = config.get('apikey') or 'sk-local-dummy-key' + + # API key is only required when calling NVIDIA's cloud API. NVIDIA keys + # use the 'nvapi-' prefix but other formats exist, so only presence is + # enforced (the llm_baidu_qianfan lesson: don't over-validate key format). + if 'api.nvidia.com' in serverbase and apikey == 'sk-local-dummy-key': + raise ValueError('NVIDIA API key is required for cloud profiles.') + + # Get the llm via the OpenAI-compatible client + self._llm = ChatOpenAI( + model=self._model, base_url=serverbase, api_key=apikey, temperature=0, max_tokens=self._modelOutputTokens + ) + + # Save our chat class into the bag + bag['chat'] = self + + def _chat(self, prompt: str) -> str: + """Invoke the LLM and strip any ... reasoning block from the response.""" + results = self._llm.invoke(prompt) + return _THINK_BLOCK_RE.sub('', results.content) diff --git a/nodes/src/nodes/llm_nemotron/nemotron.svg b/nodes/src/nodes/llm_nemotron/nemotron.svg new file mode 100644 index 000000000..d1537e7e8 --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/nemotron.svg @@ -0,0 +1 @@ + diff --git a/nodes/src/nodes/llm_nemotron/requirements.txt b/nodes/src/nodes/llm_nemotron/requirements.txt new file mode 100644 index 000000000..005b251cf --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/requirements.txt @@ -0,0 +1,7 @@ +# Plugin NVIDIA Nemotron core +openai +langchain-openai + +# LangChain deps +langchain-core +langchain diff --git a/nodes/src/nodes/llm_nemotron/services.json b/nodes/src/nodes/llm_nemotron/services.json new file mode 100644 index 000000000..9d75b0de6 --- /dev/null +++ b/nodes/src/nodes/llm_nemotron/services.json @@ -0,0 +1,294 @@ +{ + // + // Required: + // The displayable name of this node + // + "title": "Nemotron (NVIDIA)", + // + // Required: + // The protocol is the endpoint protocol + // + "protocol": "llm_nemotron://", + // + // Required: + // Class type of the node - what it does + // + "classType": ["llm"], + // + // Required: + // Capabilities are flags that change the behavior of the underlying + // engine + // + "capabilities": ["invoke"], + // + // Optional: + // Register is either filter, endpoint or ignored if not specified. If the + // type is specified, a factory is registered of that given type + // + "register": "filter", + // + // Optional: + // The node is the actual physical node to instantiate - if + // not specified, the protocol will be used + // + "node": "python", + // + // Optional: + // The path is the executable/script code - it is node dependent + // and is optional for most node + // + "path": "nodes.llm_nemotron", + // + // Required: + // The prefix map when added/removed when convertting URLs <=> paths + // + "prefix": "llm", + // + // Optional: + // Description to of this driver + // + "description": ["A component that connects to NVIDIA's Nemotron large language models for ", "advanced natural language processing. The Nemotron 3 family are open-weight ", "hybrid Mamba-Transformer MoE reasoning models with configurable thinking ", "budgets and up to a 1M-token context window: Ultra for frontier multi-agent ", "reasoning, Super for the best efficiency/accuracy balance, and Nano for ", "fast sub-agent workloads. Models are served through NVIDIA's OpenAI-compatible ", "cloud API (build.nvidia.com) or self-hosted via NIM containers, vLLM, or ", "SGLang using the same model IDs."], + // + // Optional: + // The icon is the icon to display in the UI for this node + // + "icon": "nemotron.svg", + "documentation": "https://docs.rocketride.org", + // + // + // Optional: + // Rendering hints to the UI which indicate which fields of + // the configuration should be used to display information + // + "tile": ["Model: ${parameters.llm_nemotron.profile}"], + // + // Optional: + // As a pipe component, define what this pipe component takes + // and what it produces + // + "lanes": { + "questions": ["answers"] + }, + // + // Optional: + // Profile section are configuration optoins used by the driver + // itself + // + "preconfig": { + // Define the values that will be merged into any profile configuration + // specified, unless the profile is 'absolute' + "default": "nemotron-3-super", + // Defines profiles used with the "profile": key + "profiles": { + "custom": { + "title": "Custom Model", + "model": "", + "modelTotalTokens": 131072, + "modelOutputTokens": 8192, + "serverbase": "https://integrate.api.nvidia.com/v1", + "apikey": "" + }, + "nemotron-3-super": { + "title": "Nemotron 3 Super 120B", + "model": "nvidia/nemotron-3-super-120b-a12b", + "modelSource": "provider", + "modelTotalTokens": 1000000, // manual - NVIDIA model card + "modelOutputTokens": 32768, // manual + "serverbase": "https://integrate.api.nvidia.com/v1", + "apikey": "", + "capabilities": { + "reasoning": true + } + }, + "nemotron-3-ultra": { + "title": "Nemotron 3 Ultra 550B", + "model": "nvidia/nemotron-3-ultra-550b-a55b", + "modelSource": "provider", + "modelTotalTokens": 1000000, // manual - NVIDIA model card + "modelOutputTokens": 32768, // manual + "serverbase": "https://integrate.api.nvidia.com/v1", + "apikey": "", + "capabilities": { + "reasoning": true + } + }, + "nemotron-3-nano": { + "title": "Nemotron 3 Nano 30B", + "model": "nvidia/nemotron-3-nano-30b-a3b", + "modelSource": "provider", + "modelTotalTokens": 262144, // manual - NVIDIA model card + "modelOutputTokens": 32768, // manual + "serverbase": "https://integrate.api.nvidia.com/v1", + "apikey": "", + "capabilities": { + "reasoning": true + } + }, + // Self-hosted NIM containers serve the same model IDs as the hosted + // NVIDIA API, so local profiles reuse the cloud model strings. + "nemotron-3-super-local": { + "title": "Nemotron 3 Super 120B (Local)", + "model": "nvidia/nemotron-3-super-120b-a12b", + "modelSource": "provider", + "modelTotalTokens": 1000000, // manual + "modelOutputTokens": 32768, // manual + "serverbase": "http://localhost:8000/v1", + "capabilities": { + "reasoning": true + } + }, + "nemotron-3-nano-local": { + "title": "Nemotron 3 Nano 30B (Local)", + "model": "nvidia/nemotron-3-nano-30b-a3b", + "modelSource": "provider", + "modelTotalTokens": 262144, // manual + "modelOutputTokens": 32768, // manual + "serverbase": "http://localhost:8000/v1", + "capabilities": { + "reasoning": true + } + } + } + }, + // + // Optional: + // Local fields definitions - these define fields only for the + // current service. You may specify them here, or directly + // in the shape + // + "fields": { + "model": { + "type": "string", + "title": "Model", + "description": "NVIDIA Nemotron model" + }, + "modelTotalTokens": { + "type": "number", + "title": "Tokens", + "description": "Total Tokens" + }, + "nemotron.serverbase": { + "type": "string", + "title": "Server base URL", + "description": "OpenAI-compatible base URL for the Nemotron endpoint (e.g. https://integrate.api.nvidia.com/v1 for NVIDIA's cloud API, http://localhost:8000/v1 for a self-hosted NIM / vLLM server).", + "default": "https://integrate.api.nvidia.com/v1" + }, + "nemotron.custom": { + "object": "custom", + "properties": [ + "model", + "modelTotalTokens", + "nemotron.serverbase", + "llm.cloud.apikey", + "llm.cloud.modelSource" + ] + }, + "nemotron.nemotron-3-super": { + "object": "nemotron-3-super", + "properties": [ + "llm.cloud.apikey", + "llm.cloud.modelSource" + ] + }, + "nemotron.nemotron-3-ultra": { + "object": "nemotron-3-ultra", + "properties": [ + "llm.cloud.apikey", + "llm.cloud.modelSource" + ] + }, + "nemotron.nemotron-3-nano": { + "object": "nemotron-3-nano", + "properties": [ + "llm.cloud.apikey", + "llm.cloud.modelSource" + ] + }, + "nemotron.nemotron-3-super-local": { + "object": "nemotron-3-super-local", + "properties": [ + "llm.local.serverbase" + ] + }, + "nemotron.nemotron-3-nano-local": { + "object": "nemotron-3-nano-local", + "properties": [ + "llm.local.serverbase" + ] + }, + "nemotron.profile": { + "title": "Model", + "description": "NVIDIA Nemotron LLM model", + "type": "string", + "default": "nemotron-3-super", + "enum": [ + "*>preconfig.profiles.*.title" + ], + "conditional": [ + { + "value": "custom", + "properties": [ + "nemotron.custom" + ] + }, + { + "value": "nemotron-3-super", + "properties": [ + "nemotron.nemotron-3-super" + ] + }, + { + "value": "nemotron-3-ultra", + "properties": [ + "nemotron.nemotron-3-ultra" + ] + }, + { + "value": "nemotron-3-nano", + "properties": [ + "nemotron.nemotron-3-nano" + ] + }, + { + "value": "nemotron-3-super-local", + "properties": [ + "nemotron.nemotron-3-super-local" + ] + }, + { + "value": "nemotron-3-nano-local", + "properties": [ + "nemotron.nemotron-3-nano-local" + ] + } + ] + } + }, + // + // Required: + // Defines the fields (shape) of the service. Either source or target + // map be specified, or both, but at least one is required + // + "shape": [ + { + "section": "Pipe", + "title": "Nemotron (NVIDIA)", + "properties": ["nemotron.profile"] + } + ], + "test": { + "profiles": ["nemotron-3-super"], + "outputs": ["answers"], + "cases": [ + { + "name": "LLM returns mock response", + "text": "What is 2+2?", + "expect": { + "answers": { + "contains": "Mock LLM response" + } + } + } + ] + } +} diff --git a/nodes/test/framework/pipeline.py b/nodes/test/framework/pipeline.py index adb33e5b8..66b70742b 100644 --- a/nodes/test/framework/pipeline.py +++ b/nodes/test/framework/pipeline.py @@ -130,6 +130,7 @@ def _parse_credential_env_var(env_var: str, provider: str) -> Optional[str]: 'llm_qwen': {'apikey': 'sk-mock-placeholder-for-tests'}, 'llm_minimax': {'apikey': 'sk-mock-placeholder-for-tests'}, 'llm_baidu_qianfan': {'apikey': 'mock-baidu-qianfan-placeholder-for-tests'}, + 'llm_nemotron': {'apikey': 'nvapi-mock-placeholder-for-tests'}, 'llm_vision_ollama': {'apikey': 'sk-mock-placeholder-for-tests'}, 'rerank_cohere': {'apikey': 'mock-cohere-placeholder-for-tests'}, 'tool_apify': {'apikey': 'mock-apify-placeholder-for-tests'}, diff --git a/tools/sync_models/README.md b/tools/sync_models/README.md index 403409d84..b5aa67185 100644 --- a/tools/sync_models/README.md +++ b/tools/sync_models/README.md @@ -81,6 +81,7 @@ python tools/sync_models/src/sync_models.py --provider llm_openai --model-source | `llm_minimax` | `llm_minimax` | `ROCKETRIDE_MINIMAX_KEY` | | `llm_kimi` | `llm_kimi` | `ROCKETRIDE_KIMI_KEY` | | `llm_baidu_qianfan` | `llm_baidu_qianfan` | `ROCKETRIDE_BAIDU_QIANFAN_KEY` | +| `llm_nemotron` | `llm_nemotron` | `ROCKETRIDE_NVIDIA_KEY` | If an API key env var is not set the provider is skipped with a warning (not an error). Set keys in a `.env` file in the repo root or export them in the shell. diff --git a/tools/sync_models/src/providers/nemotron.py b/tools/sync_models/src/providers/nemotron.py new file mode 100644 index 000000000..c63416bb8 --- /dev/null +++ b/tools/sync_models/src/providers/nemotron.py @@ -0,0 +1,61 @@ +""" +NVIDIA Nemotron provider handler (Handler A) — cloud models only. + +Fetches models from the NVIDIA /v1/models endpoint and syncs the cloud +profiles (nemotron-3-super, nemotron-3-ultra, nemotron-3-nano) into +nodes/src/nodes/llm_nemotron/services.json. + +The NVIDIA API (build.nvidia.com) is OpenAI-compatible, so the openai SDK can +be used with a custom base_url. The endpoint lists the full multi-vendor +catalog (Llama, GLM, Kimi, Mistral, ...), so the model_filter in +sync_models.config.json keeps only the nvidia/*nemotron* text chat models; +vision (VL/Omni), safety, parse, retriever, and speech variants are excluded +(they belong in dedicated nodes). +""" + +from __future__ import annotations + +from typing import Dict, Any, List + +from providers.base import CloudProvider + + +class NemotronProvider(CloudProvider): + """ + Handler for cloud (API) models in the llm_nemotron node. + + The NVIDIA API is OpenAI-compatible, so the openai SDK can be used + with a custom base_url. + """ + + provider_name = 'llm_nemotron' + display_name = 'Nemotron (NVIDIA)' + smoke_type = 'chat_openai_compat' + + def make_client(self, api_key: str) -> object: + """ + Args: + api_key: NVIDIA API key (nvapi-...) + + Returns: + openai.OpenAI client pointed at the NVIDIA endpoint + """ + import openai + + return openai.OpenAI( + api_key=api_key, + base_url='https://integrate.api.nvidia.com/v1', + ) + + def fetch_models(self, client: object) -> List[Dict[str, Any]]: + """ + Fetch available models from the NVIDIA API. + + Args: + client: openai.OpenAI instance with the NVIDIA base_url + + Returns: + List of model dicts with {"id": str} + """ + response = client.models.list() # type: ignore[attr-defined] + return [{'id': m.id} for m in response.data] diff --git a/tools/sync_models/src/sync_models.config.json b/tools/sync_models/src/sync_models.config.json index f6dfca033..b4b7c8b66 100644 --- a/tools/sync_models/src/sync_models.config.json +++ b/tools/sync_models/src/sync_models.config.json @@ -501,6 +501,46 @@ "ernie-4.5-turbo-32k": 32768, "ernie-5.0-thinking-preview": 128000 } + }, + // ---------------------------------------------------------------------- + // NVIDIA Nemotron — Nemotron text chat models through the OpenAI-compatible + // build.nvidia.com API. The /v1/models endpoint lists the full multi-vendor + // catalog (Llama, GLM, Kimi, Mistral, ...), so the include filter keeps only + // the nvidia/*nemotron* family; vision (VL/Omni), safety, parse, retriever, + // and speech variants are excluded (they belong in dedicated nodes). + // ---------------------------------------------------------------------- + "llm_nemotron": { + "handler": "cloud", + "env_var": "ROCKETRIDE_NVIDIA_KEY", + "module": "nemotron", + "models_endpoint": "https://integrate.api.nvidia.com/v1/models", + "auth_header": "Authorization", + "auth_prefix": "Bearer", + "smoke_test": "chat", + "model_filter": { + "include_prefixes": ["nvidia/nemotron", "nvidia/llama-3.1-nemotron", "nvidia/llama-3.3-nemotron"], + "exclude_prefixes": [], + "exclude_patterns": ["vl", "vision", "omni", "safety", "guard", "parse", "ocr", "embed", "rerank", "retriever", "asr", "tts", "speech", "audio", "reward"] + }, + // Local NIM profiles reuse the cloud model IDs, so they resolve against + // the live API too — protect them (and the seeded cloud set) so the + // keyless OpenRouter fallback can never wrongly retire them. + "protected_profiles": [ + ["custom", "2126-04-09"], + ["nemotron-3-super", "2026-11-01"], + ["nemotron-3-ultra", "2026-11-01"], + ["nemotron-3-nano", "2026-11-01"], + ["nemotron-3-super-local", "2026-11-01"], + ["nemotron-3-nano-local", "2026-11-01"] + ], + // NVIDIA's /v1/models omits context_window; lock the seeded models at + // their model-card values. + "default_context_window": 131072, + "token_limit_overrides": { + "nvidia/nemotron-3-super-120b-a12b": 1000000, + "nvidia/nemotron-3-ultra-550b-a55b": 1000000, + "nvidia/nemotron-3-nano-30b-a3b": 262144 + } } }, // -------------------------------------------------------------------------- @@ -544,6 +584,8 @@ "minimax-": "MiniMax ", "kimi-": "Kimi ", "moonshot-": "Moonshot ", + "nvidia/nemotron-": "Nemotron ", + "nvidia/llama-": "Llama ", "text-embedding-": "Text Embedding ", "o1": "o1", "o3": "o3", @@ -605,7 +647,13 @@ // // Baidu Qianfan "ernie-4.5-turbo-128k": 4096, "ernie-4.5-turbo-32k": 4096, - "ernie-5.0-thinking-preview": 8192 + "ernie-5.0-thinking-preview": 8192, + // NVIDIA Nemotron — reasoning models with configurable thinking budgets; + // NVIDIA's /v1/models publishes no output limit, so pin a budget large + // enough that thinking + answer are not truncated. + "nvidia/nemotron-3-super-120b-a12b": 32768, + "nvidia/nemotron-3-ultra-550b-a55b": 32768, + "nvidia/nemotron-3-nano-30b-a3b": 32768 } }, // -------------------------------------------------------------------------- diff --git a/tools/sync_models/src/sync_models.py b/tools/sync_models/src/sync_models.py index 402f3ab03..908150050 100644 --- a/tools/sync_models/src/sync_models.py +++ b/tools/sync_models/src/sync_models.py @@ -67,6 +67,7 @@ 'llm_minimax': 'providers.minimax:MiniMaxProvider', 'llm_kimi': 'providers.kimi:KimiProvider', 'llm_baidu_qianfan': 'providers.baidu_qianfan:BaiduQianfanProvider', + 'llm_nemotron': 'providers.nemotron:NemotronProvider', } # Maps provider name → relative path to its services.json from the repo root @@ -83,6 +84,7 @@ 'llm_minimax': 'nodes/src/nodes/llm_minimax/services.json', 'llm_kimi': 'nodes/src/nodes/llm_kimi/services.json', 'llm_baidu_qianfan': 'nodes/src/nodes/llm_baidu_qianfan/services.json', + 'llm_nemotron': 'nodes/src/nodes/llm_nemotron/services.json', } # Default extra fields added to every new profile (placeholder for API key) diff --git a/tools/sync_models/test/markers.py b/tools/sync_models/test/markers.py index 4683ad52c..7260651a4 100644 --- a/tools/sync_models/test/markers.py +++ b/tools/sync_models/test/markers.py @@ -58,3 +58,8 @@ not os.environ.get('ROCKETRIDE_BAIDU_QIANFAN_KEY'), reason='ROCKETRIDE_BAIDU_QIANFAN_KEY not set', ) + +requires_nvidia = pytest.mark.skipif( + not os.environ.get('ROCKETRIDE_NVIDIA_KEY'), + reason='ROCKETRIDE_NVIDIA_KEY not set', +) diff --git a/tools/sync_models/test/test_sync_live.py b/tools/sync_models/test/test_sync_live.py index a1f56b059..3a18a63b5 100644 --- a/tools/sync_models/test/test_sync_live.py +++ b/tools/sync_models/test/test_sync_live.py @@ -32,6 +32,7 @@ requires_qwen, requires_minimax, requires_baidu_qianfan, + requires_nvidia, ) from core.patcher import get_profiles @@ -277,3 +278,24 @@ def test_baidu_qianfan_profiles_exist_in_api(): base_url='https://qianfan.baidubce.com/v2', ) _check_missing_models(profiles, live_ids, 'llm_baidu_qianfan') + + +# --------------------------------------------------------------------------- +# NVIDIA Nemotron +# --------------------------------------------------------------------------- + + +@requires_nvidia +def test_nemotron_profiles_exist_in_api(): + """Every non-deprecated llm_nemotron profile model ID must be in the live API. + + Local NIM profiles reuse the cloud model IDs, so they resolve against the + live NVIDIA API as well. + """ + api_key = os.environ['ROCKETRIDE_NVIDIA_KEY'] + profiles = _load_profiles('llm_nemotron') + live_ids = _fetch_openai_model_ids( + api_key, + base_url='https://integrate.api.nvidia.com/v1', + ) + _check_missing_models(profiles, live_ids, 'llm_nemotron') From ee80a33ac58aa24ed43eb9289051fc85b1df33d8 Mon Sep 17 00:00:00 2001 From: leela8256 Date: Fri, 31 Jul 2026 05:13:01 -0700 Subject: [PATCH 2/5] fix(llm_nemotron): strip unterminated blocks on truncated generations A generation that hits max_tokens mid-reasoning emits no closing tag; make the closing tag optional so partial reasoning never leaks downstream. Add unit tests for terminated, unterminated, multiple, and absent think blocks (stub-loaded, no heavy deps). Co-Authored-By: Claude Fable 5 --- nodes/src/nodes/llm_nemotron/nemotron.py | 6 +- nodes/test/test_llm_nemotron_think_strip.py | 91 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 nodes/test/test_llm_nemotron_think_strip.py diff --git a/nodes/src/nodes/llm_nemotron/nemotron.py b/nodes/src/nodes/llm_nemotron/nemotron.py index 04c37fc07..b2e046508 100644 --- a/nodes/src/nodes/llm_nemotron/nemotron.py +++ b/nodes/src/nodes/llm_nemotron/nemotron.py @@ -34,8 +34,10 @@ # Nemotron reasoning models can return chain-of-thought wrapped in # ... inside the `content` field on the OpenAI-compatible # endpoint. The block is stripped here so downstream pipeline nodes only see -# the final answer. Mirrors the llm_minimax pattern. -_THINK_BLOCK_RE = re.compile(r'.*?\s*', re.DOTALL | re.IGNORECASE) +# the final answer. The closing tag is optional: a generation truncated at +# max_tokens mid-reasoning has no , and the partial reasoning must +# not leak downstream either. +_THINK_BLOCK_RE = re.compile(r'.*?(?:|$)\s*', re.DOTALL | re.IGNORECASE) class Chat(ChatBase): diff --git a/nodes/test/test_llm_nemotron_think_strip.py b/nodes/test/test_llm_nemotron_think_strip.py new file mode 100644 index 000000000..bdf562ec6 --- /dev/null +++ b/nodes/test/test_llm_nemotron_think_strip.py @@ -0,0 +1,91 @@ +"""Unit tests for llm_nemotron's -block stripping. + +Loads nodes/src/nodes/llm_nemotron/nemotron.py with stubbed heavy imports +(ai.common, langchain_openai), same approach as +test_baidu_qianfan_global_validation.py, and verifies that reasoning content +never leaks downstream — including when generation is truncated at max_tokens +before the closing tag is emitted. +""" + +from __future__ import annotations + +import importlib.util +import sys +import types +from pathlib import Path + + +def _load_nemotron(monkeypatch, response_content: str): + ai_module = types.ModuleType('ai') + common_module = types.ModuleType('ai.common') + chat_module = types.ModuleType('ai.common.chat') + config_module = types.ModuleType('ai.common.config') + langchain_openai_module = types.ModuleType('langchain_openai') + + class ChatBase: + def __init__(self, _provider, _conn_config, _bag): + self._model = 'nvidia/nemotron-3-super-120b-a12b' + self._modelOutputTokens = 32768 + + class Config: + @staticmethod + def getNodeConfig(_logical_type, _conn_config): + return { + 'apikey': 'nvapi-test-key', + 'model': 'nvidia/nemotron-3-super-120b-a12b', + 'serverbase': 'https://integrate.api.nvidia.com/v1', + } + + class ChatOpenAI: + def __init__(self, **_kwargs): + pass + + def invoke(self, _prompt): + return types.SimpleNamespace(content=response_content) + + chat_module.ChatBase = ChatBase + config_module.Config = Config + langchain_openai_module.ChatOpenAI = ChatOpenAI + ai_module.common = common_module + common_module.chat = chat_module + common_module.config = config_module + + monkeypatch.setitem(sys.modules, 'ai', ai_module) + monkeypatch.setitem(sys.modules, 'ai.common', common_module) + monkeypatch.setitem(sys.modules, 'ai.common.chat', chat_module) + monkeypatch.setitem(sys.modules, 'ai.common.config', config_module) + monkeypatch.setitem(sys.modules, 'langchain_openai', langchain_openai_module) + + module_path = Path(__file__).parent.parent / 'src' / 'nodes' / 'llm_nemotron' / 'nemotron.py' + spec = importlib.util.spec_from_file_location('nemotron_under_test', module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _chat(monkeypatch, response_content: str) -> str: + module = _load_nemotron(monkeypatch, response_content) + chat = module.Chat('llm_nemotron', {}, {}) + return chat._chat('test prompt') + + +def test_terminated_think_block_is_stripped(monkeypatch): + answer = _chat(monkeypatch, 'step 1... step 2...The answer is 4.') + assert answer == 'The answer is 4.' + + +def test_unterminated_think_block_is_stripped(monkeypatch): + # Generation truncated at max_tokens mid-reasoning: no closing tag. The + # partial reasoning must not leak downstream as the answer. + answer = _chat(monkeypatch, 'step 1... step 2... and then we') + assert answer == '' + + +def test_plain_answer_passes_through(monkeypatch): + answer = _chat(monkeypatch, 'The answer is 4.') + assert answer == 'The answer is 4.' + + +def test_multiple_think_blocks_are_stripped(monkeypatch): + answer = _chat(monkeypatch, 'aFirst. bSecond.') + assert answer == 'First. Second.' From 1ec7454616849e6ab430c8159f11725b9485df28 Mon Sep 17 00:00:00 2001 From: leela8256 Date: Fri, 31 Jul 2026 05:19:58 -0700 Subject: [PATCH 3/5] test(llm_nemotron): add docstring to stub-loader test helper Co-Authored-By: Claude Fable 5 --- nodes/test/test_llm_nemotron_think_strip.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nodes/test/test_llm_nemotron_think_strip.py b/nodes/test/test_llm_nemotron_think_strip.py index bdf562ec6..6ddbe757c 100644 --- a/nodes/test/test_llm_nemotron_think_strip.py +++ b/nodes/test/test_llm_nemotron_think_strip.py @@ -16,6 +16,12 @@ def _load_nemotron(monkeypatch, response_content: str): + """Load nemotron.py from source with stubbed dependencies. + + Installs fake ai.common.chat / ai.common.config / langchain_openai + modules in sys.modules, with the stubbed LLM's invoke() returning + ``response_content``, then imports the node module for testing. + """ ai_module = types.ModuleType('ai') common_module = types.ModuleType('ai.common') chat_module = types.ModuleType('ai.common.chat') From e7ac4bf01a13a7a7173d95f0bb5e11cbc2f5cb7c Mon Sep 17 00:00:00 2001 From: Leela8256 Date: Fri, 31 Jul 2026 14:28:50 -0700 Subject: [PATCH 4/5] Update nodes/src/nodes/llm_nemotron/nemotron.py Co-authored-by: Alexandru Sclearuc --- nodes/src/nodes/llm_nemotron/nemotron.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nodes/src/nodes/llm_nemotron/nemotron.py b/nodes/src/nodes/llm_nemotron/nemotron.py index b2e046508..435ef1a85 100644 --- a/nodes/src/nodes/llm_nemotron/nemotron.py +++ b/nodes/src/nodes/llm_nemotron/nemotron.py @@ -86,5 +86,4 @@ def __init__(self, provider: str, connConfig: Dict[str, Any], bag: Dict[str, Any def _chat(self, prompt: str) -> str: """Invoke the LLM and strip any ... reasoning block from the response.""" - results = self._llm.invoke(prompt) - return _THINK_BLOCK_RE.sub('', results.content) + return _THINK_BLOCK_RE.sub('', super()._chat(prompt)) From 46cfe6e57ce3271ad346e28fcb2c3372c6d09397 Mon Sep 17 00:00:00 2001 From: leela8256 Date: Fri, 31 Jul 2026 17:59:12 -0700 Subject: [PATCH 5/5] test(llm_nemotron): stub ChatBase._chat to match super()._chat delegation The node's _chat now delegates the model call to ChatBase._chat (which forwards agent stop sequences); teach the test stub the same method so the tests exercise the delegated call path. Co-Authored-By: Claude Fable 5 --- nodes/test/test_llm_nemotron_think_strip.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nodes/test/test_llm_nemotron_think_strip.py b/nodes/test/test_llm_nemotron_think_strip.py index 6ddbe757c..3c26b98d0 100644 --- a/nodes/test/test_llm_nemotron_think_strip.py +++ b/nodes/test/test_llm_nemotron_think_strip.py @@ -33,6 +33,10 @@ def __init__(self, _provider, _conn_config, _bag): self._model = 'nvidia/nemotron-3-super-120b-a12b' self._modelOutputTokens = 32768 + def _chat(self, prompt: str) -> str: + """Mirror ai.common.chat.ChatBase._chat: invoke the LLM, return its content.""" + return self._llm.invoke(prompt).content + class Config: @staticmethod def getNodeConfig(_logical_type, _conn_config):