Skip to content

fix: HAProxy routing, LiteLLM UI auth, Langfuse redirect + canonical endpoint tests#256

Closed
sheepdestroyer wants to merge 5 commits into
masterfrom
test/canonical-endpoint-verification
Closed

fix: HAProxy routing, LiteLLM UI auth, Langfuse redirect + canonical endpoint tests#256
sheepdestroyer wants to merge 5 commits into
masterfrom
test/canonical-endpoint-verification

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes four production issues and adds comprehensive endpoint verification.

HAProxy routing fixes

  • Langfuse /_next assets 404: Moved root-relative SPA asset rules before the host_dashboard catch-all so Langfuse static assets route correctly
  • LiteLLM /ui/login 404: Added use_backend litellm-backend if url_litellm_ui before the catch-all so LiteLLM SPA routes work

Container env fixes

  • LiteLLM UI password not recognized: .env had UI_USERNAME/UI_PASSWORD but LiteLLM expects LITELLM_UI_USERNAME/LITELLM_UI_PASSWORD — added to pod.yaml + start-stack.sh rendering
  • Langfuse post-login redirect to localhost: NEXTAUTH_URL was hardcoded to http://localhost:LANGFUSE_WEB_PORT — now derived from PUBLIC_BASE_URL

Verification script (scripts/verification/verify_canonical_endpoints.py)

New comprehensive endpoint test suite covering:

Section Endpoints
Router API /v1/models, /metrics, /dashboard, /api/dashboard-stats, /visualizer
LiteLLM /health/liveness, /health/readiness, /v1/models, /llm-routing/litellm/ui/
Langfuse /api/public/health, / (web UI)
Infrastructure MinIO /minio/health/live, ClickHouse /ping
E2E chat 3 completions through triage router
LiteLLM direct 1 completion directly to LiteLLM
Canonical URLs 6 HTTPS endpoints (graceful DNS skip)

Test results

  • 92 unit tests: all passing
  • Prod verification: 17/17 passed
  • Dev verification: 22/22 passed

Summary by Sourcery

Fix environment configuration for LiteLLM and Langfuse and add a script to verify canonical endpoints across router, UI, and infrastructure services.

New Features:

  • Add a canonical endpoint verification script that exercises router APIs, LiteLLM and Langfuse UIs, infrastructure health checks, and chat completion flows for prod and dev environments.

Bug Fixes:

  • Ensure LiteLLM UI credentials are correctly passed into the pod configuration using the expected environment variable names.
  • Derive Langfuse NEXTAUTH_URL from PUBLIC_BASE_URL so OAuth redirects use the public URL instead of localhost.

Enhancements:

  • Extend pod rendering to include NEXTAUTH_URL and LiteLLM UI credential placeholders for more flexible deployment configuration.

Chores:

  • Update start-stack.sh environment exports to include UI credential variables used by the pod renderer.

Summary by CodeRabbit

  • New Features

    • LiteLLM UI access can now be configured with a username and password.
    • Langfuse automatically uses the configured public URL for authentication and navigation.
  • Bug Fixes

    • Improved deployment configuration to prevent incorrect Langfuse callback URLs.
  • Tests

    • Added comprehensive checks for service health, endpoint availability, model requests, infrastructure connectivity, and public URL reachability.

boy added 4 commits July 11, 2026 22:47
Reads ports/URLs from .env (and .env.dev overlay), validates:
- Router: /v1/models, /metrics, /dashboard, /api/dashboard-stats
- LiteLLM: /health/liveness, /health/readiness, /v1/models
- Langfuse: /api/public/health
- E2E: 3 chat completions through triage router
- Canonical HTTPS URLs (graceful skip on DNS failure)

Usage:
  python scripts/verification/verify_canonical_endpoints.py         # prod
  python scripts/verification/verify_canonical_endpoints.py --dev   # dev
