terra: add Hermes Agent, wired to local ollama

Points Nous Research's Hermes Agent at terra's own ROCm ollama server
(gemma4:12b) as a custom OpenAI-compatible provider instead of a cloud
key. Native systemd mode via the hermes-agent flake's own NixOS module
— simpler than container mode, avoids the podman-rootful-sudo dance
its docs call out.

Also bumps OLLAMA_CONTEXT_LENGTH (and Hermes' mirrored model.context_length)
from ollama's ~4k default to 131072, load-tested with real multi-ten-
thousand-token prompts rather than just idle `ollama ps` checks — chosen
as the practical ceiling where VRAM headroom and prefill throughput both
start visibly degrading, not just the largest number that technically fit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 01:28:34 +02:00
co-authored by Claude Sonnet 5
parent 6c8046bac8
commit 969bd69d8d
4 changed files with 222 additions and 4 deletions
Generated
+161 -4
View File
@@ -114,6 +114,27 @@
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": [
@@ -135,6 +156,29 @@
"type": "github"
}
},
"hermes-agent": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_2",
"npm-lockfile-fix": "npm-lockfile-fix",
"pyproject-build-systems": "pyproject-build-systems_2",
"pyproject-nix": "pyproject-nix_2",
"uv2nix": "uv2nix_2"
},
"locked": {
"lastModified": 1787007062,
"narHash": "sha256-C1SJX0VqlxrLknGhFKj7+DKpfJ1eI4udPE67kW+dT+M=",
"owner": "NousResearch",
"repo": "hermes-agent",
"rev": "55e34fb7d0cd8fd1e16be82a1ceafee05fca63d3",
"type": "github"
},
"original": {
"owner": "NousResearch",
"repo": "hermes-agent",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -198,9 +242,9 @@
"nixpkgs": [
"nixpkgs"
],
"pyproject-build-systems": "pyproject-build-systems_2",
"pyproject-nix": "pyproject-nix_2",
"uv2nix": "uv2nix_2"
"pyproject-build-systems": "pyproject-build-systems_3",
"pyproject-nix": "pyproject-nix_3",
"uv2nix": "uv2nix_3"
},
"locked": {
"lastModified": 1768002052,
@@ -369,6 +413,22 @@
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1785318670,
"narHash": "sha256-dN6Ou5x/+23FZLEpYP3IffO+NyJFzUlGumt1uu3MMaY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0954f7ee2f6bb3dc7d4e3d0d8bcb8fd4bde4cfc5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1786943417,
"narHash": "sha256-b4qgjdFtlz5TAZ1Hi7DFJeqX3sjaDUnrwr9OO+O1rM0=",
@@ -384,6 +444,27 @@
"type": "github"
}
},
"npm-lockfile-fix": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775903712,
"narHash": "sha256-2GV79U6iVH4gKAPWYrxUReB0S41ty/Y3dBLquU8AlaA=",
"owner": "jeslie0",
"repo": "npm-lockfile-fix",
"rev": "c6093acb0c0548e0f9b8b3d82918823721930fe8",
"type": "github"
},
"original": {
"owner": "jeslie0",
"repo": "npm-lockfile-fix",
"type": "github"
}
},
"pyproject-build-systems": {
"inputs": {
"nixpkgs": [
@@ -414,6 +495,35 @@
}
},
"pyproject-build-systems_2": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
],
"pyproject-nix": [
"hermes-agent",
"pyproject-nix"
],
"uv2nix": [
"hermes-agent",
"uv2nix"
]
},
"locked": {
"lastModified": 1785115949,
"narHash": "sha256-8AM37BfyGaL2v/SZyg4PupRxJ01Y4htvM+WrTjWrPpo=",
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"rev": "62c0d86027edb1c4f39a5facc09876348144f7c9",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"type": "github"
}
},
"pyproject-build-systems_3": {
"inputs": {
"nixpkgs": [
"mediamanager-nix",
@@ -464,6 +574,27 @@
}
},
"pyproject-nix_2": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1784591072,
"narHash": "sha256-zP/WaDxrRu8GANZM61+V2LT/7ycEEdoyLWn7M6WzU7M=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "e3b599ca2e7fcf93d4edf65d7f19bbf6491724f3",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"pyproject-nix_3": {
"inputs": {
"nixpkgs": [
"mediamanager-nix",
@@ -488,13 +619,14 @@
"inputs": {
"authentik-nix": "authentik-nix",
"disko": "disko",
"hermes-agent": "hermes-agent",
"home-manager": "home-manager",
"hypr-chrome": "hypr-chrome",
"mediamanager-nix": "mediamanager-nix",
"nix-flatpak": "nix-flatpak",
"nixos-anywhere": "nixos-anywhere",
"nixos-images": "nixos-images",
"nixpkgs": "nixpkgs_2",
"nixpkgs": "nixpkgs_3",
"nixpkgs-unstable": "nixpkgs-unstable",
"sops-nix": "sops-nix",
"tome": "tome"
@@ -598,6 +730,31 @@
}
},
"uv2nix_2": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
],
"pyproject-nix": [
"hermes-agent",
"pyproject-nix"
]
},
"locked": {
"lastModified": 1785277507,
"narHash": "sha256-9Tq3UDX2hD/aveW/HvkBlAmEwJTOlY5HQXJM+L5BGmE=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "5a836d395cbf5fc22670eb98dd4aa4fc4d406977",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github"
}
},
"uv2nix_3": {
"inputs": {
"nixpkgs": [
"mediamanager-nix",
+6
View File
@@ -33,6 +33,11 @@
};
authentik-nix.url = "github:nix-community/authentik-nix";
nix-flatpak.url = "github:gmodena/nix-flatpak";
# Hermes Agent (see services/desktop/hermes-agent.nix) — Tier 2 platform
# per its own docs (best-effort Nix support, can break on any upstream
# commit), so pinned like everything else via flake.lock rather than
# followed loosely.
hermes-agent.url = "github:NousResearch/hermes-agent";
# Own Hyprland plugin (border + title bar), public repo, fetched over
# https (no credentials needed, unlike tome below). `nixpkgs.follows` is
# what makes its packaged build ABI-correct — Hyprland plugins are
@@ -105,6 +110,7 @@
sops-nix.nixosModules.sops
home-manager.nixosModules.home-manager
inputs.nix-flatpak.nixosModules.nix-flatpak
inputs.hermes-agent.nixosModules.default
./hosts/terra/configuration.nix
];
};
+19
View File
@@ -18,6 +18,7 @@ in
../../services/desktop/desktop-hyprland.nix
../../services/desktop/desktop-apps.nix
../../services/desktop/librechat.nix
../../services/desktop/hermes-agent.nix
];
networking.hostName = "terra";
@@ -95,6 +96,24 @@ in
# Sparse activation makes that far less painful than it'd be for a dense
# model this size, but still expect it to run slower than the two above.
loadModels = [ "gemma4:12b" "qwen3.6:35b-a3b" ];
# Ollama truncates context far below the model's real window unless
# told otherwise (the OpenAI-compat /v1 route it's reached through has
# no way to set this per-request — see services/desktop/hermes-agent.nix,
# which points at this server and mirrors the same number in
# model.context_length). 131072 chosen as the practical ceiling after
# load-testing with real prompts, not just idle `ollama ps` checks:
# 32768 (31.6k-token prompt) and 65536 (40.8k-token prompt) both stayed
# 100% GPU with VRAM barely moving (~10.1G / ~10.67G of 16G) — KV cache
# cost barely grows with context, likely sliding-window/local attention
# on most of gemma4:12b's layers. At 131072 that stopped being true: a
# ~108k-token prompt pushed VRAM to ~11.4G/16G (still 100% GPU, no CPU
# spillover, negligible GTT) but with visibly shrinking headroom, and
# prefill throughput measurably dropped (~490 -> ~460 tok/s) over just
# the last 13k tokens — filling the full window would take minutes of
# pure prompt processing. Stopped here rather than push further: next
# doubling would risk CPU spillover under any concurrent GPU load
# (desktop compositor, jellyfin transcode) for diminishing benefit.
environmentVariables.OLLAMA_CONTEXT_LENGTH = "131072";
};
# ---- Dev-data disks — NOT in disko, mounted read-write, never wiped ----
+36
View File
@@ -0,0 +1,36 @@
{ ... }:
# Hermes Agent (https://github.com/NousResearch/hermes-agent) — Nous
# Research's tool-calling CLI/gateway agent, pointed at the local ollama
# server (see hosts/terra/configuration.nix) over its OpenAI-compatible /v1
# route instead of a cloud provider. The nixosModules.default this pulls in
# comes from the hermes-agent flake input itself (flake.nix), not from
# nixpkgs — it builds the agent from source via uv2nix, so there's no
# curl-|-bash installer or runtime pip/npm involved.
#
# Native mode (container.enable stays false): a hardened systemd service is
# enough here since the agent only needs to reach loopback ollama, and it
# avoids the podman-rootful-needs-passwordless-sudo dance the module's docs
# call out for container mode. addToSystemPackages puts `hermes` on darman's
# PATH sharing state (sessions, memories, cron) with the service instead of
# starting a second, disconnected ~/.hermes.
{
services.hermes-agent = {
enable = true;
addToSystemPackages = true;
settings.model = {
provider = "custom";
base_url = "http://127.0.0.1:11434/v1";
# gemma4:12b, not qwen3.6:35b-a3b: same daily-driver model already
# loaded for librechat (services/desktop/librechat.nix) — reusing it
# means no second model has to swap into the 6800 XT's 16G VRAM
# alongside whatever's already resident.
default = "gemma4:12b";
# No api_key — ollama's OpenAI-compat endpoint doesn't check one.
# Mirrors OLLAMA_CONTEXT_LENGTH set on the ollama service itself;
# this is a client-side hint only, ollama enforces the real limit.
context_length = 131072;
};
};
}