Skip to navigation

Configure

View as Markdown

Use this guide to configure plugins in plugins.toml, either by editing the file directly or with nemo-relay plugins edit. The runtime discovers and layers this generic plugin document for direct Rust, Python, and Node.js integrations as well as for the nemo-relay CLI gateway. The file encodes the same document that binding APIs accept, but uses TOML at the file root.

This guide covers file locations, the interactive editor, discovery, and merge rules. Use Manage to install and manage discoverable plugins with the CLI, or Initialize for SDK initialization. Each component guide documents its specific fields.

NeMo Relay plugin configuration keys use snake_case regardless of language or file format. Node.js helper APIs can have camelCase function names, but the generic plugin document and component-local config objects use canonical snake_case keys.

Gateway Editing Files

Use the interactive editor for Observability, Adaptive, PII Redaction, and existing NeMo Guardrails configuration. The built-in nemo_guardrails component is deprecated and scheduled for removal in NeMo Relay 0.9; do not use the editor to start a new Guardrails deployment. The editor also updates the configuration of manifest-backed dynamic plugins that plugins add has registered:

nemo-relay plugins edit

By default, the editor writes the user plugin file:

$XDG_CONFIG_HOME/nemo-relay/plugins.toml

or:

~/.config/nemo-relay/plugins.toml

When the top-level CLI receives --plugin-config-path, the editor uses that exact file. Otherwise, --config path/to/config.toml makes the editor use the sibling path/to/plugins.toml, matching the runtime selection for that configuration. This explicit file replaces the user editor target, so plugins edit --user keeps the inherited explicit target. Use --global to edit the system layer. These rules only select the file opened by the editor; they do not change runtime discovery, layering, or merge precedence.

Use a scope flag to edit another location:

nemo-relay plugins edit --global

Scope flags are mutually exclusive.

--global writes /etc/nemo-relay/plugins.toml on Unix or %ProgramData%\nemo-relay\plugins.toml on Windows and usually requires elevated filesystem permissions.

The editor menus support these controls:

KeyBehavior
Arrow keys, j, kMove through menu items.
Enter, SpaceSelect or toggle the highlighted item.
Backspace, DeleteClear the highlighted optional field.
rReset the highlighted field or section to its default.
pPreview TOML from the main menu.
sSave from the main menu.
?Show help.
q, EscGo back or cancel without saving.

Text and JSON value prompts use normal line editing. Use the surrounding field menu to reset, clear, preview, or save.

Shortest Path Example

Use this minimal plugins.toml when you want Relay to start with one plugin-managed observability exporter and no extra layering:

version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 4
[components.config.atof]
enabled = true
[[components.config.atof.sinks]]
type = "file"
output_directory = "logs"
filename = "events.jsonl"
mode = "append"

For the most deterministic CLI-gateway verification path, keep plugins.toml in the same directory as the gateway config.toml file for this run, then launch the wrapper with that explicit config path. For example:

path/to/
config.toml
plugins.toml

Run the gateway with the following command:

nemo-relay --config path/to/config.toml run -- codex

This uses the colocated plugins.toml instead of the ambient user plugin file. The system file still layers on top, so ensure system policy does not override the exporter you want to verify. The plugin file is the configuration being demonstrated here; --config only tells the gateway which low plugin layer to use for this run. For implicit discovery, place the file in the XDG user configuration directory. Refer to CLI Basic Usage for the wrapper command shapes.

What Success Looks Like

The shortest-path setup is working when:

  • nemo-relay starts without plugin validation or activation errors.
  • The observability component activates the ATOF exporter configured in this file.
  • An instrumented gateway run writes ATOF JSONL output to logs/events.jsonl.

After that path works, expand the config with additional components, policies, or higher-precedence files as needed.

File Shape

plugins.toml uses the canonical plugin document shape:

version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 4
[components.config.atof]
enabled = true
[[components.config.atof.sinks]]
type = "file"
output_directory = "logs"
filename = "events.jsonl"
mode = "append"
[policy]
unknown_component = "warn"
unknown_field = "warn"
unsupported_value = "error"

The top-level fields are:

FieldDefaultNotes
version1Plugin configuration format version. Non-1 versions fail validation by default.
components[]Ordered plugin components to validate and activate.
policywarn unknown components and fields, error on unsupported valuesGlobal host validation policy. Non-default field/value settings override the matching policy for built-in components.

Each built-in component has:

FieldDefaultNotes
kindRequiredRegistered plugin kind, such as observability or adaptive.
enabledtrueRelay validates disabled components but does not initialize them.
config{}Component-local configuration object. The shape depends on kind.

Gateway Discoverable Plugin Records

Use [[plugins.dynamic]] for a native or worker plugin that has a manifest. The CLI gateway and SDK initialization read these records from plugins.toml. Keep them separate from [[components]] entries for built-in plugins or plugins that application code registers. At startup, Relay loads each selected plugin and creates its component. It then validates the component’s settings.

The following record configures a dynamic plugin:

[[plugins.dynamic]]
manifest = "./plugins/acme/relay-plugin.toml"
[plugins.dynamic.config]
mode = "audit"
[plugins.dynamic.config.executor]
worker_threads = 4

For a Python worker, run nemo-relay plugins add <manifest> instead of adding this record manually. The command creates and records the Relay-managed Python environment that the worker requires at startup.

Each dynamic record accepts only these fields:

FieldDefaultNotes
manifestRequiredPath to relay-plugin.toml, relative to this plugins.toml file. The manifest defines the plugin ID, kind, and how Relay loads it.
config{}Plugin settings, written as TOML tables and values. The plugin’s schema and validation hook define the fields.

Relay rejects records with unknown fields. Do not put kind, id, or enabled beside manifest. The manifest must include [defaults] with enabled = false. This field does not control startup selection for manual records.

When you use nemo-relay plugins add <manifest>, Relay records the plugin as disabled in .dynamic-plugins.json beside plugins.toml. Use nemo-relay plugins enable <plugin-id> and nemo-relay plugins disable <plugin-id> to change that saved state.

If you add a record manually and no .dynamic-plugins.json file exists beside plugins.toml, Relay selects the plugin at startup. It can activate after validation, trust, and policy checks pass, even though the manifest declares enabled = false. If the state file exists, Relay selects only plugins with a matching record marked present and enabled. It skips plugins that have no matching state record.

The tables [plugins.dynamic.config] and [plugins.dynamic.config.executor] above belong to the most recent [[plugins.dynamic]] record. Add another [[plugins.dynamic]] header before configuring a second plugin.

Relay passes the contents of config to the plugin as a JSON object. For the example above, that object is:

{
"mode": "audit",
"executor": { "worker_threads": 4 }
}

The plugin receives only this object. Relay also checks this object against the manifest’s optional config_schema. The schema describes mode and executor directly, without an extra config level.

If you omit config, Relay passes an empty object ({}). Validation can still fail if the plugin requires settings. JSON Schema default values do not fill in missing fields. The plugin code must supply its own defaults.

The mode setting belongs to the example plugin. Each plugin defines its own settings.

For Rust native plugins using the typed SDK, config.executor.worker_threads sets the positive worker count for that component’s SDK-owned Tokio executor. When the manifest declares a configuration schema, it must include the SDK-owned executor object and a positive-integer worker_threads property; otherwise static validation and nemo-relay plugins edit reject this supported configuration. Run nemo-relay plugins validate <plugin-id> to validate it against the manifest’s optional static JSON Schema before you enable or run the plugin. Use nemo-relay plugins add, validate, inspect, and enable to manage the dynamic-plugin lifecycle. Refer to Manage for manifest, trust, and policy requirements.

For SDK initialization and activation handles, refer to Initialize.

Worker Timeouts and Payload Limits