- Pass UI_USERNAME/UI_PASSWORD from .env to LiteLLM container as
  LITELLM_UI_USERNAME/LITELLM_UI_PASSWORD (LiteLLM's expected env vars)
- Derive NEXTAUTH_URL from PUBLIC_BASE_URL instead of hardcoding
  localhost — fixes Langfuse post-login redirect to broken URL
- LiteLLM: /llm-routing/litellm/ui/ (SERVER_ROOT_PATH prefix required)
- Langfuse: / (web UI root)
- Canonical URLs: /litellm/ui/ and /langfuse
…cation

- Router: /visualizer endpoint
- Infrastructure: MinIO /minio/health/live, ClickHouse /ping
- LiteLLM: direct chat completion (bypasses triage router)
- Canonical URLs: /visualizer
@sourcery-ai

sourcery-ai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Updates deployment configuration for LiteLLM and Langfuse auth, and adds a Python-based canonical endpoint verification suite that exercises router, LiteLLM, Langfuse, infra health, chat flows, and public URLs.

Flow diagram for canonical endpoint verification script

flowchart LR
    main[verify_canonical_endpoints.py main] --> load_env[load_env]
    load_env --> cfg[config_dict]

    cfg --> router_tests[test_router_endpoints]
    cfg --> litellm_tests[test_litellm_endpoints]
    cfg --> langfuse_tests[test_langfuse_endpoints]
    cfg --> infra_tests[test_infra_health]
    cfg --> e2e_tests[test_e2e_chat]
    cfg --> direct_tests[test_litellm_direct_chat]
    cfg --> canonical_tests[test_canonical_urls]

    router_tests --> check_fn[check]
    litellm_tests --> check_fn
    langfuse_tests --> check_fn
    infra_tests --> check_fn
    e2e_tests --> check_fn
    direct_tests --> check_fn
    canonical_tests --> check_fn

    check_fn --> summary[print results and exit]
Loading

File-Level Changes

Change Details Files
Propagate LiteLLM UI credentials and Langfuse NEXTAUTH_URL correctly from environment into the pod deployment.
  • Include UI_USERNAME and UI_PASSWORD in the environment exported by start-stack.sh before rendering pod.yaml.
  • Add new placeholders for LiteLLM UI username/password and NEXTAUTH_URL to the pod.yaml templating logic.
  • Derive NEXTAUTH_URL from PUBLIC_BASE_URL with a /langfuse suffix instead of a localhost-based URL.
  • Inject LITELLM_UI_USERNAME and LITELLM_UI_PASSWORD env vars into the Langfuse/LiteLLM container spec in pod.yaml, and make NEXTAUTH_URL use the templated value.
start-stack.sh
pod.yaml
Introduce a canonical endpoint verification script that validates router, LiteLLM, Langfuse, infra services, chat completions, and canonical HTTPS URLs for prod/dev.
  • Parse .env and optional .env.dev to construct a runtime configuration for ports, keys, and public URL.
  • Implement endpoint test helpers for router APIs, LiteLLM health and UI, Langfuse health and UI, MinIO and ClickHouse, and chat completion flows through both router and LiteLLM.
  • Add canonical URL checks against PUBLIC_BASE_URL with graceful handling of DNS/unreachable hosts.
  • Provide a main entrypoint with CLI flags for selecting dev vs prod, aggregating results, and exiting non‑zero on failures.
scripts/verification/verify_canonical_endpoints.py

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 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 8 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: ecd985a7-556e-436e-a7dd-a14b940eb724

📥 Commits

Reviewing files that changed from the base of the PR and between 664a1cd and 678a8cc.

📒 Files selected for processing (1)
  • scripts/verification/verify_canonical_endpoints.py
📝 Walkthrough

Walkthrough

Adds a standalone endpoint verification CLI and updates stack rendering to inject LiteLLM UI credentials and derive Langfuse’s NEXTAUTH_URL from PUBLIC_BASE_URL.

Changes

Service verification and stack configuration

Layer / File(s) Summary
Stack placeholder wiring
pod.yaml, start-stack.sh
LiteLLM UI credentials are rendered from environment values, and Langfuse receives a derived NEXTAUTH_URL.
Verification configuration and service checks
scripts/verification/verify_canonical_endpoints.py
Loads environment configuration and verifies router, LiteLLM, and Langfuse endpoints.
End-to-end and canonical verification
scripts/verification/verify_canonical_endpoints.py
Checks chat completions, MinIO and ClickHouse health, canonical HTTPS URLs, and aggregated CLI results.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VerificationCLI
  participant Router
  participant LiteLLM
  participant Langfuse
  VerificationCLI->>Router: Verify REST and chat endpoints
  Router->>LiteLLM: Route chat completion
  LiteLLM-->>Router: Return completion response
  VerificationCLI->>LiteLLM: Verify health, models, UI, and direct chat
  VerificationCLI->>Langfuse: Verify health and web UI
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: routing fixes, LiteLLM UI auth, Langfuse redirect handling, and canonical endpoint tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/canonical-endpoint-verification

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 4 issues, and left some high level feedback:

  • The verify_canonical_endpoints.py script hardcodes MinIO and ClickHouse ports instead of reading them from the same env config, which makes it harder to reuse in environments with nonstandard port mappings.
  • The .env parsing in load_env assumes simple KEY=VALUE lines and strips quotes naively; if any values contain = or require preserved quotes this may misparse, so consider using a more robust dotenv parser or handling these edge cases.
  • In load_env, defaulting base_url to a specific hostname (x570.vendeuvre.lan) can produce misleading results in other setups; it would be safer to require an explicit BASE_URL or fail fast when it's missing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `verify_canonical_endpoints.py` script hardcodes MinIO and ClickHouse ports instead of reading them from the same env config, which makes it harder to reuse in environments with nonstandard port mappings.
- The `.env` parsing in `load_env` assumes simple `KEY=VALUE` lines and strips quotes naively; if any values contain `=` or require preserved quotes this may misparse, so consider using a more robust dotenv parser or handling these edge cases.
- In `load_env`, defaulting `base_url` to a specific hostname (`x570.vendeuvre.lan`) can produce misleading results in other setups; it would be safer to require an explicit BASE_URL or fail fast when it's missing.

## Individual Comments

### Comment 1
<location path="start-stack.sh" line_range="615-618" />
<code_context>
+    export WORKDIR HOME LITELLM_MASTER_KEY UI_USERNAME UI_PASSWORD POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY CLASSIFIER_INPUT_MAX_CHARS REDIS_AUTH CLICKHOUSE_PASSWORD PUBLIC_BASE_URL ROUTING_DOMAIN POD_NAME DATA_ROOT ROUTER_IMAGE ROUTER_PORT LITELLM_PORT LANGFUSE_WEB_PORT LANGFUSE_WORKER_PORT POSTGRES_PORT VALKEY_CACHE_PORT VALKEY_LF_PORT CLICKHOUSE_HTTP_PORT CLICKHOUSE_TCP_PORT CLICKHOUSE_INTERSERVER_PORT MINIO_S3_PORT MINIO_CONSOLE_PORT
</code_context>
<issue_to_address>
**🚨 issue (security):** Defaulting UI credentials to 'admin'/'admin' is risky and can hide misconfiguration.

Using `os.environ.get("UI_USERNAME", "admin")` and `os.environ.get("UI_PASSWORD", "admin")` silently creates an `admin/admin` account whenever the env vars are missing. This encourages accidental deployments with trivial credentials and masks misconfiguration because the service still "works." Instead, fail if these env vars are unset (e.g., raise on missing keys) or, at minimum, use strong randomized defaults and emit a clear log warning so operators know the configuration is insecure.
</issue_to_address>

### Comment 2
<location path="scripts/verification/verify_canonical_endpoints.py" line_range="365-367" />
<code_context>
+            r = httpx.get(url, timeout=15, follow_redirects=True)
+            ok = r.status_code == 200
+            passed += check(f"GET {url}", ok, f"HTTP {r.status_code}")
+        except httpx.ConnectError as e:
+            # DNS/unreachable — skip gracefully (host may not resolve from test machine)
+            passed += check(f"GET {url}", True, f"SKIP: DNS/unreachable ({e})")
+        except Exception as e:
+            passed += check(f"GET {url}", False, str(e)[:100])
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Treating DNS/unreachable endpoints as "passed" can mask real connectivity issues.

On `httpx.ConnectError`, the code currently increments `passed` and reports the check as OK with a skip message. This can cause the overall summary to appear fully successful even when canonical URLs are not reachable. Consider marking these as failures or not counting them toward `passed` (e.g., track a separate `skipped` count) so the final status clearly reflects that some URLs could not be validated.

Suggested implementation:

```python
        except httpx.ConnectError as e:
            # DNS/unreachable — treat as failed so overall summary reflects unreachable canonical URLs
            passed += check(f"GET {url}", False, f"SKIP: DNS/unreachable ({e})")

```

If the `check()` helper currently assumes that "SKIP" results should be counted differently (e.g., has explicit handling for skip vs fail), you may want to:
1. Adjust `check()` so that it can distinguish skipped checks from hard failures if that’s important for reporting.
2. Optionally introduce a separate `skipped` counter that is incremented when `httpx.ConnectError` occurs and include it in the final output, while still not incrementing `passed`.
</issue_to_address>

### Comment 3
<location path="scripts/verification/verify_canonical_endpoints.py" line_range="104-113" />
<code_context>
+            )
+            elapsed = time.time() - start
+            if r.status_code == 200:
+                data = r.json()
+                content = (data["choices"][0]["message"].get("content") or "").strip()
+                reasoning = (data["choices"][0]["message"].get("reasoning_content") or "").strip()
+                model_used = data.get("model", "?")
+                ok = len(content) > 0 or len(reasoning) > 0
</code_context>
<issue_to_address>
**suggestion:** Chat completion response parsing assumes a specific shape that may not hold under all error/edge cases.

