Skip to content

fix: HAProxy routing, env vars, + canonical endpoint verification#258

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

fix: HAProxy routing, env vars, + canonical endpoint verification#258
sheepdestroyer wants to merge 9 commits into
masterfrom
test/canonical-endpoint-verification

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes HAProxy routing, container env vars, and adds comprehensive endpoint verification.

HAProxy fixes (5 issues)

1. Dev subpath routing hit prod backends
Prod subpath rules matched before the host_dev rule, so dev.vendeuvre.lan/llm-routing/litellm/ui was served by prod LiteLLM (port 4000) instead of dev (4010). Fixed by moving dev rules to position 0 — they now match first.

2. Missing dev-specific backends
Added dev-litellm-backend (port 4010), dev-langfuse-backend (port 3011), and dev-llm-routing-backend (port 5010) with the same path-rewrite logic as their prod counterparts.

3. Langfuse empty-path 400 on HTTP/1.1
The regsub(^/llm-routing/langfuse,,) produced an empty string when the path was exactly /llm-routing/langfuse. HAProxy then sent GET HTTP/1.1 (empty path) which is invalid HTTP → 400. Fixed by replacing with /: regsub(^/llm-routing/langfuse,/,).

4. Langfuse /_next assets 404
Moved root-relative SPA asset rules before the host_dashboard catch-all so Langfuse static assets route correctly.

5. 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

6. 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.

7. Langfuse post-login redirect to localhost
NEXTAUTH_URL was hardcoded to http://localhost:LANGFUSE_WEB_PORT — now derived from PUBLIC_BASE_URL.

8. Prod .env missing PUBLIC_BASE_URL
Added PUBLIC_BASE_URL="https://x570.vendeuvre.lan/llm-routing" to prod .env so the verification script can test canonical HTTPS URLs.

Verification script (scripts/verification/verify_canonical_endpoints.py)

Comprehensive endpoint test suite:

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 (router) 3 completions through triage router
LiteLLM direct 1 completion directly to LiteLLM
Canonical URLs 6 GET + 1 POST through public HTTPS (graceful DNS skip)

Review fixes applied

  • Gemini Code Assist: Defensive .get() access on choices/message in all three chat completion parsers
  • Sourcery: Skipped canonical URL checks are now tracked separately and shown in summary as "X skipped"
  • Docs: Added canonical endpoint verification section to README

Test results

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

Summary by Sourcery

Add canonical endpoint verification across environments and wire required configuration for LiteLLM UI and Langfuse authentication.

New Features:

  • Introduce a canonical endpoint verification script that validates router, LiteLLM, Langfuse, infrastructure, and public HTTPS endpoints for prod and dev.

Enhancements:

  • Document canonical endpoint verification usage and coverage in the README.
  • Derive Langfuse NEXTAUTH_URL from PUBLIC_BASE_URL to align OAuth redirects with the public base URL.
  • Inject LiteLLM UI credentials into the pod configuration via new environment variables for the admin UI.

Documentation:

  • Add README section describing canonical endpoint verification commands, covered endpoints, and PUBLIC_BASE_URL requirements.

Tests:

  • Add an automated verification script that exercises core API endpoints, health checks, and chat completion flows, including canonical HTTPS URLs.

boy added 7 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
- 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
- All three chat completion parsers now use .get() on choices/message
  instead of direct indexing (Gemini Code Assist review)
- Canonical URL skipped checks are tracked separately and shown
  in summary as 'X skipped' (Sourcery review)
@sourcery-ai

sourcery-ai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Fixes HAProxy routing precedence and backend definitions, corrects LiteLLM/Langfuse environment wiring (UI credentials and NEXTAUTH_URL), and adds a new canonical endpoint verification script plus README documentation.

Sequence diagram for canonical URL verification requests

