A Hermes Agent model-provider plugin for
ModelRelay. It tells Hermes what each ModelRelay model can do:
whether it takes images, whether it supports tools and reasoning, and its context window.
It reads all of this from ModelRelay's own GET /v1/models catalog. Hermes source is not changed.
models.dev has no entry for ModelRelay. When ModelRelay is set up as a custom provider
(provider: custom:modelrelay), Hermes has no capability data for its models. When an image
arrives, Hermes then replaces it with a vision_analyze text description, even for a model
like glm-5.3-flash that takes images natively. This plugin fills
ProviderProfile.model_capabilities, which is the seam agent/models_dev.py reads. Image routing,
context-length lookup, picker badges and /api/model/info all read through that seam.
They are fetched live, not kept in a static table. model_capabilities is a lazy mapping:
-
Importing the plugin makes no network call.
-
On first lookup in a process, the plugin reads the mirror at
$HERMES_HOME/cache/modelrelay_models.json. If there is no mirror, it fetches/v1/modelsonce, with a 5 s timeout. -
A catalog older than 6 h is still served while a background thread refreshes it.
-
The model picker's
fetch_models()also seeds the cache. -
Each catalog row is mapped to Hermes' canonical schema:
supports_vision:"image"is inarchitecture.input_modalitiessupports_tools:"tools"is insupported_parameterssupports_reasoning:"reasoning"is insupported_parameterscontext_window:context_length
A field the row does not publish is left out, never guessed.
If the catalog can't be loaded, the plugin declares nothing and logs a warning. It retries at most
once a minute. Hermes then treats the model's capabilities as unknown. For an attached image that
means the text path (a vision_analyze description), exactly as without the plugin. An explicit
providers.modelrelay.models.<model>.supports_vision in config.yaml still overrides the catalog.
Pick one of these two methods.
Directory plugin (per Hermes profile, no pip):
mkdir -p "$HERMES_HOME/plugins/model-providers"
cp -R hermes_modelrelay "$HERMES_HOME/plugins/model-providers/modelrelay"pip (into the Python environment Hermes runs from):
pip install /path/to/hermes-modelrelay # or: uv pip install --python <hermes venv python> ...Pip-installed provider plugins load only when they are opted in. Add this to config.yaml:
plugins:
enabled: [modelrelay]If both methods are installed, the directory plugin wins.
Put the key in the profile's .env (or in the environment):
MODELRELAY_API_KEY=mr_...
# optional: MODELRELAY_BASE_URL=https://api.modelrelay.ai/v1Then select the provider by its plugin name in config.yaml:
model:
provider: modelrelay
default: glm-5.3-flashDo not use custom:modelrelay. Do not set model.base_url either: the plugin supplies it.
model.provider: modelrelayresolves to the plugin, even whenconfig.yamlstill has aproviders.modelrelay:custom entry. Hermes doesn't map a name onto a custom entry once a provider with that canonical name is registered. The custom entry'sapi,key_cmdandtransportare then not used.model.provider: custom:modelrelaykeeps using the custom entry. Hermes resolves the request to its genericcustomprovider, so the plugin is never consulted and images get the text path.- Credentials come from
MODELRELAY_API_KEY. The plugin path does not runkey_cmd. Move the key into the profile.env. - You can remove the
providers.modelrelayblock. If you keep it, itsmodels.<id>.supports_visionentries still work as explicit overrides.
Run them with the Python environment that has hermes-agent installed:
<hermes venv>/bin/python -m pytestThe tests mock HTTP and run against stock Hermes. Each one loads the plugin through Hermes' own
directory discovery in an isolated HERMES_HOME.