In `test_e2e_chat` (and `test_litellm_direct_chat`), the test assumes `data["choices"][0]["message"]` always exists. Some providers or future API changes may omit `choices` or `message` (e.g., partial failures, streaming responses), causing `KeyError`/`IndexError` instead of a clear test failure. Please make this more defensive by validating the structure (or using `.get` and explicit checks) and failing the test with a clear error message if the expected fields are missing.
</issue_to_address>

### Comment 4
<location path="scripts/verification/verify_canonical_endpoints.py" line_range="43-52" />
<code_context>
+        _parse(WORKDIR / ".env.dev")
+
+    # Resolve with defaults
+    return {
+        "router_port": env.get("ROUTER_PORT", "5000"),
+        "litellm_port": env.get("LITELLM_PORT", "4000"),
+        "langfuse_web_port": env.get("LANGFUSE_WEB_PORT", "3001"),
+        "litellm_master_key": env.get("LITELLM_MASTER_KEY", "gateway-pass"),
+        "router_api_key": env.get("ROUTER_API_KEY", "gateway-pass"),
+        "public_base_url": env.get("PUBLIC_BASE_URL", ""),
+        "base_url": env.get("BASE_URL", env.get("BASEURL", "x570.vendeuvre.lan")),
+    }
+
</code_context>
<issue_to_address>
**suggestion:** The `base_url` config is resolved but never used, which may indicate an incomplete or dead configuration path.

`load_env` returns `base_url` (from `BASE_URL`/`BASEURL` with a default host), but this value is never used. Either connect `base_url` to `test_canonical_urls` (if you intend to test externally reachable URLs beyond `PUBLIC_BASE_URL`) or remove it from the config to avoid ambiguity about which hostname is actually under test.

```suggestion
    # Resolve with defaults
    return {
        "router_port": env.get("ROUTER_PORT", "5000"),
        "litellm_port": env.get("LITELLM_PORT", "4000"),
        "langfuse_web_port": env.get("LANGFUSE_WEB_PORT", "3001"),
        "litellm_master_key": env.get("LITELLM_MASTER_KEY", "gateway-pass"),
        "router_api_key": env.get("ROUTER_API_KEY", "gateway-pass"),
        "public_base_url": env.get("PUBLIC_BASE_URL", ""),
    }
```
</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 start-stack.sh
Comment on lines +615 to 618
export WORKDIR HOME LITELLM_MASTER_KEY UI_USERNAME UI_PASSWORD POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD OLLAMA_API_KEY LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY CLASSIFIER_INPUT_MAX_CHARS REDIS_AUTH CLICKHOUSE_PASSWORD PUBLIC_BASE_URL ROUTING_DOMAIN POD_NAME DATA_ROOT ROUTER_IMAGE ROUTER_PORT LITELLM_PORT LANGFUSE_WEB_PORT LANGFUSE_WORKER_PORT POSTGRES_PORT VALKEY_CACHE_PORT VALKEY_LF_PORT CLICKHOUSE_HTTP_PORT CLICKHOUSE_TCP_PORT CLICKHOUSE_INTERSERVER_PORT MINIO_S3_PORT MINIO_CONSOLE_PORT
python3 - "$WORKDIR/pod.yaml" <<'PY'
import os, sys, urllib.parse, json
uid = os.getuid()

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.

🚨 issue (security): Defaulting UI credentials to 'admin'/'admin' is risky and can hide misconfiguration.

Using os.environ.get("UI_USERNAME", "admin") and os.environ.get("UI_PASSWORD", "admin") silently creates an admin/admin account whenever the env vars are missing. This encourages accidental deployments with trivial credentials and masks misconfiguration because the service still "works." Instead, fail if these env vars are unset (e.g., raise on missing keys) or, at minimum, use strong randomized defaults and emit a clear log warning so operators know the configuration is insecure.

Comment on lines +365 to +367
except httpx.ConnectError as e:
# DNS/unreachable — skip gracefully (host may not resolve from test machine)
passed += check(f"GET {url}", True, f"SKIP: DNS/unreachable ({e})")

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.

suggestion (bug_risk): Treating DNS/unreachable endpoints as "passed" can mask real connectivity issues.

On httpx.ConnectError, the code currently increments passed and reports the check as OK with a skip message. This can cause the overall summary to appear fully successful even when canonical URLs are not reachable. Consider marking these as failures or not counting them toward passed (e.g., track a separate skipped count) so the final status clearly reflects that some URLs could not be validated.

Suggested implementation:

        except httpx.ConnectError as e:
            # DNS/unreachable — treat as failed so overall summary reflects unreachable canonical URLs
            passed += check(f"GET {url}", False, f"SKIP: DNS/unreachable ({e})")

If the check() helper currently assumes that "SKIP" results should be counted differently (e.g., has explicit handling for skip vs fail), you may want to:

  1. Adjust check() so that it can distinguish skipped checks from hard failures if that’s important for reporting.
  2. Optionally introduce a separate skipped counter that is incremented when httpx.ConnectError occurs and include it in the final output, while still not incrementing passed.