sequenceDiagram
    participant Main as main
    participant Canonical as test_canonical_urls
    participant HTTPX as httpx
    participant Router as Router
    participant LiteLLM as LiteLLM
    participant Langfuse as Langfuse

    Main->>Canonical: test_canonical_urls(cfg)
    Canonical->>HTTPX: get(public_base_url + "/v1/models")
    HTTPX-->>Router: HTTPS GET /v1/models
    Router-->>HTTPX: 200 OK
    HTTPX-->>Canonical: Response

    Canonical->>HTTPX: get(public_base_url + "/litellm/ui/")
    HTTPX-->>LiteLLM: HTTPS GET /litellm/ui/
    LiteLLM-->>HTTPX: 200 OK
    HTTPX-->>Canonical: Response

    Canonical->>HTTPX: get(public_base_url + "/langfuse")
    HTTPX-->>Langfuse: HTTPS GET /langfuse
    Langfuse-->>HTTPX: 200 OK
    HTTPX-->>Canonical: Response

    Canonical->>HTTPX: post(public_base_url + "/v1/chat/completions")
    HTTPX-->>Router: HTTPS POST /v1/chat/completions
    Router-->>HTTPX: 200 OK
    HTTPX-->>Canonical: Response
    Canonical-->>Main: (passed, total, skipped)
Loading

File-Level Changes

Change Details Files
Render correct LiteLLM UI credentials and Langfuse NEXTAUTH_URL into the pod manifest from start-stack.sh.
  • Extend render_pod_yaml environment exports to include UI_USERNAME and UI_PASSWORD.
  • Add LITELLM_UI_USERNAME_PLACEHOLDER and LITELLM_UI_PASSWORD_PLACEHOLDER to pod.yaml and wire them to new env vars.
  • Derive NEXTAUTH_URL from PUBLIC_BASE_URL/langfuse in render_pod_yaml and replace a new NEXTAUTH_URL_PLACEHOLDER.
  • Update pod.yaml NEXTAUTH_URL to use NEXTAUTH_URL_PLACEHOLDER instead of a localhost URL tied to LANGFUSE_WEB_PORT.
start-stack.sh
pod.yaml
Introduce a canonical endpoint verification script that exercises router, LiteLLM, Langfuse, infra health, E2E chat, and public HTTPS endpoints for prod and dev.
  • Load configuration from .env and optional .env.dev overlay with sensible defaults.
  • Add HTTP checks for router API endpoints including models, metrics, dashboard, stats, and visualizer.
  • Add LiteLLM health and admin UI checks plus direct chat completion tests using master key auth.
  • Add Langfuse health and web UI checks, MinIO and ClickHouse health checks, and E2E chat completions through the triage router.
  • Implement canonical HTTPS URL GET/POST checks based on PUBLIC_BASE_URL with DNS failure treated as skipped and included in summary.
  • Harden chat completion result parsing with defensive access to choices and message content/reasoning fields.
  • Aggregate and print a final summary including passed, total, and skipped test counts, exiting non‑zero on failure.
scripts/verification/verify_canonical_endpoints.py
Document canonical endpoint verification usage and PUBLIC_BASE_URL requirements in the README.
  • Add a "Canonical Endpoint Verification" section with prod/dev invocation examples of the new script.
  • Describe the endpoint categories covered by the verification suite in a markdown table.
  • Clarify the need for PUBLIC_BASE_URL in .env for canonical URL tests and specify the expected prod value.
README.md

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

Warning

Review limit reached

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

Next review available in: 48 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: 257e31e1-0e8a-4653-9ee4-75e6874c316f

📥 Commits

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

📒 Files selected for processing (7)
  • README.md
  • pod.yaml
  • scripts/benchmark_classifier.py
  • scripts/reclassify_all.py
  • scripts/retry_errors.py
  • scripts/verification/verify_canonical_endpoints.py
  • start-stack.sh
✨ 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 2 issues, and left some high level feedback:

  • In verify_canonical_endpoints.py, the repeated chat-completion parsing logic (choices/message/content/reasoning extraction) across test_e2e_chat, test_litellm_direct_chat, and canonical POST could be refactored into a small helper to reduce duplication and keep the tests easier to maintain.
  • The .env parsing in load_env silently ignores missing files and malformed lines; consider logging or printing a brief summary of which env files were loaded and which key defaults were used so misconfigurations are easier to spot when verification fails.
  • The HTTP error handling in the verification script uses broad except Exception blocks; tightening these to httpx.RequestError/HTTPStatusError where applicable would make failures more explicit and avoid accidentally masking programming errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `verify_canonical_endpoints.py`, the repeated chat-completion parsing logic (choices/message/content/reasoning extraction) across `test_e2e_chat`, `test_litellm_direct_chat`, and canonical POST could be refactored into a small helper to reduce duplication and keep the tests easier to maintain.
