Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ COPY setup-wizard/ /opt/setup-wizard/
# Copy DAppNode context files (seeded into HERMES_HOME on first boot)
COPY dappnode/ /opt/dappnode/

# Copy entrypoint script
COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh
# DAppNode s6-overlay customizations: a cont-init bootstrap hook plus the
# setup-wizard and ttyd long-run services. We deliberately do NOT override the
# image ENTRYPOINT — the upstream image runs s6-overlay's /init (which handles
# UID remap, chown, config seeding, schema migration, skills sync and drops to
# the hermes user via main-wrapper.sh). Overriding it and re-dropping privileges
# ourselves crash-looped the container (tini is symlinked to /init upstream).
COPY rootfs/ /
RUN chmod 0755 /etc/cont-init.d/10-dappnode-setup \
/etc/s6-overlay/s6-rc.d/setup-wizard/run \
/etc/s6-overlay/s6-rc.d/ttyd/run

# Persistent data directory
ENV HERMES_HOME=/opt/data
Expand All @@ -29,5 +37,7 @@ EXPOSE 3000 8080 8081 7681
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# ENTRYPOINT is inherited from the upstream image (s6-overlay /init +
# docker/main-wrapper.sh). main-wrapper routes the CMD below to
# `s6-setuidgid hermes hermes gateway run`.
CMD ["gateway", "run"]
77 changes: 77 additions & 0 deletions dappnode/dappnode-nexus/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: dappnode-nexus
description: DAppNode Nexus — Private AI Gateway for Builders. Knowledge about the Nexus platform, its architecture, API compatibility, context-length pitfalls, and integration with DAppNode infrastructure.
category: devops
tags:
- dappnode
- nexus
- ai-gateway
- privacy
- openai-compatible
---

# DAppNode Nexus

Nexus is DAppNode's **Private AI Gateway** — a unified, OpenAI-compatible API for accessing private and confidential AI models.

## Core Value Proposition

- **Privacy-first**: Prompts and data stay within the user's infrastructure
- **OpenAI-compatible API**: Drop-in replacement for OpenAI API calls
- **Multiple model routing**: Access to models from DeepSeek, Anthropic, OpenAI, Minimax, and more
- **Flexible billing**: Pay-as-you-go or subscription models

## Architecture

Nexus runs as a service within the DAppNode ecosystem. Users access it via:
- **Web UI**: https://nexus.dappnode.com/
- **API endpoint**: `https://nexus-api.dappnode.com/v1`

## Key URLs

| Resource | URL |
|----------|-----|
| Nexus Web App | https://nexus.dappnode.com/ |
| Nexus API | https://nexus-api.dappnode.com/v1 |
| DAppNode Main Site | https://dappnode.com/ |

## Privacy Guarantees

- Inference runs on DAppNode infrastructure, not external cloud providers
- Data does not leave the user's controlled environment
- No logging or retention of prompts by default

## Pitfalls

### Context length defaults to 256K with Nexus provider

When Nexus is configured as the Hermes provider (`nexus-api.dappnode.com`), Hermes may not auto-detect the model's true context length because:

1. `nexus-api.dappnode.com` is not in Hermes' `_URL_TO_PROVIDER` map → treated as an unknown custom endpoint
2. Hermes may skip provider-aware lookups (Anthropic API, models.dev, hardcoded defaults)
3. Falls back to `DEFAULT_FALLBACK_CONTEXT = 256_000` tokens if auto-detection fails

> **Update**: The `/v1/models` endpoint now returns `context_size` per model. The DAppNode package auto-sets `model.context_length` as a safety net, but you can verify with `hermes config show`.

**Symptom**: Hermes compresses context early, treats a 1M-token model as 256K, or shows `context_length: 256000` in `/usage`.

**Fix**: Set the correct context length explicitly in config:

```bash
hermes config set model.context_length 1000000
```

After setting, do `/reset` for the change to take effect.

Common Nexus-proxied models and their context lengths:

| Model | Context length |
|-------|---------------|
| `deepseek/deepseek-v4-pro` | 1,048,576 (1M) |
| `deepseek/deepseek-v4-flash` | 1,048,576 (1M) |
| `moonshotai/kimi-k2.6` | 262,144 |
| `minimax/minimax-m2.7` | 204,800 |
| `minimax/minmax-m3` | 512,000 |
| `nexus/auto` | Auto-routing (varies) |

> **Note**: `/v1/models` endpoint is now publicly accessible and returns `context_size` per model. Hermes Agent can query this for auto-detection, but the DAppNode package also pre-sets a safe default.
2 changes: 2 additions & 0 deletions dappnode/dappnode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ providers:

Verify with `hermes doctor`; the resolved provider source should read `custom_provider:DAppNode Nexus`, not `no-key-required`.

**Context length pitfall**: Nexus uses a custom domain (`nexus-api.dappnode.com`) that Hermes cannot auto-resolve for context length detection — models default to 256K tokens. The DAppNode package automatically sets `model.context_length` to 1M for new setups, but if you see early context compression, run `hermes config set model.context_length 1000000`. See the `dappnode-nexus` skill for the full root-cause analysis and per-model context lengths.

## Troubleshooting

### Package Not Reachable
Expand Down
94 changes: 94 additions & 0 deletions dappnode/patch-config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Patch a freshly-seeded Hermes config.yaml for the DAppNode environment.

Run as the `hermes` user from the 10-dappnode-setup cont-init hook, AFTER
upstream's stage2-hook has seeded config.yaml from cli-config.yaml.example
and run its schema migration. Idempotent: safe to run on every boot.
"""
import json
import os
import urllib.request

import yaml

config_path = os.path.join(os.environ.get("HERMES_HOME", "/opt/data"), "config.yaml")


def fetch_nexus_context_size(base_url, model_id):
"""Return the context_size Nexus reports for model_id, or None.

Queries the OpenAI-compatible ``{base_url}/models`` listing, which Nexus
serves publicly with a ``context_size`` field per model.
"""
url = base_url.rstrip("/") + "/models"
try:
req = urllib.request.Request(url, headers={"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.load(resp)
except Exception:
return None
for m in data.get("data", []):
if m.get("id") == model_id:
size = m.get("context_size")
return int(size) if size else None
return None

try:
with open(config_path) as f:
config = yaml.safe_load(f) or {}
except FileNotFoundError:
# Nothing to patch — upstream seeding should have created it, but don't
# fail the boot if it hasn't.
raise SystemExit(0)
except Exception:
config = {}

# --- Network access: bind the gateway to the LAN on the DAppNode port ---
gw = config.setdefault("gateway", {})
gw["port"] = 3000
gw["bind"] = "lan"
cui = gw.setdefault("controlUi", {})
cui.setdefault("dangerouslyAllowHostHeaderOriginFallback", True)
cui.setdefault("allowInsecureAuth", True)
cui.setdefault("dangerouslyDisableDeviceAuth", True)

term = config.setdefault("terminal", {})
term["cwd"] = os.environ.get("HERMES_HOME", "/opt/data")

platforms = config.setdefault("platforms", {})
if isinstance(platforms, dict):
whatsapp = platforms.setdefault("whatsapp", {})
if isinstance(whatsapp, dict):
extra = whatsapp.setdefault("extra", {})
if isinstance(extra, dict) and extra.get("bridge_port") in (None, 3000, "3000"):
extra["bridge_port"] = 3010

with open(config_path, "w") as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print("Patched config.yaml for DAppNode (api_port=3000, whatsapp_bridge_port=3010)")

# --- Nexus context length: source the real value from /v1/models ---
# nexus-api.dappnode.com is not in Hermes' URL-to-provider map, so the agent
# cannot auto-detect a model's context window and falls back to 256K. Rather
# than hardcode a single number (wrong for the smaller models -- e.g. Kimi is
# 262K, MiniMax M2.7 is 205K), query the endpoint Nexus already exposes:
# GET /v1/models returns `context_size` per model. Set model.context_length to
# that authoritative value for the configured model.
model_section = config.setdefault("model", {})
provider = model_section.get("provider", "")
base_url = str(model_section.get("base_url", ""))
model_id = model_section.get("default") or model_section.get("model") or ""

if provider == "custom" and "nexus-api.dappnode.com" in base_url and model_id:
ctx = fetch_nexus_context_size(base_url, model_id)
if ctx and model_section.get("context_length") != ctx:
model_section["context_length"] = ctx
with open(config_path, "w") as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print(f"Nexus: set model.context_length={ctx} for '{model_id}' (from /v1/models)")
elif ctx:
print(f"Nexus: model.context_length already {ctx} for '{model_id}', leaving as-is")
else:
# Endpoint unreachable or model not listed. Leave context_length alone:
# Hermes' own 256K fallback is safe (under-, never over-estimating).
print(f"Nexus: could not resolve context_size for '{model_id}'; using Hermes default")
12 changes: 9 additions & 3 deletions dappnode_package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
"shortDescription": "Self-improving AI agent with multi-LLM support and messaging gateway",
"description": "Hermes Agent is a self-improving AI agent built by Nous Research. It features a built-in learning loop — creating skills from experience, improving them during use, and building a deepening model of who you are across sessions.\n\n- **Web Gateway**: Full-featured web interface for interacting with AI\n- **Multi-LLM Support**: OpenRouter, OpenAI, Anthropic, Google Gemini, Ollama, Groq, and more\n- **Messaging Gateway**: Telegram, Discord, Slack, WhatsApp, Signal — all from a single process\n- **Skills System**: Agent-curated procedural memory that self-improves\n- **Persistent Memory**: Cross-session recall with user modeling\n- **Cron Scheduling**: Automated tasks with delivery to any platform\n- **Subagent Delegation**: Spawn isolated subagents for parallel workstreams\n\nRun your own AI agent with full control over your data and API keys.",
"type": "service",
"architectures": ["linux/amd64"],
"architectures": [
"linux/amd64"
],
"author": "DAppNode Association <admin@dappnode.io>",
"license": "MIT",
"categories": ["AI", "Developer tools", "Communications"],
"categories": [
"AI",
"Developer tools",
"Communications"
],
"keywords": [
"ai",
"llm",
Expand Down Expand Up @@ -79,4 +85,4 @@
"featuredBackground": "linear-gradient(135deg, #FFD700 0%, #764ba2 100%)",
"featuredColor": "white"
}
}
}
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ services:
API_SERVER_KEY: dappnode
API_SERVER_CORS_ORIGINS: "*"
GATEWAY_ALLOW_ALL_USERS: "true"
# Enable the upstream s6 dashboard service on the DAppNode port (8081).
HERMES_DASHBOARD: "true"
HERMES_DASHBOARD_HOST: 0.0.0.0
HERMES_DASHBOARD_PORT: "8081"
HERMES_DASHBOARD_INSECURE: "true"
volumes:
- hermes_data:/opt/data
logging:
Expand Down
Loading
Loading