232 lines
9.2 KiB
Python
232 lines
9.2 KiB
Python
#!/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",
|
|
)
|