Comment on lines +104 to +113
data = r.json()
ok = r.status_code == 200 and isinstance(data, dict)
passed += check("/api/dashboard-stats", ok)
except Exception as e:
passed += check("/api/dashboard-stats", False, str(e))

# /visualizer
total += 1
try:
r = httpx.get(f"{base}/visualizer", timeout=10)

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.

suggestion: Chat completion response parsing assumes a specific shape that may not hold under all error/edge cases.

In test_e2e_chat (and test_litellm_direct_chat), the test assumes data["choices"][0]["message"] always exists. Some providers or future API changes may omit choices or message (e.g., partial failures, streaming responses), causing KeyError/IndexError instead of a clear test failure. Please make this more defensive by validating the structure (or using .get and explicit checks) and failing the test with a clear error message if the expected fields are missing.

Comment on lines +43 to +52
# Resolve with defaults
return {
"router_port": env.get("ROUTER_PORT", "5000"),
"litellm_port": env.get("LITELLM_PORT", "4000"),
"langfuse_web_port": env.get("LANGFUSE_WEB_PORT", "3001"),
"litellm_master_key": env.get("LITELLM_MASTER_KEY", "gateway-pass"),
"router_api_key": env.get("ROUTER_API_KEY", "gateway-pass"),
"public_base_url": env.get("PUBLIC_BASE_URL", ""),
"base_url": env.get("BASE_URL", env.get("BASEURL", "x570.vendeuvre.lan")),
}

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.

suggestion: The base_url config is resolved but never used, which may indicate an incomplete or dead configuration path.

load_env returns base_url (from BASE_URL/BASEURL with a default host), but this value is never used. Either connect base_url to test_canonical_urls (if you intend to test externally reachable URLs beyond PUBLIC_BASE_URL) or remove it from the config to avoid ambiguity about which hostname is actually under test.

Suggested change
# Resolve with defaults
return {
"router_port": env.get("ROUTER_PORT", "5000"),
"litellm_port": env.get("LITELLM_PORT", "4000"),
"langfuse_web_port": env.get("LANGFUSE_WEB_PORT", "3001"),
"litellm_master_key": env.get("LITELLM_MASTER_KEY", "gateway-pass"),
"router_api_key": env.get("ROUTER_API_KEY", "gateway-pass"),
"public_base_url": env.get("PUBLIC_BASE_URL", ""),
"base_url": env.get("BASE_URL", env.get("BASEURL", "x570.vendeuvre.lan")),
}
# Resolve with defaults
return {
"router_port": env.get("ROUTER_PORT", "5000"),
"litellm_port": env.get("LITELLM_PORT", "4000"),
"langfuse_web_port": env.get("LANGFUSE_WEB_PORT", "3001"),
"litellm_master_key": env.get("LITELLM_MASTER_KEY", "gateway-pass"),
"router_api_key": env.get("ROUTER_API_KEY", "gateway-pass"),
"public_base_url": env.get("PUBLIC_BASE_URL", ""),
}

@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 a canonical endpoint verification script (verify_canonical_endpoints.py) to validate API endpoints, infrastructure health, and E2E chat completions. It also updates pod.yaml and start-stack.sh to support configuring LiteLLM UI credentials and dynamically deriving the NEXTAUTH_URL from PUBLIC_BASE_URL. Feedback on these changes includes adding defensive checks when parsing API responses to prevent potential indexing errors, specifying file encoding when opening files, stripping trailing slashes from the base URL to avoid malformed paths, and defaulting the LiteLLM UI password to the master key instead of 'admin' for better security and consistency.

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 on lines +248 to +249
content = (data["choices"][0]["message"].get("content") or "").strip()
reasoning = (data["choices"][0]["message"].get("reasoning_content") or "").strip()

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

Directly accessing data["choices"][0] can raise an IndexError or KeyError if the API returns an empty choices list or an error response with a 200 status code. It is safer to use defensive checks to handle these cases gracefully.

Suggested change
content = (data["choices"][0]["message"].get("content") or "").strip()
reasoning = (data["choices"][0]["message"].get("reasoning_content") or "").strip()
choices = data.get("choices", [])
message = choices[0].get("message", {}) if isinstance(choices, list) and choices and isinstance(choices[0], dict) else {}
content = (message.get("content") or "").strip()
reasoning = (message.get("reasoning_content") or "").strip()

Comment on lines +323 to +324
content = (data["choices"][0]["message"].get("content") or "").strip()
reasoning = (data["choices"][0]["message"].get("reasoning_content") or "").strip()

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

Directly accessing data["choices"][0] can raise an IndexError or KeyError if the API returns an empty choices list or an error response. It is safer to use defensive checks to handle these cases gracefully.