Set worker timeouts and message sizes under load in relay-plugin.toml. These settings apply to Python, Rust, and custom-command workers. Keep plugin settings under plugins.dynamic.config in plugins.toml. Native plugins do not use worker timeouts or message limits.

Add the following tables to a worker manifest after setting its [load] runtime and entrypoint:

relay-plugin.toml
[load.callback_timeouts]
default_ms = 60000
[load.callback_timeouts.by_name]
input_guardrail = 90000
[load.max_message_bytes]
default_bytes = 4194304
[load.max_message_bytes.by_name]
input_guardrail = 16777216

The following table lists the available settings and defaults:

SettingDefaultMeaning
load.callback_timeouts.default_ms30000Time limit in milliseconds for short callbacks. Use a positive integer.
load.callback_timeouts.by_name{}Time limits for specific callback names. These replace default_ms for those callbacks.
load.max_message_bytes.default_bytes4194304 (4 MiB)Size limit in bytes for each sent or received message. Control RPCs also use this limit.
load.max_message_bytes.by_name{}Size limits for specific callback names. These replace default_bytes for those callbacks.

Timeouts must be positive whole numbers in milliseconds. Message limits must be whole numbers from 1 through 2,147,483,647 bytes.

In this example, input_guardrail gets a 90-second deadline and a 16 MiB message limit. Other short callbacks get 60 seconds and 4 MiB.

Use the exact name that the plugin registers for each callback. Names are case-sensitive. If several callbacks share a name, the setting applies to all of them. A name that matches no callback has no effect. Names cannot be empty or have spaces at the start or end.

A callback’s deadline includes time spent waiting for calls back to Relay. When the deadline expires, Relay asks the worker to cancel the callback. The callback then follows its normal error or fallback behavior.

Tool and LLM execution intercepts, including streams, ignore callback_timeouts. Their callers control cancellation. Validation and shutdown keep their separate 30-second timeout. Refer to Callback Deadlines.

Payload limits apply to each message, including stream chunks and calls to continuations. They do not limit the total stream size or change model context limits. Relay uses the following settings in order, from highest to lowest priority:

  1. NEMO_RELAY_WORKER_MAX_MESSAGE_BYTES_BY_NAME: a JSON object, such as {"input_guardrail":16777216}.
  2. NEMO_RELAY_WORKER_MAX_MESSAGE_BYTES: a decimal byte limit. Setting it replaces the manifest default and clears its limits for specific callbacks.
  3. Manifest load.max_message_bytes.by_name.
  4. Manifest load.max_message_bytes.default_bytes, then 4 MiB.

Relay sends the final limits to the worker. It rejects invalid limits. Zero does not mean unlimited. After changing the manifest or host environment, reactivate the plugin to apply the changes. For protocol details, refer to the worker message-size contract in the worker protocol guide.

Runtime Discovery

During default discovery, the runtime reads only files named plugins.toml. The runtime resolves plugin configuration from plugins.toml files and an optional code-driven layer. Direct Rust, Python, and Node.js calls to their plugin-initialization APIs use this same discovery and layering behavior.

File configuration comes from plugins.toml:

SourceUse case
plugins.tomlNormal user- and system-managed runtime plugin configuration.

The runtime does not read plugin configuration from config.toml.

Gateway Explicit Config

When the CLI gateway receives --config path/to/config.toml, it scopes plugin file discovery’s low layer to path/to/plugins.toml. An explicit --plugin-config-path selects that low layer directly. Either explicit form replaces the ambient user plugin file; the system file still applies.

Default Discovery Locations

The runtime checks these plugins.toml locations from lowest to highest precedence:

  1. Explicit or user:
    • --plugin-config-path, or the plugins.toml beside --config, when supplied
    • otherwise $XDG_CONFIG_HOME/nemo-relay/plugins.toml, or ~/.config/nemo-relay/plugins.toml when XDG_CONFIG_HOME is not set
  2. System:
    • Unix: /etc/nemo-relay/plugins.toml
    • Windows: %ProgramData%\nemo-relay\plugins.toml

