diff --git a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py index 62f28332..95b0cf7c 100755 --- a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py +++ b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py @@ -42,6 +42,11 @@ DEFAULT_HERMES_HOME = "~/.hermes" APPROVAL_TIMEOUT_SECONDS = 120 APPROVAL_TIMEOUT_MS = APPROVAL_TIMEOUT_SECONDS * 1000 PARENT_WATCHDOG_INTERVAL_SECONDS = 2.0 +OPENROUTER_ATTRIBUTION_ENV = { + "referer": "HERMES_OPENROUTER_APP_REFERER", + "title": "HERMES_OPENROUTER_APP_TITLE", + "categories": "HERMES_OPENROUTER_APP_CATEGORIES", +} def _bridge_platform() -> str: @@ -349,6 +354,32 @@ def _ensure_agent_imports() -> None: ) os.environ.setdefault("HERMES_HOME", str(_hermes_home())) os.environ.setdefault("HERMES_AGENT_BRIDGE_BASE_HOME", str(_hermes_home())) + _apply_openrouter_attribution_override() + + +def _apply_openrouter_attribution_override() -> None: + """Override hermes-agent OpenRouter attribution at bridge runtime only.""" + referer = os.environ.get(OPENROUTER_ATTRIBUTION_ENV["referer"], "").strip() + title = os.environ.get(OPENROUTER_ATTRIBUTION_ENV["title"], "").strip() + categories = os.environ.get(OPENROUTER_ATTRIBUTION_ENV["categories"], "").strip() + if not (referer or title or categories): + return + try: + from agent import auxiliary_client + except Exception: + return + headers = dict(getattr(auxiliary_client, "_OR_HEADERS_BASE", {}) or {}) + if referer: + headers["HTTP-Referer"] = referer + if title: + headers.pop("X-Title", None) + headers["X-OpenRouter-Title"] = title + if categories: + headers["X-OpenRouter-Categories"] = categories + try: + auxiliary_client._OR_HEADERS_BASE = headers + except Exception: + pass def _load_cfg(profile: str | None = None) -> dict[str, Any]: diff --git a/packages/server/src/services/hermes/agent-bridge/manager.ts b/packages/server/src/services/hermes/agent-bridge/manager.ts index bb13a3c6..6d91ac14 100644 --- a/packages/server/src/services/hermes/agent-bridge/manager.ts +++ b/packages/server/src/services/hermes/agent-bridge/manager.ts @@ -9,6 +9,11 @@ import { DEFAULT_AGENT_BRIDGE_ENDPOINT } from './client' const DEFAULT_AGENT_BRIDGE_STARTUP_TIMEOUT_MS = 120000 const DEFAULT_AGENT_BRIDGE_RESTART_DELAY_MS = 1000 const MAX_AGENT_BRIDGE_RESTART_DELAY_MS = 30000 +const OPENROUTER_WEB_UI_ATTRIBUTION_ENV = { + HERMES_OPENROUTER_APP_REFERER: 'https://ekkolearnai.com', + HERMES_OPENROUTER_APP_TITLE: 'Hermes Web UI', + HERMES_OPENROUTER_APP_CATEGORIES: 'cli-agent,personal-agent', +} as const export interface AgentBridgeManagerOptions { endpoint?: string @@ -43,6 +48,18 @@ function envPositiveInt(name: string): number | undefined { return Number.isFinite(value) && value > 0 ? value : undefined } +export function buildAgentBridgeProcessEnv(endpoint: string, hermesHome: string | undefined, agentRoot: string | undefined): NodeJS.ProcessEnv { + return { + ...process.env, + HERMES_AGENT_BRIDGE_ENDPOINT: endpoint, + HERMES_HOME: hermesHome, + HERMES_OPENROUTER_APP_REFERER: process.env.HERMES_OPENROUTER_APP_REFERER || OPENROUTER_WEB_UI_ATTRIBUTION_ENV.HERMES_OPENROUTER_APP_REFERER, + HERMES_OPENROUTER_APP_TITLE: process.env.HERMES_OPENROUTER_APP_TITLE || OPENROUTER_WEB_UI_ATTRIBUTION_ENV.HERMES_OPENROUTER_APP_TITLE, + HERMES_OPENROUTER_APP_CATEGORIES: process.env.HERMES_OPENROUTER_APP_CATEGORIES || OPENROUTER_WEB_UI_ATTRIBUTION_ENV.HERMES_OPENROUTER_APP_CATEGORIES, + ...(agentRoot ? { HERMES_AGENT_ROOT: agentRoot } : {}), + } +} + function pathCandidates(agentRoot?: string): string[] { if (!agentRoot) return [] return process.platform === 'win32' @@ -358,12 +375,7 @@ export class AgentBridgeManager { if (agentRoot) args.push('--agent-root', agentRoot) if (hermesHome) args.push('--hermes-home', hermesHome) - const env = { - ...process.env, - HERMES_AGENT_BRIDGE_ENDPOINT: this.endpoint, - HERMES_HOME: hermesHome, - ...(agentRoot ? { HERMES_AGENT_ROOT: agentRoot } : {}), - } + const env = buildAgentBridgeProcessEnv(this.endpoint, hermesHome, agentRoot) logger.info('[agent-bridge] starting: %s %s', command.command, args.join(' ')) const child = spawn(command.command, args, { diff --git a/tests/server/agent-bridge-manager.test.ts b/tests/server/agent-bridge-manager.test.ts index 6a499a66..06ff55c4 100644 --- a/tests/server/agent-bridge-manager.test.ts +++ b/tests/server/agent-bridge-manager.test.ts @@ -93,6 +93,28 @@ describe('agent bridge manager command resolution', () => { }) }) + it('injects Web UI OpenRouter attribution into the bridge process env by default', async () => { + const { buildAgentBridgeProcessEnv } = await import('../../packages/server/src/services/hermes/agent-bridge/manager') + const env = buildAgentBridgeProcessEnv('ipc:///tmp/test.sock', '/tmp/hermes-home', '/tmp/hermes-agent') + + expect(env.HERMES_OPENROUTER_APP_REFERER).toBe('https://ekkolearnai.com') + expect(env.HERMES_OPENROUTER_APP_TITLE).toBe('Hermes Web UI') + expect(env.HERMES_OPENROUTER_APP_CATEGORIES).toBe('cli-agent,personal-agent') + }) + + it('keeps explicit OpenRouter attribution env values when starting the bridge', async () => { + process.env.HERMES_OPENROUTER_APP_REFERER = 'https://example.invalid/app' + process.env.HERMES_OPENROUTER_APP_TITLE = 'Custom App' + process.env.HERMES_OPENROUTER_APP_CATEGORIES = 'custom-category' + + const { buildAgentBridgeProcessEnv } = await import('../../packages/server/src/services/hermes/agent-bridge/manager') + const env = buildAgentBridgeProcessEnv('ipc:///tmp/test.sock', '/tmp/hermes-home', undefined) + + expect(env.HERMES_OPENROUTER_APP_REFERER).toBe('https://example.invalid/app') + expect(env.HERMES_OPENROUTER_APP_TITLE).toBe('Custom App') + expect(env.HERMES_OPENROUTER_APP_CATEGORIES).toBe('custom-category') + }) + it('uses an isolated default bridge endpoint while running under Vitest', async () => { const { DEFAULT_AGENT_BRIDGE_ENDPOINT } = await import('../../packages/server/src/services/hermes/agent-bridge/client')