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:
Generated
+161
-4
@@ -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",
|
||||
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user