Skip to main content

hermes-structured-aux-models

❖ Communityv0.1.0★ 2

Routes selected Hermes auxiliary tasks (approval, MCP sampling, compression) through bounded Jev decision calls on OpenRouter instead of free-form chat prompts, failing open to the operator's own auxiliary provider for anything it cannot express as a decision.

Open in Hermes Desktop
hermes plugins install hermes-structured-aux-models

README

From the reviewed commit e5c49b0 ↗; it updates when the author re-pins.

hermes-structured-aux-models

A Hermes Agent model-provider plugin that routes selected auxiliary tasks through bounded Jev decision calls on OpenRouter instead of free-form chat prompts.

Hermes' auxiliary slots — compression, smart approval, MCP sampling — are normally served by pointing auxiliary.<task> at a chat model. Weak models are unreliable there, and pointing a structured-output model at a chat-shaped prompt is worse: the request is still a chat completion, so the model still has to generate prose, and it still fails in prose-shaped ways.

This plugin changes the request itself. It registers a provider, structured-aux, whose client is an in-process shim. When Hermes' auxiliary client asks that provider for a chat completion, the shim translates the request into a decision request: one shared state plus questions whose entire answer set is enumerated in advance. The provider returns a label; the shim returns it to Hermes as an ordinary completion.

A decision model cannot answer a question whose outcomes are not bounded. That is a constraint, not a limitation to work around — and it is why the plugin raises instead of guessing for anything it cannot express, letting Hermes fall back to your real auxiliary provider.

Scope

Task Contract Status
approval bounded choice over APPROVE / DENY / ESCALATE supported
mcp bounded choice over the MCP server's supplied tool names supported, with a caveat (below)
compression bounded KEEP / DROP per context block, then an extractive digest supported
skills_hub — out of scope

skills_hub is deliberately excluded. Hermes registers it as an auxiliary task and lists it in the model picker, but nothing in the shipped code calls it — there is no call site to intercept. It will be revisited if Hermes wires one.

Install

hermes plugins install trajectoire-ai/hermes-structured-aux-models

The tree scans clean under Hermes' install-time plugin scanner (tools/plugin_guard.py): verdict safe, no findings.

Confirm the provider actually registered. hermes doctor reports a false error for model-provider plugins (Plugin registration failed: no register() function) — they self-register at import and have no register(), so ignore that line. Check the way that matters instead, in a fresh process under the target HERMES_HOME:

HERMES_HOME=<home> python -c "
from agent import auxiliary_client as A
c, m = A.resolve_provider_client('structured-aux', 'structured-aux/approval')
print(type(c).__name__, m, bool(getattr(c, 'api_key', '')))"

A StructuredAuxClient with a non-empty key is the proof. Anything else means the plugin is not reachable from that home — see below.

Where to install

Hermes discovers provider plugins once per process, at process start, by scanning $HERMES_HOME/plugins/. Discovery is process-global and memoized: it does not re-run when a session later switches profiles. So the plugin must live in the HERMES_HOME that the process making the auxiliary calls starts with:

Consuming process HERMES_HOME at start Install into
Profile-scoped CLI or messaging gateway (hermes -p <p> …) ~/.hermes/profiles/<p> the profile: hermes -p <p> plugins install …
Machine-level desktop / remote backend (hermes serve, no profile) ~/.hermes (root) the root home: hermes plugins install …

The machine-level hermes serve backend is the easy one to get wrong: it is started once with the root home and only binds a session's profile HERMES_HOME later, when it builds that session's agent. A plugin installed only under profiles/<p>/plugins/ is therefore never imported by it — even though that same profile's CLI and gateway resolve it fine.

Symptoms of installing into the wrong home:

  • Auxiliary …: using openai-codex (…) / any provider other than using structured-aux (structured-aux/<task>);
  • Smart approvals: LLM call failed … (RuntimeError: Provider 'structured-aux' is set in config.yaml but no API key was found …), escalating;
  • no app page appears at https://openrouter.ai/apps?url=https://trajectoire.ai.

If one machine runs both a profile-scoped gateway and a machine-level serve, install in both homes. Then restart every long-lived process that makes auxiliary calls: discovery is memoized, so a process started before the install keeps its old registry until it restarts.

Configure

Point only the tasks you want at the provider. Each task's model names the contract:

hermes config set auxiliary.approval.provider structured-aux --force
hermes config set auxiliary.approval.model structured-aux/approval --force

hermes config set auxiliary.mcp.provider structured-aux --force
hermes config set auxiliary.mcp.model structured-aux/mcp --force

hermes config set auxiliary.compression.provider structured-aux --force
hermes config set auxiliary.compression.model structured-aux/compression --force

The structured-aux/<task> model string is how the shim knows which contract applies; Hermes passes it through untouched. structured-aux:approval, structured-aux.approval and structured-aux-approval are accepted too. The approval contract additionally recognises Hermes' guardian prompt by its wording, so it still routes if you leave the model blank.

That string is a routing token, not a model id. It never reaches OpenRouter — the Jev model actually sent to the decisions endpoint is decision_model below.

Credential

Only the OpenRouter credential is used. It is resolved in this order:

  1. OPENROUTER_API_KEY in the process environment;
  2. OPENROUTER_API_KEY in the active profile's .env;
  3. Hermes' own openrouter credential entry (agent.credential_pool.load_pool).