Suggested change
content = (data["choices"][0]["message"].get("content") or "").strip()
reasoning = (data["choices"][0]["message"].get("reasoning_content") or "").strip()
choices = data.get("choices", [])
message = choices[0].get("message", {}) if isinstance(choices, list) and choices and isinstance(choices[0], dict) else {}
content = (message.get("content") or "").strip()
reasoning = (message.get("reasoning_content") or "").strip()

Comment thread start-stack.sh
text = text.replace("RUN_USER_PLACEHOLDER", f"/run/user/{uid}")
text = text.replace("LITELLM_MASTER_KEY_PLACEHOLDER", yaml_scalar(os.environ["LITELLM_MASTER_KEY"]))
text = text.replace("LITELLM_UI_USERNAME_PLACEHOLDER", yaml_scalar(os.environ.get("UI_USERNAME", "admin")))
text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ.get("UI_PASSWORD", "admin")))

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

Defaulting LITELLM_UI_PASSWORD to "admin" when UI_PASSWORD is not set is insecure and conflicts with the success messages at the end of this script (lines 770 and 802), which print $LITELLM_MASTER_KEY as the password. To maintain security and consistency, default the UI password to LITELLM_MASTER_KEY if UI_PASSWORD is not provided. Additionally, consider updating the success messages at lines 770 and 802 to print ${UI_PASSWORD:-$LITELLM_MASTER_KEY} so that the correct password is shown when UI_PASSWORD is customized.

Suggested change
text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ.get("UI_PASSWORD", "admin")))
text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ.get("UI_PASSWORD", os.environ["LITELLM_MASTER_KEY"])))

def _parse(path: Path):
if not path.exists():
return
with open(path) as f:

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

It is recommended to specify the file encoding (e.g., encoding="utf-8") when opening files to prevent potential UnicodeDecodeError on systems where the default system encoding is not UTF-8.

Suggested change
with open(path) as f:
with open(path, encoding="utf-8") as f:

def test_canonical_urls(cfg: dict) -> tuple[int, int]:
"""Verify canonical HTTPS URLs are reachable (if PUBLIC_BASE_URL is set)."""
passed = total = 0
public = cfg["public_base_url"]

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