- The `.env` parsing in `load_env` silently ignores missing files and malformed lines; consider logging or printing a brief summary of which env files were loaded and which key defaults were used so misconfigurations are easier to spot when verification fails.
- The HTTP error handling in the verification script uses broad `except Exception` blocks; tightening these to `httpx.RequestError`/`HTTPStatusError` where applicable would make failures more explicit and avoid accidentally masking programming errors.

## 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):** UI credentials default to 'admin'/'admin', which is risky if not overridden.

Using "admin" as the default for both UI_USERNAME and UI_PASSWORD makes the UI trivially guessable wherever these vars aren’t set, which is a security risk in non-local deployments. Please either fail if they’re unset, generate a secure random default, or restrict weak defaults to explicit dev-only configurations so production can’t ship with these credentials.
</issue_to_address>

### Comment 2
<location path="scripts/verification/verify_canonical_endpoints.py" line_range="282-291" />
<code_context>
+
+    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:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Infra health checks use hardcoded ports instead of the loaded env configuration.

MinIO (`9002`) and ClickHouse (`8123`) ports are hardcoded here, so changing them via `.env` or stack config will cause these health checks to fail even when services are healthy. Please read the ports from the same env-based configuration used elsewhere, so the health checks track the actual deployed settings.

Suggested implementation:

```python
    # MinIO S3 health
    total += 1
    try:
        # Prefer fully configured HTTP URL; fall back to generic endpoint if present
        minio_base = cfg.get("MINIO_HTTP_URL") or cfg.get("MINIO_ENDPOINT")
        if not minio_base:
            raise RuntimeError("MINIO_HTTP_URL/MINIO_ENDPOINT not configured in cfg")
        r = httpx.get(f"{minio_base.rstrip('/')}/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))

```

```python
    # ClickHouse HTTP ping
    total += 1
    try:
        # Prefer HTTP URL; fall back to generic ClickHouse URL if present
        ch_base = cfg.get("CLICKHOUSE_HTTP_URL") or cfg.get("CLICKHOUSE_URL")
        if not ch_base:
            raise RuntimeError("CLICKHOUSE_HTTP_URL/CLICKHOUSE_URL not configured in cfg")
        r = httpx.get(f"{ch_base.rstrip('/')}/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))

```

To fully align this with the rest of the codebase:
1. Ensure that `cfg` is populated from your existing env/stack configuration with keys like `MINIO_HTTP_URL` or `MINIO_ENDPOINT`, and `CLICKHOUSE_HTTP_URL` or `CLICKHOUSE_URL`. If different keys are already standard (e.g. `MINIO_URL`, `CLICKHOUSE_HOST`/`CLICKHOUSE_PORT`), adjust the `cfg.get(...)` calls accordingly.
2. If other parts of `verify_canonical_endpoints.py` already derive MinIO/ClickHouse URLs from `cfg`, you may want to factor that logic into helper functions and reuse them here so the health checks follow exactly the same configuration rules.
</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 thread scripts/verification/verify_canonical_endpoints.py

@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 comprehensive canonical endpoint verification script (verify_canonical_endpoints.py) to validate router, LiteLLM, Langfuse, and infrastructure services, along with corresponding documentation in the README.md. It also updates pod.yaml and start-stack.sh to support LiteLLM UI credentials and dynamically derive NEXTAUTH_URL from PUBLIC_BASE_URL. Feedback on these changes focuses on improving the robustness of the verification script, specifically by dynamically loading MinIO and ClickHouse ports from the environment, enhancing the custom .env parser to handle inline comments and export prefixes, and applying defensive programming guards when parsing JSON responses to prevent unhandled exceptions.

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 +44 to +52
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.

high

The verification script hardcodes the default ports for MinIO (9002) and ClickHouse (8123) in test_infra_health, which defeats the purpose of reading configuration from .env files. If these ports are overridden in the environment, the health checks will fail. We should load MINIO_S3_PORT and CLICKHOUSE_HTTP_PORT dynamically from the environment with sensible defaults, and also strip any trailing slashes from PUBLIC_BASE_URL to prevent double slashes in constructed URLs.

Suggested change
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")),
}
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", "").rstrip("/"),
"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"),
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch! Fixed: the .env parser now handles export prefix, strips inline comments, and logs which env files were loaded (with a warning for missing files).

Comment on lines +285 to +296
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))

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

Use the dynamically loaded minio_s3_port and clickhouse_http_port from the configuration instead of hardcoding 9002 and 8123.

Suggested change
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))
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(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.")

Comment on lines +31 to +37
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

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

Improve the custom .env parser to handle inline comments (e.g., PORT=5000 # comment) and export prefixes, which are common in environment files and can cause malformed port or URL values.

Suggested change
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
line = line.strip()
if " #" in line:
line = line.split(" #", 1)[0].strip()
if line.startswith("export "):
line = line[7:].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

Comment on lines +75 to +77
models = r.json()
model_ids = [m["id"] for m in models.get("data", [])]
ok = r.status_code == 200 and len(model_ids) > 0

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

Ensure defensive programming when parsing JSON responses. If the response is not a dictionary or contains unexpected types, accessing .get() or list elements directly can raise unhandled exceptions. This refactoring safely handles non-dictionary or missing key scenarios.

Suggested change
models = r.json()
model_ids = [m["id"] for m in models.get("data", [])]
ok = r.status_code == 200 and len(model_ids) > 0
models = r.json()
data_list = models.get("data") if isinstance(models, dict) else None
model_ids = [m.get("id") for m in data_list if isinstance(m, dict) and "id" in m] if isinstance(data_list, list) else []
ok = r.status_code == 200 and len(model_ids) > 0

Comment on lines +151 to +153
models = r.json()
model_ids = [m["id"] for m in models.get("data", [])]
ok = r.status_code == 200 and len(model_ids) > 0

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

Apply defensive programming guards when parsing the model list from the LiteLLM endpoint to prevent potential AttributeError or TypeError if the response structure is unexpected.

Suggested change
models = r.json()
model_ids = [m["id"] for m in models.get("data", [])]
ok = r.status_code == 200 and len(model_ids) > 0
models = r.json()
data_list = models.get("data") if isinstance(models, dict) else None
model_ids = [m.get("id") for m in data_list if isinstance(m, dict) and "id" in m] if isinstance(data_list, list) else []
ok = r.status_code == 200 and len(model_ids) > 0

Comment on lines +247 to +251
data = r.json()
choices = data.get("choices")
message = choices[0].get("message", {}) if choices else {}
content = (message.get("content") or "").strip()
reasoning = (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.

medium

Safely parse the chat completion response structure. Accessing choices[0] directly without checking if choices is a non-empty list, or calling .get() on elements that might be None, can lead to unhandled exceptions.

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

Comment on lines +324 to +328
data = r.json()
choices = data.get("choices")
message = choices[0].get("message", {}) if choices else {}
content = (message.get("content") or "").strip()
reasoning = (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.

medium

Safely parse the direct chat completion response from LiteLLM to prevent unhandled exceptions if the response structure is unexpected.

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch! Fixed: extracted a shared parse_chat_response() helper with full isinstance guards on data, choices, first_choice, and message. Applied to all three chat completion parsers (E2E, LiteLLM direct, and canonical POST).

Comment on lines +388 to +392
data = r.json()
choices = data.get("choices")
message = choices[0].get("message", {}) if choices else {}
content = (message.get("content") or "").strip()
reasoning = (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.

medium

Safely parse the canonical chat completion response to prevent unhandled exceptions if the response structure is unexpected.

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch! Fixed: the canonical POST chat completion now uses the shared parse_chat_response() helper with full isinstance guards — same fix as the direct chat parser.

boy added 2 commits July 12, 2026 00:32
… ports, .env logging

- Extract parse_chat_response() helper to deduplicate chat completion parsing
  across test_e2e_chat, test_litellm_direct_chat, and test_canonical_urls
- Add isinstance guards for defensive JSON response parsing (Gemini suggestion)
- Load MinIO and ClickHouse ports from .env (MINIO_S3_PORT, CLICKHOUSE_HTTP_PORT)
  instead of hardcoding 9002/8123 (Sourcery suggestion)
- Log which .env files were loaded and warn on missing files (Sourcery suggestion)
- Handle export prefix and inline comments in .env parser (Gemini suggestion)
Apply isinstance guards on choices/message across benchmark_classifier.py,
retry_errors.py, and reclassify_all.py — same pattern as the verification
script's parse_chat_response() helper. Prevents unhandled exceptions when
the API returns unexpected response structures.
@sheepdestroyer

Copy link
Copy Markdown
Owner Author

Closing in favor of a fresh PR with all review fixes applied. See #259.

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