Step 3 exists so an operator who already ran hermes auth add openrouter needs no duplicate secret: the plugin bills the same key as the rest of Hermes. If none of the three resolves, the provider reports "no OpenRouter credential is configured" and Hermes falls back to your real auxiliary provider — a missing key is never fatal. No other credential is required, and none is forwarded anywhere except the OpenRouter decisions endpoint.

Plugin settings

Optional, under plugins.entries.hermes-structured-aux-models.settings:

Key Default Meaning
tasks [approval, mcp, compression] which task keys this provider may serve
decision_model ~typesafe/jev-latest Jev model id sent to OpenRouter. The ~ prefix marks a moving alias, so the default tracks the newest Jev release instead of a dated id that has to be bumped by hand. Pin an exact version here if you need reproducibility.
decision_base_url https://openrouter.ai must be HTTPS
app_referer https://trajectoire.ai OpenRouter HTTP-Referer. The app's primary attribution identifier.
app_title hermes-structured-aux OpenRouter X-OpenRouter-Title. The app's display name in rankings.
timeout_seconds 15.0 per decision request
compression_max_blocks 48 segmentation cap per compression prompt
compression_blocks_per_call 16 blocks asked about per provider call
compression_output_budget_chars 6000 digest size ceiling
compression_min_block_chars 120 minimum size before a block is closed

App attribution

Every decision request carries OpenRouter's app attribution headers: HTTP-Referer: https://trajectoire.ai — the value OpenRouter keys the app's page and rankings on — and X-OpenRouter-Title: hermes-structured-aux, its display name. Without a HTTP-Referer, OpenRouter files the usage under "Unknown" and no app page is created. Both are plain, non-secret strings; override them with app_referer and app_title above.

Per-task behaviour

approval

Hermes' smart-approval guardian asks for exactly one word and maps anything it does not recognise to escalate. The contract therefore uses three labels, not two — collapsing ESCALATE into DENY would silently turn "unknown risk" into a refusal. The guardian's system prompt is forwarded as guardian_policy and the flagged command as request.

mcp

The mcp auxiliary task serves MCP sampling: an MCP server asks Hermes to run a completion, optionally supplying tools. When the server supplies at least two argument-free tools, the plugin asks which one to call and returns a tool call.

Caveat: a decision returns a label, never synthesised arguments. Tools that declare required parameters are therefore excluded from the candidate set rather than guessed at, and if fewer than two answerable tools remain the request is handed back to your real provider. This makes MCP support narrower than the other two tasks — it is a genuine property of decision models, not a missing feature.

compression

Hermes' compressor expects a prose summary. A decision model cannot write prose, so the plugin does the other thing: it segments the prompt into deterministic blocks, asks KEEP/DROP per block in bounded batches, and emits a digest assembled only from retained original text plus a provenance header. Nothing is generated.

Two invariants matter here:

  • Segmentation never loses text. When a prompt exceeds the block cap the overflow is merged into the final block rather than dropped.
  • The digest is never empty. If the provider retains nothing, the final block is kept anyway — an empty result would make Hermes treat compression as failed and fall back to your main model, which is worse than retaining the most recent context.

Cost posture

Only the tasks you explicitly point at this provider make provider calls. Nothing runs per turn: approval fires only for commands Hermes already flagged, compression only at compaction boundaries, MCP sampling only when a server asks. There is no background supervision loop and no turn admission.

Privacy

Every payload is redacted before it leaves the process, reusing Hermes' own redactor (agent.redact) with a conservative local fallback. Decision receipts are not written to disk by this plugin; nothing is persisted outside Hermes' own logs.

Testing

Install the dev extra declared in pyproject.toml ([project.optional-dependencies] dev), then:

python -m pytest

The suite is fully offline — every test injects a fake transport, and no test makes a network call. A live provider run requires explicit operator approval.

Two things about the layout are worth knowing before you touch the tests:

  • The repository root is a Python package (Hermes requires __init__.py there), so a test collector imports the plugin entry point from outside the Hermes runtime. That entry point therefore tolerates a missing providers module; inside Hermes the import always succeeds and registration always runs.
  • tests/conftest.py holds every fixture. Fake transports record calls as {"url", "headers", "body", "timeout"}, so assert on transport.calls[0]["body"].

How it works

The interception point is a documented plugin surface, not a core patch:

  1. plugin.yaml declares kind: model-provider, so providers/__init__.py discovers the plugin from $HERMES_HOME/plugins/<name>/ and imports it with the plugin directory as the package root.
  2. The module-level register_provider(profile) call registers structured-aux with auth_type="external_process". hermes_cli/auth.py projects that into PROVIDER_REGISTRY, which is what makes auxiliary.<task>.provider resolvable.
  3. external_process is the only auth type whose auxiliary branch consults profile.create_client(); the api_key branch always builds a plain HTTP client. That branch is therefore what lets an in-process shim answer auxiliary calls.
  4. HERMES_SKIP_TRANSPORT_WRAP and HERMES_SKIP_ASYNC_WRAP keep the shim out of the Anthropic / Codex / Bedrock wire adapters.

The repository's agent instructions record each of these seams with the symbols to re-verify, so a future maintainer can check them against a newer Hermes instead of guessing.

License

MIT — see LICENSE.

← Back to the catalog · catalog built Oct 14, 2026