Skip to content

chore: eliminate sys.path.insert hacks — migrate to package-relative imports#278

Closed
sheepdestroyer wants to merge 14 commits into
masterfrom
chore/eliminate-sys-path-hacks-565508234636993796
Closed

chore: eliminate sys.path.insert hacks — migrate to package-relative imports#278
sheepdestroyer wants to merge 14 commits into
masterfrom
chore/eliminate-sys-path-hacks-565508234636993796

Conversation

@sheepdestroyer

Copy link
Copy Markdown
Owner

What

This PR removes vestigial sys.path.insert hacks from scripts in scripts/, migrating to standard package-relative imports with ImportError fallbacks. It also adds a missing __init__.py to the router/ directory, normalizes structured content parsing in chat_helpers.py, removes dead sys.path.insert hacks from two router test files flagged by Gemini Code Assist, and moves the classifier api_base from a hardcoded URL to an env-var placeholder.

Fixes #266

Review feedback addressed (round 2 — since PR #275)

Gemini Code Assist

  • start-stack.sh: fail fast if LLAMA_CLASSIFIER_URL is unset/empty using ${VAR:?} bash expansion (instead of silently producing invalid litellm config with empty api_base)
  • router/main.py: parse boolean-like strings for CLASSIFIER_CA_BUNDLE — handles "False"/"0"/"off"/"no" → verify=False, "True"/"1"/"on"/"yes" → verify=True, anything else → file path. Prevents httpx FileNotFoundError when user naively sets CLASSIFIER_CA_BUNDLE=False in .env
  • router/main.py: add isinstance(router_api_base, str) guard before calling .startswith() — prevents AttributeError if api_base is null/non-string in YAML config

Review feedback addressed (round 1 — since PR #273)

Gemini Code Assist

  • tests/test_a2_verify.py: added try/except ImportError fallback so the script remains runnable both via pytest and directly

CodeRabbit

  • router/main.py: extracted shared _http_limits() helper to deduplicate httpx.Limits construction
  • router/main.py: replaced hardcoded verify=False with CLASSIFIER_CA_BUNDLE env var (with boolean parsing)
  • router/main.py: close _classifier_client in lifespan shutdown cleanup

Env consistency

  • .env.dev: added LLAMA_CLASSIFIER_URL since the llama classifier is shared across envs

Rejected review comments (with reasoning)

Original Changes

Import cleanup (9 scripts)

  • Removed sys.path.insert hacks from all scripts in scripts/ and verification/
  • Replaced with from scripts.chat_helpers import parse_chat_response + ImportError fallback

Router package

  • Added router/__init__.py to make router a proper Python package
  • Updated router/main.py to use from router.circuit_breaker import get_breaker

chat_helpers.py

  • Added _normalize_chat_content() helper for structured content payloads

upgrade-prod.sh

  • Self-copy guard, centralized cleanup via trap, non-interactive mode, pre-flight .env validation

Misc

  • Removed unused imports from test files

Related

google-labs-jules Bot and others added 13 commits July 12, 2026 14:04
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
- Revert 'from .circuit_breaker' to 'from router.circuit_breaker' in main.py:
  relative imports break when main.py is imported directly as 'main'
  (tests use 'from main import ...' with PYTHONPATH=.:router)
- Add try/except ImportError fallback to 'from scripts.chat_helpers'
  across all 7 scripts (4 in scripts/, 3 in verification/)
  so they remain runnable via both package import and direct execution
…e imports

This commit:
- Removes sys.path.insert hacks from scripts/
- Adds router/__init__.py to make router a package
- Updates router/main.py and router/agy_proxy.py to use package-aware imports
- Updates tests and mocks to use absolute package paths
- Updates CI workflow to use standard PYTHONPATH=.
- Ensures both CLI and package execution modes work correctly

Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Gemini Code Assist correctly identified that the PR's removal of sys.path
hacks broke direct execution of all 9 scripts. When Python runs a script,
it REPLACES sys.path[0] (the CWD) with the script's directory — so cross-
package imports (router.*, scripts.chat_helpers) fail with ModuleNotFoundError.

Two failure patterns, two fixes:

1. Scripts in scripts/ importing scripts.chat_helpers:
   try/except with bare 'from chat_helpers' fallback (found in sys.path[0])

2. Scripts in scripts/verification/ importing scripts.chat_helpers or router.*:
   try/except with sys.path.insert to repo root (parents[2])

Verified: all 9 scripts now run successfully via both 'python scripts/X.py'
(direct) and package imports (from scripts.X import ...)
Gemini Code Assist flagged that test_dashboard_data.py and
test_resolve_external_urls.py still use sys.path.insert(0, router_path)
hacks despite having migrated to absolute package imports
(from router import main).

These hacks were doubly broken:
1. They inserted router/ (the package dir) into sys.path, which doesn't
   help resolve 'from router import main' — Python needs the PARENT dir.
2. router/tests/conftest.py already inserts the correct dir (repo root).

Removed the dead sys/os imports along with the hack blocks.
All 193 tests pass. Scripts import cleanly.
- Add os.environ/ resolution for api_base in router/main.py (same pattern
  as api_key resolution at line 351)
- Add get_classifier_client() with verify=False for internal self-signed TLS
- Change router/config.yaml api_base to os.environ/LLAMA_CLASSIFIER_URL
  placeholder instead of hardcoded URL
- Update test patches to use get_classifier_client
- Add LLAMA_CLASSIFIER_URL to .env pointing at canonical endpoint
  https://x570.vendeuvre.lan/llm-routing/llama/v1

The bot previously hardcoded the canonical URL directly in config.yaml.
Now it's resolved from .env at runtime, keeping config DRY and making
dev/prod environments differ only by their .env files.
The bot previously hardcoded the canonical endpoint in litellm/config.yaml
for two local models (local-qwen-3.6 and nomic-embed-text). Replace with
LLAMA_CLASSIFIER_URL_PLACEHOLDER, resolved at render time by
render_litellm_config() via the same env var used for the router classifier.

The placeholder is substituted during deploy (start-stack.sh) using the
value from .env, keeping config DRY — dev/prod differ only by .env files.
from router.circuit_breaker fails in Docker's flat /app/ layout where
files are not nested in a router/ package directory. Add try/except
ImportError with flat 'from circuit_breaker' fallback — same pattern
used for scripts/chat_helpers imports.
Router waits up to 180s for LiteLLM startup but liveness probe fired
at 20s — on cold starts (postgres from scratch, LiteLLM migrations),
the container was killed before LiteLLM was ready, causing a restart
loop. Readiness probe also bumped from 10s to 30s.

Matches LiteLLM's own 240s liveness probe.
Replace all references to gemma4-26a4b-routing, qwen-0.8b-routing, and
qwen-2b-routing with the canonical qwen-4b-routing classifier model.

Changed files: router/config.yaml, router/main.py, README.md, test
classifier accuracy test, and all 4 batch classification scripts.
- tests/test_a2_verify.py: add try/except ImportError fallback for direct
  script execution (Gemini Code Assist review)
- router/main.py: extract shared _http_limits() helper to deduplicate
  httpx.Limits construction across get_http_client() and
  get_classifier_client() (CodeRabbit review)
- router/main.py: use CLASSIFIER_CA_BUNDLE env var for classifier TLS
  verify (defaults to False — current behavior preserved)
  (CodeRabbit security review)
- router/main.py: close _classifier_client in lifespan shutdown cleanup
  (CodeRabbit stability review)
- .env.dev: add LLAMA_CLASSIFIER_URL (shared classifier across envs)

Rejected review comments:
- benchmark_tokens.py sys.path.insert: already uses try/except ImportError
  fallback — same pattern as all other scripts, not a violation
- verify_canonical_endpoints.py sys.path.insert: same — standard pattern
- docstring coverage < 80%: global repo concern, not PR-specific
- out-of-scope classifier changes: tracked via policy as issue, not
  blocking
- start-stack.sh: fail fast if LLAMA_CLASSIFIER_URL is unset/empty
  using ${VAR:?} bash parameter expansion instead of silently
  producing an invalid litellm config with empty api_base
- router/main.py: parse boolean-like strings for CLASSIFIER_CA_BUNDLE
  ('false'/'0'/'off'/'no' → verify=False, 'true'/'1'/'on'/'yes' →
  verify=True) — prevents httpx FileNotFoundError when env var is
  set to 'False' (Python treats non-empty strings as truthy)
- router/main.py: add isinstance(str) guard on router_api_base before
  calling .startswith() — prevents AttributeError if api_base is
  null/non-string in the YAML config

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

Sorry @sheepdestroyer, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 12, 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: 25 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: ebbd818b-99b8-44fe-aba1-f05eeac6e997

📥 Commits

Reviewing files that changed from the base of the PR and between 3f7f503 and 5977e14.

📒 Files selected for processing (30)
  • .env.dev
  • .github/workflows/test.yml
  • README.md
  • litellm/config.yaml
  • pod.yaml
  • router/__init__.py
  • router/agy_proxy.py
  • router/config.yaml
  • router/main.py
  • router/tests/test_dashboard_data.py
  • router/tests/test_detect_active_tool.py
  • router/tests/test_estimate_prompt_tokens.py
  • router/tests/test_get_gemini_oauth_status.py
  • router/tests/test_get_goose_sessions.py
  • router/tests/test_load_persisted_stats.py
  • router/tests/test_resolve_external_urls.py
  • router/tests/test_routing_behavior.py
  • scripts/benchmark_classifier.py
  • scripts/benchmark_tokens.py
  • scripts/classify_direct.py
  • scripts/reclassify_all.py
  • scripts/retry_errors.py
  • scripts/verification/verification_helpers.py
  • scripts/verification/verify_breaker.py
  • scripts/verification/verify_canonical_endpoints.py
  • scripts/verification/verify_ollama_routing.py
  • start-stack.sh
  • tests/test_a2_verify.py
  • tests/test_classifier_accuracy.py
  • tests/test_models_proxy.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/eliminate-sys-path-hacks-565508234636993796

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.

@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 transitions the complexity triage classifier model to qwen-4b-routing and externalizes its endpoint configuration via the LLAMA_CLASSIFIER_URL environment variable. It also introduces a dedicated HTTP client for classifier requests with support for custom CA bundles, adjusts pod startup probe delays, and refactors test imports for better portability. The review feedback highlights opportunities to make the configuration parsing more robust, specifically by safely handling whitespace-only CA bundle environment variables, stripping trailing slashes from the API base URL, and ensuring the API key is treated as a string to prevent potential runtime crashes.

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 +120 to +126
ca_bundle = os.getenv("CLASSIFIER_CA_BUNDLE")
if ca_bundle is not None and ca_bundle.strip().lower() in ("false", "0", "off", "no"):
verify = False
elif ca_bundle is not None and ca_bundle.strip().lower() in ("true", "1", "on", "yes"):
verify = True
else:
verify = ca_bundle or False

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 CLASSIFIER_CA_BUNDLE is set to an empty or whitespace-only string (e.g., " "), ca_bundle.strip().lower() will evaluate to "". Since "" is not in the boolean lists, it falls through to verify = ca_bundle or False, which evaluates to " ". Passing a whitespace-only string to httpx.AsyncClient as a file path will raise a FileNotFoundError at runtime. Stripping the value and handling empty strings safely prevents this issue.

Suggested change
ca_bundle = os.getenv("CLASSIFIER_CA_BUNDLE")
if ca_bundle is not None and ca_bundle.strip().lower() in ("false", "0", "off", "no"):
verify = False
elif ca_bundle is not None and ca_bundle.strip().lower() in ("true", "1", "on", "yes"):
verify = True
else:
verify = ca_bundle or False
ca_bundle = os.getenv("CLASSIFIER_CA_BUNDLE")
if ca_bundle is not None:
ca_bundle_stripped = ca_bundle.strip()
if ca_bundle_stripped.lower() in ("false", "0", "off", "no", ""):
verify = False
elif ca_bundle_stripped.lower() in ("true", "1", "on", "yes"):
verify = True
else:
verify = ca_bundle_stripped
else:
verify = False

Comment thread router/main.py Outdated
Comment on lines +379 to +389
router_api_base = router_model_conf.get("api_base", "http://127.0.0.1:8080/v1")
if isinstance(router_api_base, str) and router_api_base.startswith("os.environ/"):
env_var = router_api_base.split("/", 1)[1]
router_api_base = os.environ.get(env_var, "")
if not router_api_base:
if "pytest" in sys.modules:
router_api_base = "http://127.0.0.1:8080/v1"
else:
raise RuntimeError(
f"Configuration error: Environment variable '{env_var}' is missing or empty."
)

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 api_base is explicitly set to null or an empty string in the YAML configuration, router_api_base will be None or "", which can cause runtime errors when formatting the request URL. Additionally, if router_api_base has a trailing slash, appending /chat/completions will result in double slashes (e.g., v1//chat/completions), which can cause 404 errors or redirection issues with strict reverse proxies. Normalizing the URL by defaulting falsy values and stripping trailing slashes makes the routing more robust.

Suggested change
router_api_base = router_model_conf.get("api_base", "http://127.0.0.1:8080/v1")
if isinstance(router_api_base, str) and router_api_base.startswith("os.environ/"):
env_var = router_api_base.split("/", 1)[1]
router_api_base = os.environ.get(env_var, "")
if not router_api_base:
if "pytest" in sys.modules:
router_api_base = "http://127.0.0.1:8080/v1"
else:
raise RuntimeError(
f"Configuration error: Environment variable '{env_var}' is missing or empty."
)
router_api_base = router_model_conf.get("api_base") or "http://127.0.0.1:8080/v1"
if isinstance(router_api_base, str):
if router_api_base.startswith("os.environ/"):
env_var = router_api_base.split("/", 1)[1]
router_api_base = os.environ.get(env_var, "")
if not router_api_base:
if "pytest" in sys.modules:
router_api_base = "http://127.0.0.1:8080/v1"
else:
raise RuntimeError(
f"Configuration error: Environment variable '{env_var}' is missing or empty."
)
router_api_base = router_api_base.rstrip("/")

Comment thread router/main.py
Comment on lines 391 to 393
router_api_key = router_model_conf.get("api_key")
if not router_api_key:
raise RuntimeError("Configuration error: 'api_key' is missing from router_model configuration.")

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 api_key is parsed as a non-string (such as an integer or boolean) from the YAML configuration, the subsequent check router_api_key.startswith("os.environ/") on line 394 will raise an AttributeError. Ensuring router_api_key is converted to a string prevents this potential startup crash.

Suggested change
router_api_key = router_model_conf.get("api_key")
if not router_api_key:
raise RuntimeError("Configuration error: 'api_key' is missing from router_model configuration.")
router_api_key = router_model_conf.get("api_key")
if not router_api_key:
raise RuntimeError("Configuration error: 'api_key' is missing from router_model configuration.")
if not isinstance(router_api_key, str):
router_api_key = str(router_api_key)

- router/main.py: handle whitespace-only CLASSIFIER_CA_BUNDLE by stripping
  before boolean check and adding empty-string to the false list. Prevents
  httpx FileNotFoundError when env var is set to whitespace.

- router/main.py: use .get('api_base') or default instead of passing
  default as 2nd arg — handles null values in YAML config (key exists
  but is null). Add .rstrip('/') to prevent double slashes when
  appending /chat/completions.

- router/main.py: add isinstance(str) guard on router_api_key before
  .startswith() — prevents AttributeError if YAML parses api_key as
  int/bool/non-string.
@sheepdestroyer

Copy link
Copy Markdown
Owner Author

Superseded — review fixes applied + dev-verified, opening fresh PR.

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.

refactor: eliminate sys.path.insert hacks — migrate to package-relative imports

1 participant