Skip to content

chore: robust URL resolution, consolidated domain defaults, and generalized runbook (PR #244 fixes)#246

Merged
sheepdestroyer merged 21 commits into
masterfrom
chore/learn-deployment-guidelines
Jul 9, 2026
Merged

chore: robust URL resolution, consolidated domain defaults, and generalized runbook (PR #244 fixes)#246
sheepdestroyer merged 21 commits into
masterfrom
chore/learn-deployment-guidelines

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jul 9, 2026

Copy link
Copy Markdown
Owner

This PR resolves review comments from PR #244, introduces robust URL resolution safety, consolidates routing domain config, and refactors local port-independent fallbacks.

Key Changes

  1. Centralized Base URL Resolution & Deduplication:
    • Extracted all URL parsing, validation, and domain-checking logic from get_dashboard into a small, testable utility function resolve_external_urls(request: Request) -> tuple[str, str, str] in router/main.py.
    • Used PUBLIC_BASE_URL (passed directly to the llm-triage-router container) as the primary source of truth, fallbacking to BASEURL/BASE_URL/request.base_url securely to prevent configuration drift between shell scripts and Python.
  2. Robust Validation & Logging Fallbacks:
    • Enhanced the hostname/netloc validation inside resolve_external_urls in router/main.py to support reverse proxy netlocs, IPv6, and IDN punycodes.
    • Replaced rigid regex rejections with fallback warnings utilizing the llm-triage-router logger.
  3. Consolidated Domain/URL Defaults:
    • Centralized default domain configurations around a single ROUTING_DOMAIN variable (default: vendeuvre.lan), exporting and passing it directly to the container in pod.yaml.
    • Computed default base URLs dynamically in start-stack.sh leveraging ROUTING_DOMAIN.
  4. Port/Config Drift Prevention in Fallbacks:
    • Derived local fallback ports and paths dynamically using the existing config constants: LANGFUSE_HOST, LITELLM_URL, and LLAMA_SERVER_URL via urlparse instead of hardcoding ports.
  5. Generalized Production Runbook:
    • Refactored the production deployment checklist in .agents/AGENTS.md to replace host-specific paths and user names (boy, /mnt/DATA/boy/..., x570.vendeuvre.lan) with clean, generic variables (<prod-host>, <prod-home>, <prod-domain>), ensuring portability.

Summary by Sourcery

Centralize and harden external URL resolution for the dashboard while aligning deployment and routing configuration around portable, environment-driven base URLs and domains.

Enhancements:

  • Add a shared URL resolution helper used by the dashboard to derive Langfuse, LiteLLM, and Llama endpoints from PUBLIC_BASE_URL, ROUTING_DOMAIN, and local fallback config.
  • Improve hostname/netloc validation and logging to be more tolerant of reverse proxies and non-local hosts while maintaining strict domain checks and safe fallbacks.
  • Standardize pod.yaml placeholders and environment exports to remove host-specific paths and propagate PUBLIC_BASE_URL and ROUTING_DOMAIN into containers.
  • Update stack startup messaging and proxy configuration to use PUBLIC_BASE_URL/LOCAL_BASE_URL and expose a consistent LiteLLM proxy base URL.
  • Re-enable the local-qwen-3.6 model in LiteLLM with an updated llama.cpp endpoint and ensure routing tiers include it.
  • Generalize various hardcoded paths and references in README and router/agy_proxy.py to be user-agnostic and repo-relative.

Documentation:

  • Rename and retarget the NotebookLM companion notebook in .agents/AGENTS.md and add a generalized, host-agnostic production deployment checklist with SSH, HAProxy, and daemon guidance.
  • Clarify README paths, memory MCP server references, and systemd ProtectHome description to avoid user-specific filesystem details.

sheepdestroyer and others added 20 commits July 8, 2026 21:04
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…al_host/port resolution, fix agy startup SSH commands
…add fallbacks, avoid duplicate variables)
…PLACEHOLDER, HOME_PLACEHOLDER, and RUN_USER_PLACEHOLDER
@sourcery-ai

sourcery-ai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Centralizes external URL resolution and routing domain configuration, adds safer validation and fallbacks for dashboard links, exports derived base URLs into the pod, and generalizes production/runbook documentation while re-enabling a local model in LiteLLM.

Sequence diagram for dashboard URL resolution with resolve_external_urls

sequenceDiagram
    actor User
    participant FastAPIApp
    participant get_dashboard
    participant resolve_external_urls

    User->>FastAPIApp: HTTP_GET_/dashboard
    FastAPIApp->>get_dashboard: route_handler
    get_dashboard->>resolve_external_urls: resolve_external_urls(request)
    resolve_external_urls-->>get_dashboard: langfuse_url, litellm_url, llama_url
    get_dashboard-->>User: HTMLResponse_with_dynamic_links
Loading

File-Level Changes

Change Details Files
Centralized and hardened external URL resolution for dashboard links with dynamic local fallbacks.
  • Introduced resolve_external_urls(request) helper in router/main.py to compute Langfuse, LiteLLM, and Llama URLs from PUBLIC_BASE_URL/BASEURL/BASE_URL or request.base_url
  • Added ROUTING_DOMAIN-based domain validation and relaxed netloc parsing to support reverse proxies, IPv6, and punycode while logging fallbacks instead of forcing localhost
  • Switched get_dashboard to accept Request and to use resolve_external_urls for all external observability/admin links
  • Added LANGFUSE_HOST constant and reused it for Langfuse client initialization to avoid duplicated host configuration
router/main.py
Consolidated routing domain and base URL configuration and wired it into the pod template.
  • Defined and exported ROUTING_DOMAIN plus derived PUBLIC_BASE_URL and LOCAL_BASE_URL in start-stack.sh with normalization of scheme and /llm-routing path
  • Extended render_pod_yaml to export PUBLIC_BASE_URL and ROUTING_DOMAIN, added new placeholders (WORKDIR/HOME/RUN_USER/PROXY_BASE_URL/PUBLIC_BASE_URL/ROUTING_DOMAIN), and replaced hardcoded host paths with generic placeholders
  • Derived PROXY_BASE_URL from PUBLIC_BASE_URL and injected PUBLIC_BASE_URL/ROUTING_DOMAIN into pod.yaml env for the router and LiteLLM containers
  • Updated volume hostPath entries and DBUS-related mounts in pod.yaml to use WORKDIR_PLACEHOLDER, HOME_PLACEHOLDER, and RUN_USER_PLACEHOLDER instead of user-specific paths
start-stack.sh
pod.yaml
Generalized production deployment runbook and path references for portability.
  • Renamed the recommended NotebookLM notebook and updated its display name in .agents/AGENTS.md
  • Added a production deployment checklist that uses abstracted placeholders , , and for SSH, HAProxy, and daemon commands
  • Clarified notes about agy-daemon systemd limitations, sudo password handling, stack restarts, and GitHub CLI token usage
  • Replaced user-specific paths and file URLs in README and router/agy_proxy.py with generic or repository-relative paths
.agents/AGENTS.md
README.md
router/agy_proxy.py
Re-enabled the local-qwen-3.6 model via llama.cpp and exposed it to routing groups.
  • Uncommented and restored local-qwen-3.6 in multiple routing group configurations in litellm/config.yaml
  • Added a new local-qwen-3.6 model entry pointing to llama.cpp on port 8081 with api_base http://127.0.0.1:8081/v1 and model openai/local-qwen-3.6
litellm/config.yaml

Possibly linked issues

  • #LHF-03: PR defines env-based URL constants and updates router/dashboard logic to remove hardcoded microservice endpoints as specified.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@sheepdestroyer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d9048ac-bf26-4442-a806-0bfbff0ca556

📥 Commits

Reviewing files that changed from the base of the PR and between 454593c and 8331d9b.

📒 Files selected for processing (8)
  • .agents/AGENTS.md
  • README.md
  • litellm/config.yaml
  • pod.yaml
  • router/agy_proxy.py
  • router/free_models_roster.json
  • router/main.py
  • start-stack.sh
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/learn-deployment-guidelines

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="router/main.py" line_range="3133-3134" />
<code_context>
+            f"{external_scheme}://{external_netloc}/llm-routing/litellm/ui",
+            f"{external_scheme}://{external_netloc}/llm-routing/llama/"
+        )
+    elif is_valid_base:
+        netloc = request.url.netloc if re.match(r"^[a-zA-Z0-9.-]+(?::\d+)?$", request.url.netloc) else "localhost"
+        base = f"{external_scheme}://{netloc}"
+        return (
</code_context>
<issue_to_address>
**issue:** Netloc validation here still rejects IPv6 and some proxy-style netlocs, undermining the earlier relaxed handling.

The earlier `resolve_external_urls` branch relaxes validation via `urlparse`, allowing IPv6 literals and proxy-style netlocs. Here, `^[a-zA-Z0-9.-]+(?::\d+)?$` rejects valid values like `[::1]:5000`, forcing an incorrect fallback to `localhost`. To keep behavior consistent and avoid breaking proxied/IPv6 deployments, consider parsing `request.url.netloc` with `urlparse` and only falling back when the parsed hostname is missing or invalid.
</issue_to_address>

### Comment 2
<location path="router/main.py" line_range="3142-3151" />
<code_context>
+            f"{base}/llm-routing/llama/"
+        )
+    else:
+        # Local development fallback: derive ports and paths dynamically from configuration constants
+        parsed_lf = urlparse(LANGFUSE_HOST)
+        parsed_ll = urlparse(LITELLM_URL)
+        parsed_lm = urlparse(LLAMA_SERVER_URL)
+
+        lf_port = f":{parsed_lf.port}" if parsed_lf.port else ""
+        ll_port = f":{parsed_ll.port}" if parsed_ll.port else ""
+        lm_port = f":{parsed_lm.port}" if parsed_lm.port else ""
+
+        lf_path = parsed_lf.path
+        ll_path = parsed_ll.path or "/ui"
+        if not ll_path.endswith("/ui") and not ll_path.endswith("/ui/"):
+            ll_path = ll_path.rstrip("/") + "/ui"
+        lm_path = parsed_lm.path
+
+        return (
+            f"http://{external_host}{lf_port}{lf_path}",
+            f"http://{external_host}{ll_port}{ll_path}",
+            f"http://{external_host}{lm_port}{lm_path}"
+        )
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Local fallback hardcodes `http` and ignores the original schemes, which can break HTTPS dev setups.

