add web ui openrouter attribution (#1057)

This commit is contained in:
ekko
2026-05-27 11:21:12 +08:00
committed by GitHub
parent 07c4c1ddd5
commit eca06faaa0
3 changed files with 71 additions and 6 deletions
@@ -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]:
@@ -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, {
+22
View File
@@ -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')