Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/sync-models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/README-nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
151 changes: 151 additions & 0 deletions nodes/src/nodes/llm_nemotron/IGlobal.py
Original file line number Diff line number Diff line change
@@ -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 <status>:".
- 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()
28 changes: 28 additions & 0 deletions nodes/src/nodes/llm_nemotron/IInstance.py
Original file line number Diff line number Diff line change
@@ -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
93 changes: 93 additions & 0 deletions nodes/src/nodes/llm_nemotron/README.md
Original file line number Diff line number Diff line change
@@ -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 `<think>...</think>` 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.

---

<!-- ROCKETRIDE:GENERATED:PARAMS START -->
<!-- Generated by nodes:docs-generate. Do not edit by hand. -->

## Schema

| Field | Type | Description | Default |
|---|---|---|---|
| `model` | `string` | **Model**<br/>NVIDIA Nemotron model | |
| `modelTotalTokens` | `number` | **Tokens**<br/>Total Tokens | |
| `nemotron.profile` | `string` | **Model**<br/>NVIDIA Nemotron LLM model | `"nemotron-3-super"` |
| `nemotron.serverbase` | `string` | **Server base URL**<br/>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

[<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true" style="vertical-align:-0.15em;margin-right:0.35em"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> View source](https://github.com/rocketride-org/rocketride-server/tree/develop/nodes/src/nodes/llm_nemotron)
<!-- ROCKETRIDE:GENERATED:PARAMS END -->
41 changes: 41 additions & 0 deletions nodes/src/nodes/llm_nemotron/__init__.py
Original file line number Diff line number Diff line change
@@ -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',
]
Loading
Loading