In the local-dev fallback, all three URLs are forced to `http://{external_host}...`, even if `LANGFUSE_HOST`, `LITELLM_URL`, or `LLAMA_SERVER_URL` use HTTPS or another scheme. This can cause mixed-content errors or break setups where services only listen on HTTPS. Please derive the scheme from `parsed_lf`, `parsed_ll`, and `parsed_lm` (defaulting to `http` when absent) instead of hardcoding `http`.

Suggested implementation:

```python
    else:
        # Local development fallback: derive schemes, ports, and paths dynamically from configuration constants
        parsed_lf = urlparse(LANGFUSE_HOST)
        parsed_ll = urlparse(LITELLM_URL)
        parsed_lm = urlparse(LLAMA_SERVER_URL)

        lf_scheme = parsed_lf.scheme or "http"
        ll_scheme = parsed_ll.scheme or "http"
        lm_scheme = parsed_lm.scheme or "http"

        lf_port = f":{parsed_lf.port}" if parsed_lf.port else ""
        ll_port = f":{parsed_ll.port}" if parsed_ll.port else ""
        lm_port = f":{parsed_lm.port}" if parsed_lm.port else ""

        lf_path = parsed_lf.path or ""
        ll_path = parsed_ll.path or "/ui"
        if not ll_path.endswith("/ui") and not ll_path.endswith("/ui/"):
            ll_path = ll_path.rstrip("/") + "/ui"
        lm_path = parsed_lm.path or ""

        return (
            f"{lf_scheme}://{external_host}{lf_port}{lf_path}",
            f"{ll_scheme}://{external_host}{ll_port}{ll_path}",
            f"{lm_scheme}://{external_host}{lm_port}{lm_path}"
        )

```