The runtime skips missing files and loads a physical file only once when multiple paths or symlinks select it. If no plugin config source exists, initialization continues without process-level plugin activation. Repository-local .nemo-relay/plugins.toml files are not discovered.

Precedence and Merge Behavior

When more than one plugins.toml file is discovered, later files have higher precedence. System config overrides the selected explicit-or-user config.

TOML tables merge recursively. Top-level lists inside a component’s config concatenate, as do the declared observability destination lists. Entries from the higher-precedence layer are placed before entries from the lower-precedence layer. Other nested lists replace. The observability insecure_http_hosts list is an exception: the highest-precedence layer that sets it replaces the lists below it, and an explicit empty list clears them. For example, the following user configuration enables one ATOF file sink:

# user plugins.toml
[[components]]
kind = "observability"
[components.config]
version = 4
[components.config.atof]
enabled = true
[[components.config.atof.sinks]]
type = "file"
output_directory = "~/.local/state/nemo-relay"
mode = "append"

The system scope can add a fleet sink without repeating the user sink:

# system plugins.toml
[[components]]
kind = "observability"
[components.config.atof]
[[components.config.atof.sinks]]
type = "file"
output_directory = "/var/log/nemo-relay"
mode = "overwrite"

The effective Agent Trajectory Observability Format (ATOF) configuration keeps version and enabled from the user file. Its sinks list contains the system sink first, followed by the user sink.

The top-level components array is special. Relay matches enabled components by kind across files. A higher-precedence component with the same kind merges into the lower-precedence component. Relay adds a component with a different kind to the effective configuration.

Dynamic plugin records are special as well. Relay matches [[plugins.dynamic]] records by the plugin ID declared in their manifests. A higher-precedence record with the same ID replaces the lower-precedence record entirely, including its manifest reference and optional config. This lets system configuration select the approved dynamic plugin package and configuration without inheriting values from the user or explicit file. Declare a plugin ID at most once in each individual plugins.toml file.

A component entry that explicitly sets enabled = false is skipped before matching and merging. It does not change a lower-precedence component’s enabled state or contribute any config fields or list entries. If every layer for a component kind sets enabled = false, that kind is absent from the effective configuration.

This behavior applies to list fields declared at the top level of a component’s config. It also applies to the observability destination lists atof.sinks, opentelemetry.endpoints, opentelemetry.logs.endpoints, opentelemetry.metrics.endpoints, and atif.storage. For example, pricing.sources and PII redaction profiles are top-level component config lists and concatenate across layers. Higher-precedence pricing sources can therefore override one model while still retaining lower-precedence user pricing sources.

Lists nested inside arbitrary structured values are not treated as top-level plugin lists; a higher-precedence value replaces those lists.

Declare each kind at most once inside one plugins.toml file. Duplicate component kinds in the same file fail before merge. Duplicate singleton components that reach plugin validation also fail validation.

Outside component config objects, higher-precedence arrays and scalar values replace lower-precedence values. Tables continue to merge recursively.

Configuration Layering

Plugin settings come from files and code. Files form the base layer, and code sits on top. When the two conflict, code takes precedence. Layering works as follows:

  1. Discover and merge the plugins.toml files from lowest to highest precedence (explicit-or-user → system), using the Precedence And Merge Behavior rules above.
  2. Layer the config object you pass to initialize over that merged base. Any setting it specifies overrides the file value, and the result is the effective config that Relay validates and activates.

Programmatic lists participate in the same concatenation rules. For example, a programmatic config.opentelemetry.endpoints list appears before endpoints inherited from system and explicit-or-user files; it does not remove those file entries. The same higher-precedence-first order applies to config.opentelemetry.logs.endpoints and config.opentelemetry.metrics.endpoints.

For a log or metric signal, an explicitly empty higher-precedence endpoints list clears inherited explicit signal endpoints. An enabled signal then fails validation because its explicit list is empty. Omit the endpoints field to derive destinations from trace endpoints instead.

When library initialization discovers plugins.toml files, Relay emits one plugin.configuration_inherited warning per file. Each warning is written to the operational log and included in the initialization result and active plugin report. It names only the source path and does not include configuration values or credentials. Discovery itself does not block initialization, but validation or activation errors in the effective layered configuration still fail normally.

Files and code differ only in how they treat a setting you omit:

You omitIn a fileIn code
version, policy, or the enabled flag of a component you declareInherited from a lower-precedence fileAlways taken from code — its default if you did not set it
A whole component kind, or a key inside a component’s configInherited from a lower-precedence fileInherited from the file layer

Lower-precedence files fill fields that higher-precedence files omit. Typed code fields always have values, so they override file values. Only component selection and keys inside component config merge with files.

When a component declared in code resolves enabled = true over a discovered enabled = false, initialization and the active plugin report include a plugin.component_reenabled warning that names the contributing file.

Without filesystem access, no files are read, so the base is empty and only your initialize config applies.

Explicit Defaults and Overrides

The editor writes explicit defaults for edited Observability and Adaptive sections. It writes NeMo Guardrails, PII Redaction, and dynamic-plugin fields only when readers configure them. Use the NeMo Guardrails editor only to maintain or remove an existing configuration. In a layered config model, omitting a field means “inherit a lower precedence value”; it does not mean “delete that value.” Use the dedicated nemo-relay model-pricing commands to manage model-pricing catalog sources.

For example, this system file disables ATOF even if an explicit or user file enables it:

[[components]]
kind = "observability"
[components.config.atof]
enabled = false

The merged configuration can still contain an inherited ATOF sinks array, but the runtime ignores the section because enabled = false.

To override an inherited non-default scalar field with its default value, write the default explicitly in the higher-precedence file. List entries are not merged item by item: higher-precedence entries are added before inherited entries. To change or remove an inherited sink, profile, source, or other list entry, edit the layer that declares it.

There is no tombstone syntax for deleting an inherited nested field while keeping the rest of the lower-precedence component. To remove inherited settings entirely, edit the lower-precedence file or override the behavior with another field such as enabled = false.

Validation

Plugin validation runs before activation. Invalid plugin config blocks gateway startup instead of starting with a partially installed plugin set.

Common validation failures include:

  • Unknown component kinds when policy treats them as errors.
  • Unknown fields when policy treats them as errors.
  • Unsupported field values, such as an invalid exporter mode or transport.
  • Duplicate singleton components.
  • Enabled components whose build-time features are unavailable.
  • Component-specific semantic failures, such as an Agent Trajectory Interchange Format (ATIF) filename template that does not contain {session_id}.
  • Dynamic manifests that have an incompatible Relay version, unsupported capability, invalid load contract, or failed trust evidence. Run nemo-relay plugins validate <plugin-id> to check optional static schema validation for dynamic component config.

Use nemo-relay doctor to inspect the resolved gateway configuration and plugin diagnostics. For Observability, doctor also reports enabled exporter sections, checks writable file exporter directories, probes configured ATOF stream sinks, and checks reachable OTLP endpoints when those settings are present. For model pricing, doctor validates enabled file and inline sources and fails when a source is unreadable or the catalog schema is invalid. For dynamic-plugin validation, lifecycle state, and trust diagnostics, use nemo-relay plugins validate, inspect, and list; doctor reports only the resolved manifest references and host configuration status.

Relationship to config.toml

config.toml owns gateway and agent setup, such as upstream provider base URLs and agent command configuration. plugins.toml owns reusable runtime behavior installed by the plugin system.

Keep all long-lived plugin setup in plugins.toml. config.toml owns gateway and agent setup only.

Legacy observability config sections in config.toml, such as [exporters], [observability], and [export.openinference], are not supported. Configure Observability exporters through plugins.toml.

Component Guides

Use the component guides for field-level configuration: