From a5da107f70bd9f2226d57f971abe2941cd039f84 Mon Sep 17 00:00:00 2001 From: cori Date: Fri, 11 Sep 2026 16:44:21 +0000 Subject: [PATCH] feat: add hermes-obsidian-bridge app OpenAI-compatible proxy in front of Hermes api_server with session continuity, vault RAG (Obsidian Local REST API), and cost routing. Image sourced from local Forgejo (forge.407.lol/cori/hermes-obsidian-bridge). Schema-validated against @runtipi/common (114/114 bun test pass). --- apps/hermes-obsidian-bridge/config.json | 92 ++++++++++++++++++ .../docker-compose.json | 33 +++++++ .../metadata/description.md | 29 ++++++ apps/hermes-obsidian-bridge/metadata/logo.jpg | Bin 0 -> 5304 bytes 4 files changed, 154 insertions(+) create mode 100644 apps/hermes-obsidian-bridge/config.json create mode 100644 apps/hermes-obsidian-bridge/docker-compose.json create mode 100644 apps/hermes-obsidian-bridge/metadata/description.md create mode 100644 apps/hermes-obsidian-bridge/metadata/logo.jpg diff --git a/apps/hermes-obsidian-bridge/config.json b/apps/hermes-obsidian-bridge/config.json new file mode 100644 index 0000000..b060217 --- /dev/null +++ b/apps/hermes-obsidian-bridge/config.json @@ -0,0 +1,92 @@ +{ + "$schema": "../app-info-schema.json", + "name": "Hermes Obsidian Bridge", + "id": "hermes-obsidian-bridge", + "available": true, + "short_desc": "OpenAI-compatible proxy in front of Hermes api_server with session continuity and vault RAG", + "author": "cori", + "port": 9120, + "categories": [ + "ai", + "utilities" + ], + "description": "Thin OpenAI-compatible proxy in front of the Hermes api_server. Adds session continuity (X-Session-Id), vault RAG via the Obsidian Local REST API (X-Obsidian-Note-Path), and automatic cost routing between a cheap and strong model based on prompt length. Wire it into any OpenAI-compatible Obsidian plugin (Copilot for Obsidian, Text Generator, Local GPT) for in-note chat with the full Hermes agent. Image: forge.407.lol/cori/hermes-obsidian-bridge:sha-7139662d. Source: https://github.com/cori/hermes-obsidian-bridge.", + "tipi_version": 1, + "version": "sha-7139662d", + "source": "https://github.com/cori/hermes-obsidian-bridge", + "website": "https://github.com/cori/hermes-obsidian-bridge", + "exposable": true, + "supported_architectures": [ + "arm64", + "amd64" + ], + "created_at": 1757610000000, + "updated_at": 1757610000000, + "dynamic_config": true, + "form_fields": [ + { + "type": "password", + "label": "Hermes API Server Key", + "hint": "Bearer token for the Hermes api_server. Must match the API_SERVER_KEY on the gateway host. Required.", + "required": true, + "env_variable": "API_SERVER_KEY", + "default": "" + }, + { + "type": "text", + "label": "Hermes api_server URL", + "hint": "Base URL of the Hermes api_server (e.g. http://192.168.1.10:8642). Used by the bridge for chat and model listing. host.docker.internal works for Docker Desktop; on Linux Runtipi, use the host IP.", + "required": false, + "env_variable": "HERMES_BASE_URL", + "default": "http://host.docker.internal:8642/v1" + }, + { + "type": "text", + "label": "Obsidian Local REST API URL", + "hint": "Base URL of the Obsidian Local REST API plugin. Default 27123. On Tailscale setups, use the Mac's Tailscale IP.", + "required": false, + "env_variable": "OBSIDIAN_REST_URL", + "default": "http://host.docker.internal:27123" + }, + { + "type": "password", + "label": "Obsidian Local REST API Key", + "hint": "API key from the Obsidian Local REST API plugin (Settings → Local REST API → API Key). Leave empty if the plugin is configured for no auth on a trusted network.", + "required": false, + "env_variable": "OBSIDIAN_REST_KEY", + "default": "" + }, + { + "type": "text", + "label": "Cheap Model", + "hint": "Model id to route short prompts to. Default uses umans-flash to burn expiring credits.", + "required": false, + "env_variable": "CHEAP_MODEL", + "default": "umans-flash" + }, + { + "type": "text", + "label": "Strong Model", + "hint": "Model id to route longer prompts to.", + "required": false, + "env_variable": "STRONG_MODEL", + "default": "minimax-m3" + }, + { + "type": "number", + "label": "Cheap Model Char Threshold", + "hint": "Prompts shorter than this (in characters) go to CHEAP_MODEL. Set to a large number to disable cheap-routing entirely.", + "required": false, + "env_variable": "CHEAP_MODEL_CHAR_THRESHOLD", + "default": 500 + }, + { + "type": "text", + "label": "Enable Vault RAG", + "hint": "Set to 'true' to pre-fetch the current note + today's daily + this week's review from Obsidian and prepend as context. Set to 'false' to disable.", + "required": false, + "env_variable": "RAG_ENABLED", + "default": "true" + } + ] +} diff --git a/apps/hermes-obsidian-bridge/docker-compose.json b/apps/hermes-obsidian-bridge/docker-compose.json new file mode 100644 index 0000000..ef287a8 --- /dev/null +++ b/apps/hermes-obsidian-bridge/docker-compose.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 2, + "services": [ + { + "name": "hermes-obsidian-bridge", + "image": "forge.407.lol/cori/hermes-obsidian-bridge:sha-7139662d", + "isMain": true, + "internalPort": 9120, + "command": "python -m bridge", + "environment": [ + { "key": "PORT", "value": "9120" }, + { "key": "API_SERVER_KEY", "value": "${API_SERVER_KEY}" }, + { "key": "HERMES_BASE_URL", "value": "${HERMES_BASE_URL}" }, + { "key": "OBSIDIAN_REST_URL", "value": "${OBSIDIAN_REST_URL}" }, + { "key": "OBSIDIAN_REST_KEY", "value": "${OBSIDIAN_REST_KEY}" }, + { "key": "CHEAP_MODEL", "value": "${CHEAP_MODEL}" }, + { "key": "STRONG_MODEL", "value": "${STRONG_MODEL}" }, + { "key": "CHEAP_MODEL_CHAR_THRESHOLD", "value": "${CHEAP_MODEL_CHAR_THRESHOLD}" }, + { "key": "RAG_ENABLED", "value": "${RAG_ENABLED}" }, + { "key": "LOG_LEVEL", "value": "info" } + ], + "volumes": [ + { "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/data" } + ], + "healthCheck": { + "test": "python -c \"import urllib.request,sys; r=urllib.request.urlopen('http://127.0.0.1:9120/health', timeout=3); sys.exit(0 if r.status==200 else 1)\"", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } + } + ] +} diff --git a/apps/hermes-obsidian-bridge/metadata/description.md b/apps/hermes-obsidian-bridge/metadata/description.md new file mode 100644 index 0000000..7bdd1d0 --- /dev/null +++ b/apps/hermes-obsidian-bridge/metadata/description.md @@ -0,0 +1,29 @@ +# Hermes Obsidian Bridge + +A thin OpenAI-compatible proxy in front of the Hermes `api_server`. Designed to be pointed +at from any client that speaks OpenAI chat-completions (Copilot for Obsidian, Text Generator, +Local GPT, Open WebUI, curl scripts). + +## What it adds + +- **Session continuity.** Pass `X-Session-Id` and the bridge forwards it; conversations resume + across separate HTTP calls instead of starting a fresh session every time. +- **Vault RAG.** Send `X-Obsidian-Note-Path` and the bridge reads the current note + today's + daily + this week's review from the Obsidian Local REST API and prepends them as context. +- **Cost routing.** Prompts shorter than `CHEAP_MODEL_CHAR_THRESHOLD` go to `CHEAP_MODEL`; + longer ones go to `STRONG_MODEL`. Explicit `model` in the request body is always respected. + +## Install + +Install via the Runtipi dashboard. Required field: `API_SERVER_KEY` (the bearer token for +the Hermes `api_server` — must match the value on the gateway host's `~/.hermes/.env`). + +## Wire to Obsidian + +Any OpenAI-compatible community plugin works. Recommended: **Copilot for Obsidian**, with +base URL `http://hermes-obsidian-bridge.407.lol/v1` and any string as the API key. + +## Image source + +Image is built and hosted at `forge.407.lol/cori/hermes-obsidian-bridge` (local Forgejo). +Source repo: https://github.com/cori/hermes-obsidian-bridge. diff --git a/apps/hermes-obsidian-bridge/metadata/logo.jpg b/apps/hermes-obsidian-bridge/metadata/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..911455a5569044b108e2d670425f3969c1dad0e9 GIT binary patch literal 5304 zcmeHKdsvcb7Jm^HQ`0EOu2O16x|@?-EX(MIq)iv9$u2f@H>=4kyJ+f{PGw;mnVQv9 z6yi@iDeh7*-W#+bdyNROidvn5cuAGX~#A`pQmkio0fEWL6V=JVp;OmR)GjnSo$l!D(yc44TW1=bW_Cg%qi3vbI@s8BpmlWUNc)c$ECf_L zGbS?04Omf0WGV^X1Pp}JhV*`cUlz##b+C6pH(c%ntVm?Cl{MLn8r>a>eh1c6 zo6da)kF)K~o4Gk4ySrOTFwt_WMXK61mxO`D^lzu&ffN8-+1{Cz2@Y5UU; z96TgCaWdn_Q>V|Iy?80>@|EnXIk`8AZr-|mr}*x@vPb2Qf0k7|sZ>0#QNDOt`>O7> zO8vG)qkX5-x0>TJ$Jw_285kihDvHa>+L~-lF~>!++JuZuwYKRy*tYXHE@e?f7dM}H zJKFf<Oo<2rMQ+R% z2*wJWuxG>WK)^VG_dIn++G5vSi<3hzRjc=)!k9XCtP2DkAJxcPtv9cSfFx$=OpNBI zWBegF#z|x~_o#tDP#U$7TpQ%|N~&8UbcNuddy!DrF#>`+uOQe_r)n};jUEZXRvE(> zmvsLE!;i~jLqNdAAp-w)!B%}4ztx+7ibaGiQa4Z@qz0E_?NGG??vlQ6Or)qFsU0do zJK*_wq_-KrMmm$4Cuk+A@S9S85APis|55CO+wV%=dSoRmOI@81XzLJEbk^yZ>X80a z4aJxy?Fm7}Is*hjuG^kA6e^h_{&5=`ckBm?bumOjaCd$gtAXN-r-lSSNNeFWK7ycM z_#_A}^kTJgBWB%0?1DUDa3U_nrkB`4eA! zD*1*~tAXHTN^o*8fnKRsmiTYd8X-8?1(!phtuA0o#M1B*eKM=joTu;VmGch7FuNgd zi$Czm%&jl#89|r=LCV%mdm2FL9F)7(Q_I z?dYaPJp?`jOi~CMuf-(sBI9Jz4*9_<4o+q10)|Q&Sr<%_Mn)Z#eF;m+HZ2ja&&&!-d9;*Icww zrlWCvHutQtjXz%>O{vJr<7GuXGF_>jl6PLo*3TG!=TC)eN0r|TDw#f_}_wNcu-TwijqardUbMKO%N zwWdiz{TxITTAW|V(YE@N^npA>xL)Jr*!sT3^C z(a1iAf)&MK<)|N~3Gh;$TPeq5i%p4Gs*ti?PlVtenm|^F^Ax^~TOe@YJhx3#2z2QC zOJknCMAd?YMdb~jsE(BnTz}-iYd_fy4WVXEDrg5nH1evq_|jVO zoMb~_%9bRD6)DEv;4{xr3D1f-1g*_&vTR%Ks_*e2`ClMt7#WMeIM3AXSch0+DOLqZ zrd&*UzK)?KCnCWf)nWwtl8!k9Vf)SJCUKbGEj<1-8~c$LQYR}7y`PhnCQ9?MKkTzNV4VA&n~(jN zWZq5JuVndimDqRn(R(gwW2>iWxBopwza=wojXL7Ui4pbjb#p(SF55nblx;{dk*D(^ z__`qHkwjFIzCO&0?YWdbVp*Tv37p=$&PGnRmF^t9NV54-F<$kvrgvzAAz#?&kTXF2 zZpp@{%9>FD5i)Yw#b2hv-&t?lroK)yoHFQm1r4{&LSE}x2Z^+*%^_(>4 z#Q`TubaRR2VUtDVzj&TnUBk^g`sPj4pf#s9?w|W|U6Xs}{UL*u`vN_VW_dJiv>r;- Q<