If the surrounding code assumes non-empty paths, you may want to normalize `lf_path` and `lm_path` (e.g., ensure they start with `/`) based on how `LANGFUSE_HOST`, `LITELLM_URL`, and `LLAMA_SERVER_URL` are typically configured elsewhere in the codebase.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread router/main.py Outdated
Comment thread router/main.py Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces dynamic URL resolution for Langfuse, LiteLLM, and Llama.cpp in the router dashboard, replacing hardcoded local paths and hostnames with environment-driven placeholders across configurations, scripts, and documentation. It also re-enables the local Qwen model on a new port, updates the free models roster, and adds a production deployment checklist. The review feedback highlights a duplicate import of urlparse and recommends improving IPv6 compatibility in the new URL resolution logic by allowing colons in the host validation regex and wrapping IPv6 addresses in square brackets for fallback URLs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread router/main.py Outdated
Comment on lines +3094 to +3100
if not isinstance(external_host, str) or not re.match(r"^[a-zA-Z0-9.-]+$", external_host):
logger.warning(
"Unexpected external_host %r, falling back to request.base_url.hostname (%r)",
external_host,
request.base_url.hostname,
)
external_host = request.base_url.hostname or "localhost"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The regular expression r"^[a-zA-Z0-9.-]+$" used for sanity-checking external_host does not allow colons (:), which causes valid IPv6 addresses (e.g., ::1) to fail the check and trigger a warning, even though IPv6 support is intended.

Suggested change
if not isinstance(external_host, str) or not re.match(r"^[a-zA-Z0-9.-]+$", external_host):
logger.warning(
"Unexpected external_host %r, falling back to request.base_url.hostname (%r)",
external_host,
request.base_url.hostname,
)
external_host = request.base_url.hostname or "localhost"
if not isinstance(external_host, str) or not re.match(r"^[a-zA-Z0-9.-:]+$", external_host):
logger.warning(
"Unexpected external_host %r, falling back to request.base_url.hostname (%r)",
external_host,
request.base_url.hostname,
)
external_host = request.base_url.hostname or "localhost"

Comment thread router/main.py
Comment on lines +3157 to +3161
return (
f"http://{external_host}{lf_port}{lf_path}",
f"http://{external_host}{ll_port}{ll_path}",
f"http://{external_host}{lm_port}{lm_path}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When formatting fallback URLs for local development, if external_host is an IPv6 address (containing colons), it must be enclosed in square brackets (e.g., [::1]) to produce syntactically valid URLs.

Suggested change
return (
f"http://{external_host}{lf_port}{lf_path}",
f"http://{external_host}{ll_port}{ll_path}",
f"http://{external_host}{lm_port}{lm_path}"
)
host_formatted = f"[{external_host}]" if ":" in external_host else external_host
return (
f"http://{host_formatted}{lf_port}{lf_path}",
f"http://{host_formatted}{ll_port}{ll_path}",
f"http://{host_formatted}{lm_port}{lm_path}"
)

Comment thread router/main.py Outdated
Comment on lines +24 to +25
from typing import Dict, Optional, Union
from urllib.parse import urlparse

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The urlparse function is imported twice in this file (on line 21 and line 25). Removing the duplicate import will clean up the code and improve maintainability.

Suggested change
from typing import Dict, Optional, Union
from urllib.parse import urlparse
from typing import Dict, Optional, Union

…tibility in external URL resolution and local fallback formatting
Comment thread router/main.py

# Basic sanity-check on external_host, but don't over-restrict valid hostnames;
# fall back to the request base URL rather than silently forcing localhost.
if not isinstance(external_host, str) or not re.match(r"^[a-zA-Z0-9.-:]+$", external_host):
@sheepdestroyer
sheepdestroyer merged commit 32bf961 into master Jul 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants