atlas-iac/dockerfiles/hermes-webui-router-patch.py

232 lines
9.2 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Add Atlas per-turn routing controls to the community Hermes WebUI."""
import os
from pathlib import Path
ROOT = Path(os.environ.get("HERMES_WEBUI_PATCH_ROOT", "/opt/hermes-webui"))
def replace_once(path: Path, before: str, after: str, label: str) -> None:
"""Replace one pinned upstream fragment or fail the image build."""
source = path.read_text(encoding="utf-8")
count = source.count(before)
if count != 1:
raise SystemExit(f"{label} context changed: expected 1, found {count}")
path.write_text(source.replace(before, after, 1), encoding="utf-8")
index = ROOT / "static/index.html"
replace_once(
index,
''' <div class="composer-ws-wrap">\n''',
''' <div class="composer-routing-wrap" id="composerRoutingWrap">
<button class="composer-routing-chip" id="composerRoutingChip" type="button" title="Automatic routing priority" aria-haspopup="true" aria-expanded="false" aria-controls="composerRoutingDropdown">
<span class="composer-routing-label" id="composerRoutingLabel">AUTO</span>
<span class="composer-routing-chevron" aria-hidden="true"></span>
</button>
</div>
<div class="composer-ws-wrap">\n''',
"routing chip",
)
replace_once(
index,
''' <div class="composer-reasoning-dropdown" id="composerReasoningDropdown">\n''',
''' <div class="composer-routing-dropdown" id="composerRoutingDropdown" role="menu" aria-label="Automatic routing priority">
<div class="routing-option" data-priority="auto"><strong>Auto</strong><span>Use this service's default</span></div>
<div class="routing-option" data-priority="fast"><strong>Fast</strong><span>Prefer a quicker capable route</span></div>
<div class="routing-option" data-priority="balanced"><strong>Balanced</strong><span>Balance latency and depth</span></div>
<div class="routing-option" data-priority="deep"><strong>Deep</strong><span>Favor careful reasoning</span></div>
<div class="routing-option" data-priority="maximum"><strong>Maximum</strong><span>Strongest route, capped at xhigh</span></div>
</div>
<div class="composer-reasoning-dropdown" id="composerReasoningDropdown">\n''',
"routing dropdown",
)
replace_once(
index,
'''<script src="static/ui.js?v=__WEBUI_VERSION__" defer></script>\n''',
'''<script src="static/ui.js?v=__WEBUI_VERSION__" defer></script>
<script src="static/atlas-router.js?v=__WEBUI_VERSION__" defer></script>\n''',
"router script",
)
style = ROOT / "static/style.css"
replace_once(
style,
''' .composer-reasoning-chip{display:inline-flex;align-items:center;gap:5px;max-width:none;padding:8px 10px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}\n''',
''' .composer-routing-chip{display:inline-flex;align-items:center;gap:5px;max-width:none;padding:8px 10px;border-radius:999px;border:1px solid var(--accent-bg);background:var(--accent-bg);color:var(--accent-text);font-weight:600;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}
.composer-routing-chip:hover,.composer-routing-chip.active{color:var(--text);border-color:var(--accent);}
.composer-routing-label{font-size:11px;font-weight:700;letter-spacing:.04em;}
.composer-routing-chevron{font-size:11px;line-height:1;}
.composer-routing-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;min-width:250px;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:205;padding:4px;overflow:hidden;}
.composer-routing-dropdown.open{display:block;}
.routing-option{display:flex;flex-direction:column;gap:2px;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--text);white-space:nowrap;transition:background-color .12s;}
.routing-option span{font-size:11px;color:var(--muted);font-weight:400;}
.routing-option:hover{background:rgba(255,255,255,.07);}
.routing-option.selected{background:var(--accent-bg);}
.composer-reasoning-chip{display:inline-flex;align-items:center;gap:5px;max-width:none;padding:8px 10px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}\n''',
"routing styles",
)
messages = ROOT / "static/messages.js"
replace_once(
messages,
''' explicit_model_pick:_explicitPick||undefined,
attachments:uploaded.length?uploaded:undefined,
''',
''' explicit_model_pick:_explicitPick||undefined,
...((typeof window.hermesRoutingRequest==='function')?window.hermesRoutingRequest():{}),
attachments:uploaded.length?uploaded:undefined,
''',
"chat routing payload",
)
ui = ROOT / "static/ui.js"
replace_once(
ui,
''' _currentReasoningEffort=effort;
''',
''' _currentReasoningEffort=effort;
window._atlasCurrentReasoningEffort=effort;
''',
"reasoning effort bridge",
)
routes = ROOT / "api/routes.py"
replace_once(
routes,
''' gateway_chat_enabled: bool | None = None,
):
''',
''' gateway_chat_enabled: bool | None = None,
routing_priority: str = "",
explicit_model_pick: bool = False,
explicit_reasoning_effort: str = "",
):
''',
"start run routing arguments",
)
replace_once(
routes,
''' external_runtime_owned=gateway_chat_enabled,
)
''',
''' external_runtime_owned=gateway_chat_enabled,
routing_priority=routing_priority,
explicit_model_pick=explicit_model_pick,
explicit_reasoning_effort=explicit_reasoning_effort,
)
''',
"adapter routing forwarding",
)
replace_once(
routes,
''' external_runtime_owned=gateway_chat_enabled,
)
def _process_wakeup_revalidation_provider''',
''' external_runtime_owned=gateway_chat_enabled,
routing_priority=routing_priority,
explicit_model_pick=explicit_model_pick,
explicit_reasoning_effort=explicit_reasoning_effort,
)
def _process_wakeup_revalidation_provider''',
"direct routing forwarding",
)
replace_once(
routes,
''' external_runtime_owned: bool | None = None,
):
''',
''' external_runtime_owned: bool | None = None,
routing_priority: str = "",
explicit_model_pick: bool = False,
explicit_reasoning_effort: str = "",
):
''',
"stream routing arguments",
)
replace_once(
routes,
''' worker_kwargs = {"model_provider": model_provider, "goal_related": goal_related}
''',
''' worker_kwargs = {"model_provider": model_provider, "goal_related": goal_related}
if backend_is_gateway:
worker_kwargs.update({
"routing_priority": routing_priority,
"explicit_model_pick": explicit_model_pick,
"explicit_reasoning_effort": explicit_reasoning_effort,
})
''',
"gateway worker routing arguments",
)
replace_once(
routes,
''' explicit_model_pick = bool(body.get("explicit_model_pick"))
moa_config = None
''',
''' explicit_model_pick = bool(body.get("explicit_model_pick"))
routing_priority = str(body.get("routing_priority") or "").strip().lower()
if routing_priority not in {"", "auto", "fast", "balanced", "deep", "maximum"}:
return bad(handler, "invalid routing priority", 400)
if routing_priority == "auto":
routing_priority = ""
explicit_reasoning_effort = str(
body.get("explicit_reasoning_effort") or ""
).strip().lower()
if explicit_reasoning_effort not in {
"", "none", "minimal", "low", "medium", "high", "xhigh"
}:
return bad(handler, "invalid explicit reasoning effort", 400)
moa_config = None
''',
"chat routing validation",
)
replace_once(
routes,
''' "gateway_chat_enabled": gateway_chat_enabled,
}
''',
''' "gateway_chat_enabled": gateway_chat_enabled,
"routing_priority": routing_priority,
"explicit_model_pick": explicit_model_pick,
"explicit_reasoning_effort": explicit_reasoning_effort,
}
''',
"chat routing start arguments",
)
gateway = ROOT / "api/gateway_chat.py"
replace_once(
gateway,
''' model_provider=None,
goal_related=False,
):
''',
''' model_provider=None,
goal_related=False,
routing_priority="",
explicit_model_pick=False,
explicit_reasoning_effort="",
):
''',
"gateway routing arguments",
)
replace_once(
gateway,
''' if _gw_overrides.get("service_tier"):
body_extras["service_tier"] = _gw_overrides["service_tier"]
''',
''' if _gw_overrides.get("service_tier"):
body_extras["service_tier"] = _gw_overrides["service_tier"]
body_extras["routing_priority"] = routing_priority
body_extras["explicit_model_pick"] = bool(explicit_model_pick)
body_extras["explicit_reasoning_effort"] = explicit_reasoning_effort
''',
"runs API routing body",
)