-
Notifications
You must be signed in to change notification settings - Fork 0
chore: subdirectory routing, configurable domain, and deployment guidelines #241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
47ce9c5
ee818d7
f542bd0
3292da2
75f4851
675a59c
36cbc72
c2a27e8
aded670
49b4f3a
e096379
a702b5c
86442b5
0404eff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| from circuit_breaker import get_breaker | ||
| from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator, RootModel | ||
| from typing import Dict, Optional, Union | ||
| from urllib.parse import urlparse | ||
|
|
||
| LITELLM_URL = (os.getenv("LITELLM_ADMIN_URL") or "http://127.0.0.1:4000").rstrip("/") | ||
| LLAMA_SERVER_URL = (os.getenv("LLAMA_SERVER_URL") or "http://127.0.0.1:8080").rstrip( | ||
|
|
@@ -3070,8 +3071,46 @@ async def get_dashboard_stats(): | |
|
|
||
|
|
||
| @app.get("/dashboard", response_class=HTMLResponse) | ||
| async def get_dashboard(): | ||
| async def get_dashboard(request: Request): | ||
| """Render the router main dashboard HTML showing system metrics, health checks, and recent token usage.""" | ||
| external_host_env = os.getenv("BASEURL") or os.getenv("BASE_URL") | ||
| if external_host_env: | ||
| if "://" not in external_host_env: | ||
| parsed = urlparse(f"http://{external_host_env}") | ||
| else: | ||
| parsed = urlparse(external_host_env) | ||
| external_host = parsed.hostname or "localhost" | ||
| external_netloc = parsed.netloc or "localhost" | ||
| else: | ||
| external_host = request.base_url.hostname or "localhost" | ||
| external_netloc = request.base_url.netloc or "localhost" | ||
|
|
||
| domain = os.getenv("ROUTING_DOMAIN") or "vendeuvre.lan" | ||
| if not isinstance(external_host, str) or not re.match(r"^[a-zA-Z0-9.-]+$", external_host): | ||
| external_host = "localhost" | ||
| if not isinstance(external_netloc, str) or not re.match(r"^[a-zA-Z0-9.-]+(?::\d+)?$", external_netloc): | ||
| external_netloc = "localhost" | ||
|
|
||
| # Enforce strict domain validation to prevent loose substring match bypasses (e.g., attacker-vendeuvre.lan) | ||
| is_valid_external = external_host == domain or external_host.endswith("." + domain) | ||
| is_valid_base = request.base_url.hostname == domain or (request.base_url.hostname or "").endswith("." + domain) | ||
|
|
||
| if is_valid_external: | ||
| langfuse_url = f"https://{external_netloc}/llm-routing/langfuse" | ||
| litellm_url = f"https://{external_netloc}/llm-routing/litellm/ui" | ||
| llama_url = f"https://{external_netloc}/llm-routing/llama/" | ||
| elif is_valid_base: | ||
| scheme = request.url.scheme if re.match(r"^(?:http|https)$", request.url.scheme) else "https" | ||
| netloc = request.url.netloc if re.match(r"^[a-zA-Z0-9.-]+(?::\d+)?$", request.url.netloc) else "localhost" | ||
| base = f"{scheme}://{netloc}" | ||
| langfuse_url = f"{base}/llm-routing/langfuse" | ||
| litellm_url = f"{base}/llm-routing/litellm/ui" | ||
| llama_url = f"{base}/llm-routing/llama/" | ||
| else: | ||
| langfuse_url = f"http://{external_host}:3001" | ||
| litellm_url = f"http://{external_host}:4000/ui" | ||
| llama_url = f"http://{external_host}:8080" | ||
|
Comment on lines
+3076
to
+3112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The domain validation logic uses loose substring matching ( Additionally, the duplicate external_host_env = os.getenv("BASEURL") or os.getenv("BASE_URL")
if external_host_env:
from urllib.parse import urlparse
if "://" not in external_host_env:
parsed = urlparse(f"http://{external_host_env}")
else:
parsed = urlparse(external_host_env)
external_host = parsed.hostname or "localhost"
external_netloc = parsed.netloc or "localhost"
else:
external_host = request.base_url.hostname or "localhost"
external_netloc = request.base_url.netloc or "localhost"
domain = os.getenv("ROUTING_DOMAIN") or "vendeuvre.lan"
if not isinstance(external_host, str) or not re.match(r"^[a-zA-Z0-9.-]+$", external_host):
external_host = "localhost"
if not isinstance(external_netloc, str) or not re.match(r"^[a-zA-Z0-9.-]+(?::\d+)?$", external_netloc):
external_netloc = "localhost"
# Strict domain validation to prevent substring matching vulnerabilities (e.g., attacker-vendeuvre.lan)
is_valid_external = external_host == domain or external_host.endswith("." + domain)
is_valid_base = request.base_url.hostname == domain or (request.base_url.hostname or "").endswith("." + domain)
if is_valid_external:
langfuse_url = f"https://{external_netloc}/llm-routing/langfuse"
litellm_url = f"https://{external_netloc}/llm-routing/litellm/ui"
llama_url = f"https://{external_netloc}/llm-routing/llama/"
elif is_valid_base:
scheme = request.url.scheme if re.match(r"^(?:http|https)$", request.url.scheme) else "https"
netloc = request.url.netloc if re.match(r"^[a-zA-Z0-9.-]+(?::\d+)?$", request.url.netloc) else "localhost"
base = f"{scheme}://{netloc}"
langfuse_url = f"{base}/llm-routing/langfuse"
litellm_url = f"{base}/llm-routing/litellm/ui"
llama_url = f"{base}/llm-routing/llama/"
else:
langfuse_url = f"http://{external_host}:3001"
litellm_url = f"http://{external_host}:4000/ui"
llama_url = f"http://{external_host}:8080" |
||
|
|
||
| data = await get_dashboard_data() | ||
|
|
||
| # Unpack data for the f-string template | ||
|
|
@@ -3710,7 +3749,7 @@ async def get_dashboard(): | |
| </div> | ||
| <div style="text-align: center; padding: 25px 20px;"> | ||
| <p style="opacity: 0.7; margin-bottom: 14px; font-size: 14px;">Per-model usage, token consumption & cost are tracked with full trace detail in Langfuse.</p> | ||
| <a href="http://localhost:3001" target="_blank" style="display: inline-block; padding: 8px 18px; background: rgba(232,121,249,0.12); color: #e879f9; border: 1px solid rgba(232,121,249,0.25); border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 13px;">Open Langfuse Observability →</a> | ||
| <a href="{langfuse_url}" target="_blank" style="display: inline-block; padding: 8px 18px; background: rgba(232,121,249,0.12); color: #e879f9; border: 1px solid rgba(232,121,249,0.25); border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 13px;">Open Langfuse Observability →</a> | ||
| </div> | ||
| </div> | ||
|
|
||
|
|
@@ -3846,15 +3885,15 @@ async def get_dashboard(): | |
| </a> | ||
| </div> | ||
| <div class="btn-group"> | ||
| <a href="http://localhost:3001" target="_blank" class="btn"> | ||
| <a href="{langfuse_url}" target="_blank" class="btn"> | ||
| <span>{src_badge("LANGFUSE", "#e879f9")} Observability UI</span> | ||
| <span class="btn-arrow">→</span> | ||
| </a> | ||
| <a href="http://localhost:4000/ui" target="_blank" class="btn"> | ||
| <a href="{litellm_url}" target="_blank" class="btn"> | ||
| <span>{src_badge("LITELLM", "#34d399")} Admin UI</span> | ||
| <span class="btn-arrow">→</span> | ||
| </a> | ||
| <a href="http://localhost:8080" target="_blank" class="btn"> | ||
| <a href="{llama_url}" target="_blank" class="btn"> | ||
| <span>{src_badge("LLAMA.CPP", "#fb923c")} Server Router UI</span> | ||
| <span class="btn-arrow">→</span> | ||
| </a> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When starting a background process using
nohup ... &over SSH, failing to redirect standard input (stdin) can cause the SSH session to hang or terminate the background process when the connection closes. Redirecting stdin from/dev/nullensures the background process is properly decoupled.