If PUBLIC_BASE_URL is configured with a trailing slash in the .env file, constructing the URLs by simple concatenation (e.g., f"{public}{path}") will result in double slashes (e.g., .../llm-routing//v1/models). It is safer to strip any trailing slashes from the base URL.

Suggested change
public = cfg["public_base_url"]
public = cfg["public_base_url"].rstrip("/")

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
scripts/verification/verify_canonical_endpoints.py (1)

44-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

base_url config key is loaded but never used.

The base_url entry in the config dict (line 51) is not referenced by any test function. This is dead configuration that may confuse future maintainers.

Either remove it or add a test that uses it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verification/verify_canonical_endpoints.py` around lines 44 - 52, The
base_url entry returned by the configuration-loading function is unused. Remove
the base_url configuration key and its nested environment lookup, unless a
verification test is added that explicitly consumes it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/verification/verify_canonical_endpoints.py`:
- Around line 340-371: Update test_canonical_urls so httpx.ConnectError cases
are not counted as passed: track skipped endpoints separately and expose that
count to the final verification summary, or at minimum emit an explicit warning
that canonical URL coverage is incomplete. Preserve the existing graceful
handling and SKIP message while ensuring unreachable or misconfigured public
endpoints cannot silently appear as successful tests.
- Around line 274-296: Update load_env to include MINIO_S3_PORT and
CLICKHOUSE_HTTP_PORT in the returned config dictionary, then update
test_infra_health to build the MinIO and ClickHouse health URLs from those
configured values instead of hardcoded ports. Preserve the existing health
checks and failure handling.

In `@start-stack.sh`:
- Around line 672-673: Update the credential handling in the LiteLLM UI
placeholder replacement to require UI_USERNAME and UI_PASSWORD explicitly
instead of falling back to "admin". Fail fast with a clear error when either
environment variable is unset, while preserving yaml_scalar processing for valid
credentials.

---

Nitpick comments:
In `@scripts/verification/verify_canonical_endpoints.py`:
- Around line 44-52: The base_url entry returned by the configuration-loading
function is unused. Remove the base_url configuration key and its nested
environment lookup, unless a verification test is added that explicitly consumes
it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 873b9600-4065-458f-86ed-1f22b64e711b

📥 Commits

Reviewing files that changed from the base of the PR and between 6d064bb and 664a1cd.

📒 Files selected for processing (3)
  • pod.yaml
  • scripts/verification/verify_canonical_endpoints.py
  • start-stack.sh

Comment on lines +274 to +296
def test_infra_health(cfg: dict) -> tuple[int, int]:
"""Test infrastructure services: MinIO, ClickHouse. Returns (passed, total)."""
passed = total = 0

print(f"\n── Infrastructure health ──")

# MinIO S3 health
total += 1
try:
r = httpx.get("http://127.0.0.1:9002/minio/health/live", timeout=10)
passed += check("MinIO /minio/health/live", r.status_code == 200)
except Exception as e:
passed += check("MinIO /minio/health/live", False, str(e))

# ClickHouse HTTP ping
total += 1
try:
r = httpx.get("http://127.0.0.1:8123/ping", timeout=10)
passed += check("ClickHouse /ping", r.status_code == 200 and r.text.strip() == "Ok.")
except Exception as e:
passed += check("ClickHouse /ping", False, str(e))

return passed, total

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Hardcoded MinIO and ClickHouse ports should be read from config.

test_infra_health hardcodes MinIO port 9002 and ClickHouse port 8123. If the .env file specifies different MINIO_S3_PORT or CLICKHOUSE_HTTP_PORT values, the verification will target the wrong ports and report false failures (or false successes if something else happens to listen there).

The load_env function should include these ports in the returned config dict.

🔧 Proposed fix: read infra ports from .env
 def load_env(dev: bool = False) -> dict:
     """Load .env (and optionally .env.dev overlay), return resolved config dict."""
     env = {}
 
     def _parse(path: Path):
         if not path.exists():
             return
         with open(path) as f:
             for line in f:
                 line = line.strip()
                 if not line or line.startswith("#") or "=" not in line:
                     continue
                 key, _, val = line.partition("=")
                 key = key.strip()
                 val = val.strip().strip('"').strip("'")
                 env[key] = val
 
     _parse(WORKDIR / ".env")
     if dev:
         _parse(WORKDIR / ".env.dev")
 
     # Resolve with defaults
     return {
         "router_port": env.get("ROUTER_PORT", "5000"),
         "litellm_port": env.get("LITELLM_PORT", "4000"),
         "langfuse_web_port": env.get("LANGFUSE_WEB_PORT", "3001"),
         "litellm_master_key": env.get("LITELLM_MASTER_KEY", "gateway-pass"),
         "router_api_key": env.get("ROUTER_API_KEY", "gateway-pass"),
         "public_base_url": env.get("PUBLIC_BASE_URL", ""),
         "base_url": env.get("BASE_URL", env.get("BASEURL", "x570.vendeuvre.lan")),
+        "minio_s3_port": env.get("MINIO_S3_PORT", "9002"),
+        "clickhouse_http_port": env.get("CLICKHOUSE_HTTP_PORT", "8123"),
     }

Then update test_infra_health:

 def test_infra_health(cfg: dict) -> tuple[int, int]:
     """Test infrastructure services: MinIO, ClickHouse. Returns (passed, total)."""
     passed = total = 0
 
-    print(f"\n── Infrastructure health ──")
+    print("\n── Infrastructure health ──")
 
     # MinIO S3 health
     total += 1
     try:
-        r = httpx.get("http://127.0.0.1:9002/minio/health/live", timeout=10)
+        r = httpx.get(f"http://127.0.0.1:{cfg['minio_s3_port']}/minio/health/live", timeout=10)
         passed += check("MinIO /minio/health/live", r.status_code == 200)
     except Exception as e:
         passed += check("MinIO /minio/health/live", False, str(e))
 
     # ClickHouse HTTP ping
     total += 1
     try:
-        r = httpx.get("http://127.0.0.1:8123/ping", timeout=10)
+        r = httpx.get(f"http://127.0.0.1:{cfg['clickhouse_http_port']}/ping", timeout=10)
         passed += check("ClickHouse /ping", r.status_code == 200 and r.text.strip() == "Ok.")
     except Exception as e:
         passed += check("ClickHouse /ping", False, str(e))
🧰 Tools
🪛 Ruff (0.15.20)

[error] 278-278: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 285-285: Do not catch blind exception: Exception

(BLE001)


[warning] 293-293: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verification/verify_canonical_endpoints.py` around lines 274 - 296,
Update load_env to include MINIO_S3_PORT and CLICKHOUSE_HTTP_PORT in the
returned config dictionary, then update test_infra_health to build the MinIO and
ClickHouse health URLs from those configured values instead of hardcoded ports.
Preserve the existing health checks and failure handling.

Comment on lines +340 to +371
def test_canonical_urls(cfg: dict) -> tuple[int, int]:
"""Verify canonical HTTPS URLs are reachable (if PUBLIC_BASE_URL is set)."""
passed = total = 0
public = cfg["public_base_url"]
if not public:
return 0, 0

print(f"\n── Canonical URLs ({public}) ──")

endpoints = [
("/v1/models", "router models"),
("/dashboard", "dashboard"),
("/metrics", "metrics"),
("/visualizer", "visualizer"),
("/litellm/ui/", "LiteLLM admin UI"),
("/langfuse", "Langfuse web UI"),
]

for path, label in endpoints:
total += 1
url = f"{public}{path}"
try:
r = httpx.get(url, timeout=15, follow_redirects=True)
ok = r.status_code == 200
passed += check(f"GET {url}", ok, f"HTTP {r.status_code}")
except httpx.ConnectError as e:
# DNS/unreachable — skip gracefully (host may not resolve from test machine)
passed += check(f"GET {url}", True, f"SKIP: DNS/unreachable ({e})")
except Exception as e:
passed += check(f"GET {url}", False, str(e)[:100])

return passed, total

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

ConnectError marks skipped canonical URL tests as passed.

When httpx.ConnectError occurs (line 365-367), the test is counted as passed with True. While the comment explains this is intentional for DNS-unreachable hosts, it means canonical URL failures are silently masked. If the public base URL is misconfigured or the reverse proxy is down, all canonical URL tests will "pass" as SKIP.

Consider counting skipped tests separately so the final summary distinguishes passed/skipped/failed, or at minimum log a warning that results may be incomplete.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 361-361: Request-controlled URL passed to httpx; validate against an allowlist to prevent SSRF.
Context: httpx.get(url, timeout=15, follow_redirects=True)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(avoid-ssrf)

🪛 Ruff (0.15.20)

[warning] 358-358: Loop control variable label not used within loop body

Rename unused label to _label

(B007)


[warning] 368-368: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verification/verify_canonical_endpoints.py` around lines 340 - 371,
Update test_canonical_urls so httpx.ConnectError cases are not counted as
passed: track skipped endpoints separately and expose that count to the final
verification summary, or at minimum emit an explicit warning that canonical URL
coverage is incomplete. Preserve the existing graceful handling and SKIP message
while ensuring unreachable or misconfigured public endpoints cannot silently
appear as successful tests.

Comment thread start-stack.sh
Comment on lines +672 to +673
text = text.replace("LITELLM_UI_USERNAME_PLACEHOLDER", yaml_scalar(os.environ.get("UI_USERNAME", "admin")))
text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ.get("UI_PASSWORD", "admin")))

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Default admin/admin credentials for LiteLLM UI are insecure.

If UI_USERNAME or UI_PASSWORD are not set in the environment, the LiteLLM admin UI will be accessible with admin/admin. This is a weak default that could be exploited, especially in production deployments.

Consider requiring these variables to be set explicitly (fail fast if missing) rather than defaulting to a known-weak credential, or at minimum generate a random password when unset.

🔒️ Proposed fix: fail if credentials are unset
-text = text.replace("LITELLM_UI_USERNAME_PLACEHOLDER", yaml_scalar(os.environ.get("UI_USERNAME", "admin")))
-text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ.get("UI_PASSWORD", "admin")))
+ui_username = os.environ.get("UI_USERNAME")
+ui_password = os.environ.get("UI_PASSWORD")
+if not ui_username or not ui_password:
+    sys.stderr.write("Error: UI_USERNAME and UI_PASSWORD must be set in the environment.\n")
+    sys.exit(1)
+text = text.replace("LITELLM_UI_USERNAME_PLACEHOLDER", yaml_scalar(ui_username))
+text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(ui_password))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text = text.replace("LITELLM_UI_USERNAME_PLACEHOLDER", yaml_scalar(os.environ.get("UI_USERNAME", "admin")))
text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(os.environ.get("UI_PASSWORD", "admin")))
ui_username = os.environ.get("UI_USERNAME")
ui_password = os.environ.get("UI_PASSWORD")
if not ui_username or not ui_password:
sys.stderr.write("Error: UI_USERNAME and UI_PASSWORD must be set in the environment.\n")
sys.exit(1)
text = text.replace("LITELLM_UI_USERNAME_PLACEHOLDER", yaml_scalar(ui_username))
text = text.replace("LITELLM_UI_PASSWORD_PLACEHOLDER", yaml_scalar(ui_password))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@start-stack.sh` around lines 672 - 673, Update the credential handling in the
LiteLLM UI placeholder replacement to require UI_USERNAME and UI_PASSWORD
explicitly instead of falling back to "admin". Fail fast with a clear error when
either environment variable is unset, while preserving yaml_scalar processing
for valid credentials.

- POST agent-simple-core through public URL (dev.vendeuvre.lan)
- Validates full HTTPS path: HAProxy → dev router → LiteLLM → model
- Graceful DNS skip when host unreachable
@sheepdestroyer

Copy link
Copy Markdown
Owner Author

Closing to open a fresh PR with all latest fixes.

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.

1 participant