Skip to content

PR 204 Review Fixes#205

Closed
sheepdestroyer wants to merge 20 commits into
masterfrom
chore/pr204-review-fixes
Closed

PR 204 Review Fixes#205
sheepdestroyer wants to merge 20 commits into
masterfrom
chore/pr204-review-fixes

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jul 2, 2026

Copy link
Copy Markdown
Owner

This PR addresses and resolves all outstanding review findings on PR #204. All automated unit tests and 5-tier intent routing integration tests on the live gateway stack have successfully passed.

Summary by CodeRabbit

  • New Features

    • Added faster, more resilient routing and cache handling, including improved prompt sizing, asynchronous file reads, and better support for live stack verification.
  • Bug Fixes

    • Fixed several reliability issues around model scoring, routing decisions, memory handling, and quota detection.
    • Improved startup and deployment defaults by updating service versions and replacing hardcoded credentials with placeholders.
  • Documentation

    • Expanded setup and verification docs with clearer guidance for testing, stack checks, and release validation.

…am limits and add early arg parsing for --help in start-stack.sh

@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 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR bundles agent documentation/policies, deployment secret hardening across pod.yaml/start-stack.sh, router core refactors (async Redis init, regex token estimation, lazy AA score loading, routing normalization, cached dashboard annotations, async agy_proxy quota detection, memory MCP key/delete hardening), new developer tooling scripts, extensive new/updated test coverage, test path cleanup, and Prisma datetime serialization.

Changes

Agent Documentation and Policy Files

Layer / File(s) Summary
Rebase policy, audit report, and regression analysis docs
.agents/AGENTS.md, .agents/branch_audit_plan.md, .agents/jules_regression_analysis.md
Adds git rebase/conflict resolution rules, a branch audit report documenting a restored regression fix, and a regression analysis doc with guardrails and prompt templates for bot agents.

Infrastructure, Deployment Config, and CI

Layer / File(s) Summary
pod.yaml credential placeholders and image version bumps
pod.yaml
Replaces hardcoded secrets with placeholder values and bumps LiteLLM, ClickHouse, Langfuse, pgvector, and Minio image versions.
start-stack.sh secret generation and rendering
start-stack.sh
Extends argument parsing, secret precheck, generate_uuid()-based secret generation, interactive OLLAMA_API_KEY prompt, and render_pod_yaml() placeholder substitution.
README and docs version-pin updates
README.md, scripts/README.md
Updates image version pin references, adds a live stack tier testing section, and revises integration test path documentation.
CI workflow and Dockerfile dependency/path updates
.github/workflows/test.yml, router/Dockerfile, scripts/watch_quota.sh
Adds aiofiles dependency, updates pytest ignore/verification script paths, and fixes script path resolution.

Router Core Behavior Changes

Layer / File(s) Summary
Valkey/Redis init, regex token estimator, classifier truncation
router/main.py, router/tests/test_get_redis.py, router/tests/test_estimate_prompt_tokens.py
Prefers VALKEY_URL for Redis init, replaces character-count token estimation with regex-weighted heuristic, truncates classifier input.
Lazy AA score loading and routing normalization
router/main.py, tests/test_compute_free_model_score.py
Moves AA score loading to lazy background-thread loading and normalizes "llm-routing-agy" to "agent-advanced-core".
Async mtime-cached dashboard annotations
router/main.py, tests/test_read_annotations_async.py
Adds mtime-based async cache for annotation reads and invalidates it after saves.
Async quota detection in agy_proxy
router/agy_proxy.py, router/tests/test_agy_proxy.py
Converts quota-exhaustion log check to async using aiofiles-based tailing.
Memory MCP key v2 format and delete hardening
router/memory_mcp.py, router/tests/test_memory_mcp.py
Adds URL-encoded v2 key format, hardens key/value parsing, and URL-quotes keys in delete handlers.
New unit tests for additional router/main.py helpers
router/tests/test_detect_active_tool.py, router/tests/test_get_gemini_oauth_status.py, router/tests/test_get_goose_sessions.py, router/tests/test_load_persisted_stats.py
Adds new test coverage for detect_active_tool, get_gemini_oauth_status, get_goose_sessions, and load_persisted_stats.

Developer Tooling Scripts

Layer / File(s) Summary
Concurrent classifier benchmarking
scripts/benchmark_classifier.py
Rewrites benchmarking loop to use ThreadPoolExecutor concurrency with process_item() and rate-limiting.
Token estimation accuracy benchmark script
scripts/benchmark_tokens.py
New script validating estimate_prompt_tokens accuracy against hard-coded cases.
PR status CLI script and tests
scripts/get_pr_status.py, tests/test_get_pr_status.py, get_pr_status.py
Adds gh pr view wrapper script with tests; removes old run_cmd helper from root script.
Live tier verification and breaker path fix
scripts/verification/test_reasoning_tiers.py, scripts/verification/verify_breaker.py
Adds tier-routing verification script and fixes sys.path base in verify_breaker.py.

Test Infrastructure Cleanup and New Daemon Tests

Layer / File(s) Summary
Shared conftest.py and removal of per-test path setup
tests/conftest.py, tests/test_a2_verify.py, tests/test_circuit_breaker.py, tests/test_models_proxy.py, tests/test_agy_behavior.py, tests/test_antigravity.py
Adds shared root-discovery conftest, removes redundant sys.path setup, and adds CI skip for antigravity test.
host_agy_daemon comprehensive test suite
tests/test_host_agy_daemon.py
Adds extensive new tests for daemon /run endpoint, streaming, error handling, and server lifecycle.

Prisma Datetime Serialization

Layer / File(s) Summary
Datetime serializer registration in litellm entrypoint
litellm/entrypoint.py
Registers UTC ISO-8601 datetime serializers with Prisma's builder for original_datetime and RobustDatetime.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant get_redis
  participant aioredis

  Caller->>get_redis: request client
  alt VALKEY_URL set
    get_redis->>aioredis: Redis.from_url(VALKEY_URL)
  else host/port fallback
    get_redis->>aioredis: Redis(host, port)
  end
  aioredis-->>get_redis: client or exception
  get_redis-->>Caller: cached client or None
Loading
sequenceDiagram
  participant Client
  participant save_annotations
  participant _read_annotations_async
  participant Filesystem
  participant _annotations_cache

  Client->>save_annotations: save new annotations
  save_annotations->>_read_annotations_async: load existing annotations
  _read_annotations_async->>Filesystem: getmtime()
  alt cache matches mtime
    _read_annotations_async-->>save_annotations: cached data (deep copy)
  else stale or missing
    _read_annotations_async->>Filesystem: aiofiles read + JSON parse
    _read_annotations_async->>_annotations_cache: store mtime + data
    _read_annotations_async-->>save_annotations: parsed data
  end
  save_annotations->>Filesystem: write updated annotations
  save_annotations->>_annotations_cache: invalidate entry
Loading

Possibly related PRs

  • sheepdestroyer/LLM-Routing#47: Both PRs modify Valkey/Redis connection initialization in router/main.py to use URL-based/environment-driven configuration.
  • sheepdestroyer/LLM-Routing#118: Both PRs change the prompt token estimation logic in router/main.py from character-based counting to a more accurate heuristic.
  • sheepdestroyer/LLM-Routing#168: Both PRs target the shared _annotations_cache mtime-based caching mechanism in router/main.py, one adding async reads and the other sync-read test coverage.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the PR but too generic to convey the main changes. Use a more specific title that names the primary fix area, such as restored review fixes for routing, tests, and deployment configs.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/pr204-review-fixes

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 introduces comprehensive guidelines and audits to prevent merge regressions, updates container image versions, and secures credentials using placeholders in pod.yaml which are dynamically rendered by start-stack.sh. Key code improvements include offloading blocking synchronous I/O operations (such as loading agentic scores and reading annotations) to asynchronous tasks or thread pools, implementing a regex-based token estimation heuristic, and fixing a category injection vulnerability in the Memory MCP by URL-encoding category keys. Additionally, numerous unit and integration tests have been added or reorganized. The review feedback correctly identifies two critical bash syntax errors in start-stack.sh where the local keyword is used outside of a function, which will cause the script to crash when prompting for API keys.

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 start-stack.sh Outdated
read -rs OPENROUTER_API_KEY
echo ""
echo "OPENROUTER_API_KEY=\"$OPENROUTER_API_KEY\"" >> "$ENV_FILE"
local escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")

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

Using the local keyword outside of a function is a bash syntax error. Since set -e is enabled at the top of the script, this will cause start-stack.sh to crash immediately when prompting the user for the OpenRouter API key. Remove the local keyword to make it a standard global variable assignment.

Suggested change
local escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")

Comment thread start-stack.sh Outdated
echo "❌ Error: API key cannot be empty. Please try again."
fi
done
local escaped_key=$(escape_env_val "$OLLAMA_API_KEY")

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

Using the local keyword outside of a function is a bash syntax error. Since set -e is enabled at the top of the script, this will cause start-stack.sh to crash immediately when prompting the user for the Ollama API key. Remove the local keyword to make it a standard global variable assignment.

Suggested change
local escaped_key=$(escape_env_val "$OLLAMA_API_KEY")
escaped_key=$(escape_env_val "$OLLAMA_API_KEY")

@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: 6

🧹 Nitpick comments (7)
router/tests/test_load_persisted_stats.py (1)

36-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Blanket os.path.exists=True mock unintentionally exercises the timeline-loading branch too.

Upstream load_persisted_stats also checks os.path.exists(timeline_path) and reads a second file when the stats file is found. Since this test mocks os.path.exists to always return True, the timeline branch is silently triggered too, and mock_open's read_data (the stats JSON) gets parsed as the timeline JSON. It happens not to raise, so the test passes, but this masks the actual timeline-loading behavior with an unasserted side effect.

♻️ Suggested refactor using a path-keyed side_effect
-    with patch("main.os.path.exists", return_value=True):
+    with patch("main.os.path.exists", side_effect=lambda p: p == main.STATS_JSON_PATH):
         with patch("main.open", mock_open(read_data=mock_json)):
🤖 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 `@router/tests/test_load_persisted_stats.py` around lines 36 - 58, The
`test_load_persisted_stats_success` setup is over-mocking `os.path.exists`,
which also forces the timeline-loading path in `load_persisted_stats` and hides
unintended behavior. Update the test to use a path-aware `side_effect` for
`main.os.path.exists` so only the stats file exists and the timeline branch is
not exercised unless explicitly intended. Keep the assertions on
`load_persisted_stats` and `mock_logger` focused on the stats-loading behavior.
router/tests/test_detect_active_tool.py (1)

1-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated sys.path/CONFIG_PATH bootstrap across new test files.

This same boilerplate (setting CONFIG_PATH and inserting the router dir into sys.path) is repeated verbatim in test_get_gemini_oauth_status.py, test_get_goose_sessions.py, and test_load_persisted_stats.py. Consider extracting it into a router/tests/conftest.py fixture/autouse setup, similar to the shared tests/conftest.py being introduced elsewhere in this PR stack for the top-level tests/ directory.

🤖 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 `@router/tests/test_detect_active_tool.py` around lines 1 - 10, The test
bootstrap for CONFIG_PATH and sys.path is duplicated across multiple new router
test files, so move that shared setup into router/tests/conftest.py and make it
apply automatically for the suite. Update the affected tests such as
test_detect_active_tool.py, test_get_gemini_oauth_status.py,
test_get_goose_sessions.py, and test_load_persisted_stats.py to rely on the
shared conftest setup instead of repeating the same import-time boilerplate.
scripts/benchmark_classifier.py (1)

76-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the request staggering actually serialize start times.

Semaphore(5) matches max_workers=5, so all workers can acquire it, sleep together, and hit the server together. Use a small lock-protected start interval if the goal is to smooth live-gateway load.

Proposed fix
 with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
     sem = threading.Semaphore(5)
+    rate_lock = threading.Lock()
+    next_start_time = [time.monotonic()]
     
     def process_item_with_rate_limit(index_and_item):
         i, item = index_and_item
         with sem:
-            # Add a small delay between acquisitions to stagger server load
-            time.sleep(0.05)
+            with rate_lock:
+                now = time.monotonic()
+                if now < next_start_time[0]:
+                    time.sleep(next_start_time[0] - now)
+                next_start_time[0] = time.monotonic() + 0.05
             expected, predicted = process_item(item)
         return i, item, expected, predicted
🤖 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/benchmark_classifier.py` around lines 76 - 84, The current
rate-limiting in process_item_with_rate_limit uses Semaphore(5), which still
allows all ThreadPoolExecutor workers to start together and only sleep
concurrently. Replace this with a lock-protected stagger mechanism that
serializes the start of each request in benchmark_classifier.py, so each call to
process_item begins after a controlled interval rather than all at once. Keep
the change localized around process_item_with_rate_limit and the executor setup,
and ensure the staggering logic enforces one-at-a-time start spacing across
workers.
tests/test_host_agy_daemon.py (1)

45-46: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Global monkeypatching of shared os module functions.

Several tests patch attributes directly on host_agy_daemon.os (e.g., os.read, os.getcwd, os.path.exists, os.unlink) rather than on a more narrowly-scoped wrapper. Since os is a process-wide singleton module, this temporarily alters behavior for the entire interpreter (including the daemon server's background thread) for the duration of the test. monkeypatch reverts these safely at teardown, so this is low risk in the current sequential test setup, but it's worth keeping in mind if tests are ever parallelized (e.g., via pytest-xdist with -n and shared processes) as it could introduce flakiness.

Also applies to: 66-66, 155-155, 251-251, 296-296, 405-405

🤖 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 `@tests/test_host_agy_daemon.py` around lines 45 - 46, Global monkeypatching of
host_agy_daemon.os is too broad and can affect other threads or future parallel
test runs. Refactor host_agy_daemon to use narrow helper wrappers for filesystem
and cwd access, then update the affected tests to patch those wrappers instead
of os.read, os.getcwd, os.path.exists, and os.unlink directly. Use the existing
host_agy_daemon module symbols as the patch target so the behavior stays
isolated to the daemon under test.
scripts/verification/test_reasoning_tiers.py (2)

14-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded gateway URL/auth for a reusable verification script.

gateway_url and the bearer token are fixed constants, limiting reuse against other stacks/environments (staging, CI). Consider sourcing from env vars with sensible defaults.

♻️ Suggested fix
-gateway_url = "http://127.0.0.1:5000/v1/chat/completions"
+gateway_url = os.environ.get("GATEWAY_URL", "http://127.0.0.1:5000/v1/chat/completions")
 headers = {
-    "Authorization": "Bearer gateway-pass",
+    "Authorization": f"Bearer {os.environ.get('GATEWAY_TOKEN', 'gateway-pass')}",
     "Content-Type": "application/json"
 }
🤖 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/test_reasoning_tiers.py` around lines 14 - 18, The
verification script currently hardcodes the gateway URL and bearer token, making
it difficult to reuse across environments. Update the configuration in
test_reasoning_tiers.py so gateway_url and the Authorization value are read from
environment variables with sensible defaults, and keep the existing
request-building logic in the same script entry points.

43-84: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Script only prints results — no actual routing validation.

The module docstring says it "validates the gateway's routing responses" (Line 7), but run_tests never checks model_used against an expected tier/model mapping, nor does it return a non-zero exit code on mismatch or request failure. As-is, this is a manual smoke-test rather than a verification the caller (e.g. CI or a human skimming output) can trust without eyeballing every line.

Consider adding an expected_model (or model prefix) per tier entry and asserting/tallying pass-fail, then calling sys.exit(1) if any tier misroutes.

♻️ Suggested direction
 prompts = [
     {
         "name": "Simple (1/5)",
-        "prompt": "Write a one-line hello world in Python."
+        "prompt": "Write a one-line hello world in Python.",
+        "expected_model": "some-cheap-model"
     },
     ...
 ]

 def run_tests():
+    failures = []
     ...
             if r.status_code == 200:
                 data = r.json()
                 model_used = data.get("model", "unknown")
+                if p.get("expected_model") and p["expected_model"] not in model_used:
+                    failures.append(p["name"])
     ...
+    if failures:
+        print(f"❌ Routing mismatches: {failures}")
+        sys.exit(1)
🤖 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/test_reasoning_tiers.py` around lines 43 - 84, The
current run_tests function in test_reasoning_tiers.py only prints the routed
model and response details, so it never actually verifies routing correctness.
Update the prompts entries to include an expected_model or expected model
prefix, then in run_tests compare the returned model_used against that
expectation, tally pass/fail results, and make the script exit with a non-zero
status when any mismatch, parse failure, or request exception occurs. Use the
existing run_tests, prompts, and model_used symbols to wire in the assertions
and failure handling so the script works as a real validation step rather than a
manual log dump.
router/memory_mcp.py (1)

240-255: 🚀 Performance & Scalability | 🔵 Trivial

Sequential per-item DELETE with no overall bound for wildcard category removal.

For category == "*", to_delete can grow to the entire memory store, and each entry is deleted with its own network round-trip sequentially. There's no overall cap/timeout on the loop itself (only a 5s per-request timeout), so a large local/global memory store could make this handler run for a long time. Consider batching or using asyncio.gather with bounded concurrency for large deletions, while preserving the partial-failure count reporting.

This is a nice-to-have for now given memory categories are unlikely to be huge in this domain — not blocking.

🤖 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 `@router/memory_mcp.py` around lines 240 - 255, The wildcard deletion path in
the memory removal handler is doing sequential per-item DELETEs with only
per-request timeouts, so large category matches can run too long. Update the
deletion flow in router/memory_mcp.py around the loop that builds deleted_count
in the memory delete handler to use batching or bounded-concurrency async
deletes (for example via asyncio.gather with a limit), while still counting
successes and preserving the existing partial-failure error/reporting format for
category and scope labels.
🤖 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 `@litellm/entrypoint.py`:
- Around line 115-118: The datetime normalization in the timezone handling block
should detect naive values using utcoffset() instead of only checking dt.tzinfo.
Update the logic around the dt conversion so that the existing utc-assignment
path applies when dt.utcoffset() is None, and only call astimezone(timezone.utc)
for truly aware datetimes; keep the behavior localized to this datetime
conversion branch.
- Around line 107-110: The prisma serializer import handling is too broad and
hides real import-time failures, causing datetime registrations to be skipped
silently. In the `entrypoint.py` import block around `serializer`, narrow the
`except ImportError` path so it only treats the
missing-serializer/missing-package case as optional, and re-raise any other
import error originating from `prisma.builder` dependency issues. Keep the
fallback assignment to `serializer = None` only for the truly absent serializer
case, and preserve the registration logic that depends on `serializer` being
available.

In `@scripts/benchmark_classifier.py`:
- Around line 66-70: The broad exception handler in the benchmark flow is
catching every Exception and masking real bugs. Narrow the except block in the
classifier benchmarking logic (around the item handling that sets expected and
predicted) to only the anticipated data, network, or JSON-related failures, and
keep the fallback error string behavior for those cases. Use the surrounding
benchmark item processing code to preserve the current expected/predicted
assignment while avoiding a blanket Exception catch.

In `@scripts/verification/test_reasoning_tiers.py`:
- Line 1: Exclude the scripts/verification/ test_reasoning_tiers.py helper from
pytest discovery so the unit-test job does not run live gateway requests. Update
the pytest collection settings and/or the CI workflow to ignore the entire
scripts/verification/ directory, and ensure the change is applied wherever
pytest is invoked. Use scripts/verification/test_reasoning_tiers.py as the
anchor when locating the offending test file.

In `@start-stack.sh`:
- Around line 23-27: `escape_env_val` only escapes backslashes and double
quotes, so shell metacharacters can still be expanded when `/config/.env` is
sourced. Update `escape_env_val` in start-stack.sh to safely escape or
neutralize `$`, backticks, and command substitution syntax before writing env
values, and make sure the same escaping is used at all call sites that generate
the sourced env file.
- Around line 84-85: The OpenRouter API key write in start-stack.sh can leave
the newly created $ENV_FILE with default umask permissions if the script exits
before the later chmod step. Update the flow around the escaped_key write so the
file is locked down immediately after appending OPENROUTER_API_KEY, using the
same permission-setting approach already used later in the script, and keep the
fix localized near the env-file write path.

---

Nitpick comments:
In `@router/memory_mcp.py`:
- Around line 240-255: The wildcard deletion path in the memory removal handler
is doing sequential per-item DELETEs with only per-request timeouts, so large
category matches can run too long. Update the deletion flow in
router/memory_mcp.py around the loop that builds deleted_count in the memory
delete handler to use batching or bounded-concurrency async deletes (for example
via asyncio.gather with a limit), while still counting successes and preserving
the existing partial-failure error/reporting format for category and scope
labels.

In `@router/tests/test_detect_active_tool.py`:
- Around line 1-10: The test bootstrap for CONFIG_PATH and sys.path is
duplicated across multiple new router test files, so move that shared setup into
router/tests/conftest.py and make it apply automatically for the suite. Update
the affected tests such as test_detect_active_tool.py,
test_get_gemini_oauth_status.py, test_get_goose_sessions.py, and
test_load_persisted_stats.py to rely on the shared conftest setup instead of
repeating the same import-time boilerplate.

In `@router/tests/test_load_persisted_stats.py`:
- Around line 36-58: The `test_load_persisted_stats_success` setup is
over-mocking `os.path.exists`, which also forces the timeline-loading path in
`load_persisted_stats` and hides unintended behavior. Update the test to use a
path-aware `side_effect` for `main.os.path.exists` so only the stats file exists
and the timeline branch is not exercised unless explicitly intended. Keep the
assertions on `load_persisted_stats` and `mock_logger` focused on the
stats-loading behavior.

In `@scripts/benchmark_classifier.py`:
- Around line 76-84: The current rate-limiting in process_item_with_rate_limit
uses Semaphore(5), which still allows all ThreadPoolExecutor workers to start
together and only sleep concurrently. Replace this with a lock-protected stagger
mechanism that serializes the start of each request in benchmark_classifier.py,
so each call to process_item begins after a controlled interval rather than all
at once. Keep the change localized around process_item_with_rate_limit and the
executor setup, and ensure the staggering logic enforces one-at-a-time start
spacing across workers.

In `@scripts/verification/test_reasoning_tiers.py`:
- Around line 14-18: The verification script currently hardcodes the gateway URL
and bearer token, making it difficult to reuse across environments. Update the
configuration in test_reasoning_tiers.py so gateway_url and the Authorization
value are read from environment variables with sensible defaults, and keep the
existing request-building logic in the same script entry points.
- Around line 43-84: The current run_tests function in test_reasoning_tiers.py
only prints the routed model and response details, so it never actually verifies
routing correctness. Update the prompts entries to include an expected_model or
expected model prefix, then in run_tests compare the returned model_used against
that expectation, tally pass/fail results, and make the script exit with a
non-zero status when any mismatch, parse failure, or request exception occurs.
Use the existing run_tests, prompts, and model_used symbols to wire in the
assertions and failure handling so the script works as a real validation step
rather than a manual log dump.

In `@tests/test_host_agy_daemon.py`:
- Around line 45-46: Global monkeypatching of host_agy_daemon.os is too broad
and can affect other threads or future parallel test runs. Refactor
host_agy_daemon to use narrow helper wrappers for filesystem and cwd access,
then update the affected tests to patch those wrappers instead of os.read,
os.getcwd, os.path.exists, and os.unlink directly. Use the existing
host_agy_daemon module symbols as the patch target so the behavior stays
isolated to the daemon under test.
🪄 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: ec942144-fe70-450f-bd34-3cfb090b9574

📥 Commits

Reviewing files that changed from the base of the PR and between c304f3d and ee7584d.

⛔ Files ignored due to path filters (1)
  • debug-homepage.png is excluded by !**/*.png
📒 Files selected for processing (53)
  • .agents/AGENTS.md
  • .agents/branch_audit_plan.md
  • .agents/jules_regression_analysis.md
  • .github/workflows/test.yml
  • README.md
  • get_pr_status.py
  • litellm/entrypoint.py
  • pod.yaml
  • router/Dockerfile
  • router/agy_proxy.py
  • router/main.py
  • router/memory_mcp.py
  • router/test_memory_mcp.py
  • router/tests/test_agy_proxy.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_get_redis.py
  • router/tests/test_load_persisted_stats.py
  • router/tests/test_memory_mcp.py
  • scripts/README.md
  • scripts/benchmark_classifier.py
  • scripts/benchmark_tokens.py
  • scripts/get_pr_status.py
  • scripts/host_agy_daemon.py
  • scripts/sync_gemini_token.py
  • scripts/test_quota_reset.sh
  • scripts/verification/test_reasoning_tiers.py
  • scripts/verification/verify_breaker.py
  • scripts/watch_quota.sh
  • start-stack.sh
  • test_memory_mcp.py
  • tests/conftest.py
  • tests/test_a2_verify.py
  • tests/test_agy_behavior.py
  • tests/test_agy_tiers.py
  • tests/test_antigravity.py
  • tests/test_atomic_write.py
  • tests/test_check_http_endpoint.py
  • tests/test_circuit_breaker.py
  • tests/test_classifier_accuracy.py
  • tests/test_compute_free_model_score.py
  • tests/test_get_pr_status.py
  • tests/test_host_agy_daemon.py
  • tests/test_map_tool_to_category.py
  • tests/test_models_proxy.py
  • tests/test_pie_chart_gradient.py
  • tests/test_read_annotations_async.py
  • tests/test_record_tool_usage.py
  • tests/test_src_badge.py
  • tests/test_stream_latency.py
  • tests/test_sync_gemini_token.py
💤 Files with no reviewable changes (6)
  • router/test_memory_mcp.py
  • get_pr_status.py
  • tests/test_models_proxy.py
  • test_memory_mcp.py
  • tests/test_circuit_breaker.py
  • tests/test_a2_verify.py

Comment thread litellm/entrypoint.py
Comment on lines +107 to +110
try:
from prisma.builder import serializer
except ImportError:
serializer = None

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Prisma dependency references and serializer registrations.
rg -n -C3 '\b(prisma|prisma-client-py)\b|from prisma\.builder import serializer|serializer\.register' .

Repository: sheepdestroyer/LLM-Routing

Length of output: 1539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant block with line numbers.
sed -n '90,130p' litellm/entrypoint.py

printf '\n--- Python import semantics probe ---\n'
python3 - <<'PY'
import tempfile, pathlib, sys, subprocess, textwrap, os

with tempfile.TemporaryDirectory() as d:
    root = pathlib.Path(d)
    # Case 1: package exists, submodule imports fail because of missing dependency.
    (root / "pkg").mkdir()
    (root / "pkg" / "__init__.py").write_text("")
    (root / "pkg" / "builder.py").write_text("import does_not_exist\nserializer = 1\n")

    # Case 2: package missing entirely.
    env = os.environ.copy()
    env["PYTHONPATH"] = d

    code = textwrap.dedent("""
        try:
            from pkg.builder import serializer
        except ImportError as exc:
            print(type(exc).__name__, getattr(exc, 'name', None), str(exc))
    """)
    out = subprocess.check_output([sys.executable, "-c", code], env=env, text=True)
    print("case1:", out.strip())

    # Case 3: module present, requested name missing.
    (root / "pkg2").mkdir()
    (root / "pkg2" / "__init__.py").write_text("")
    (root / "pkg2" / "builder.py").write_text("x = 1\n")
    code2 = textwrap.dedent("""
        try:
            from pkg2.builder import serializer
        except ImportError as exc:
            print(type(exc).__name__, getattr(exc, 'name', None), str(exc))
    """)
    out2 = subprocess.check_output([sys.executable, "-c", code2], env=env, text=True)
    print("case2:", out2.strip())

    # Case 4: top-level package missing.
    code3 = textwrap.dedent("""
        try:
            from missing_pkg.builder import serializer
        except ImportError as exc:
            print(type(exc).__name__, getattr(exc, 'name', None), str(exc))
    """)
    out3 = subprocess.check_output([sys.executable, "-c", code3], env=env, text=True)
    print("case3:", out3.strip())
PY

Repository: sheepdestroyer/LLM-Routing

Length of output: 2081


Don't swallow Prisma serializer import failures. from prisma.builder import serializer can fail because serializer is missing or because prisma.builder hits an import-time dependency error; catching bare ImportError turns both into a silent no-op and skips the datetime registrations. Catch only the missing-package case and re-raise everything else.

🤖 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 `@litellm/entrypoint.py` around lines 107 - 110, The prisma serializer import
handling is too broad and hides real import-time failures, causing datetime
registrations to be skipped silently. In the `entrypoint.py` import block around
`serializer`, narrow the `except ImportError` path so it only treats the
missing-serializer/missing-package case as optional, and re-raise any other
import error originating from `prisma.builder` dependency issues. Keep the
fallback assignment to `serializer = None` only for the truly absent serializer
case, and preserve the registration logic that depends on `serializer` being
available.

Comment thread litellm/entrypoint.py
Comment on lines +115 to +118
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
else:
dt = dt.astimezone(timezone.utc)

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Demonstrate Python's aware/naive rule without executing repository code.
python - <<'PY'
from datetime import datetime, tzinfo

class NaiveTz(tzinfo):
    def utcoffset(self, dt):
        return None

dt = datetime(2026, 1, 1, 12, 0, tzinfo=NaiveTz())
print("tzinfo is None:", dt.tzinfo is None)
print("utcoffset is None:", dt.utcoffset() is None)
PY

Repository: sheepdestroyer/LLM-Routing

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the serializer and its surrounding context.
python3 - <<'PY'
from pathlib import Path

path = Path('litellm/entrypoint.py')
lines = path.read_text().splitlines()
for start, end in [(1, 220)]:
    for i in range(start-1, min(end, len(lines))):
        print(f"{i+1}: {lines[i]}")
PY

# Find tests/usages related to _serialize_dt or datetime serialization.
rg -n "_serialize_dt|timezone.utc|utcoffset\(\)|datetime" litellm tests . -g '!**/.git/**'

Repository: sheepdestroyer/LLM-Routing

Length of output: 10455


🏁 Script executed:

python3 - <<'PY'
from datetime import datetime, tzinfo, timezone

class NaiveTz(tzinfo):
    def utcoffset(self, dt):
        return None

dt = datetime(2026, 1, 1, 12, 0, tzinfo=NaiveTz())
print("tzinfo is None:", dt.tzinfo is None)
print("utcoffset is None:", dt.utcoffset() is None)
try:
    print("astimezone UTC:", dt.astimezone(timezone.utc))
except Exception as e:
    print(type(e).__name__, e)
PY

Repository: sheepdestroyer/LLM-Routing

Length of output: 252


Use utcoffset() to detect naive datetimes here.

A datetime with tzinfo set can still be naive when utcoffset() returns None; astimezone(timezone.utc) will then reinterpret it instead of preserving the UTC-if-naive contract.

🤖 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 `@litellm/entrypoint.py` around lines 115 - 118, The datetime normalization in
the timezone handling block should detect naive values using utcoffset() instead
of only checking dt.tzinfo. Update the logic around the dt conversion so that
the existing utc-assignment path applies when dt.utcoffset() is None, and only
call astimezone(timezone.utc) for truly aware datetimes; keep the behavior
localized to this datetime conversion branch.

Comment on lines 66 to 70
except Exception as e:
expected = ""
if isinstance(item, dict):
expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "")
predicted = f"ERROR: {str(e)[:50]}"

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Narrow the exception handler to expected benchmark failures.

Line 66 catches every Exception, which can mask code defects and trips Ruff BLE001. Catch the expected data/network/JSON errors instead.

Proposed fix
+import urllib.error
+
 def process_item(item):
     try:
         if not isinstance(item, dict):
             raise TypeError("Item is not a dictionary")
         prompt = item["prompt"]
         expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "")
         predicted = classify(prompt)
-    except Exception as e:
+    except (KeyError, TypeError, urllib.error.URLError, TimeoutError, json.JSONDecodeError) as e:
         expected = ""
         if isinstance(item, dict):
             expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "")
         predicted = f"ERROR: {str(e)[:50]}"
📝 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
except Exception as e:
expected = ""
if isinstance(item, dict):
expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "")
predicted = f"ERROR: {str(e)[:50]}"
except (KeyError, TypeError, urllib.error.URLError, TimeoutError, json.JSONDecodeError) as e:
expected = ""
if isinstance(item, dict):
expected = item.get("tier") or item.get("clf_tier") or item.get("llm_tier", "")
predicted = f"ERROR: {str(e)[:50]}"
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 66-66: 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/benchmark_classifier.py` around lines 66 - 70, The broad exception
handler in the benchmark flow is catching every Exception and masking real bugs.
Narrow the except block in the classifier benchmarking logic (around the item
handling that sets expected and predicted) to only the anticipated data,
network, or JSON-related failures, and keep the fallback error string behavior
for those cases. Use the surrounding benchmark item processing code to preserve
the current expected/predicted assignment while avoiding a blanket Exception
catch.

Source: Linters/SAST tools

@@ -0,0 +1,87 @@
#!/usr/bin/env python3

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "scripts/verification|--ignore" .github/workflows/test.yml

Repository: sheepdestroyer/LLM-Routing

Length of output: 418


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== repo files of interest ==\n'
git ls-files | rg '(^|/)(pytest\.ini|pyproject\.toml|tox\.ini|conftest\.py|test\.yml|test_reasoning_tiers\.py|scripts/verification/)'

printf '\n== pytest config search ==\n'
rg -n --hidden --glob '!**/.git/**' '^\s*\[tool\.pytest\.ini_options\]|\[pytest\]|addopts|testpaths|norecursedirs|python_files|python_functions|python_classes|--ignore' pyproject.toml pytest.ini tox.ini .github/workflows/test.yml scripts/verification 2>/dev/null || true

printf '\n== file details ==\n'
wc -l scripts/verification/test_reasoning_tiers.py 2>/dev/null || true
cat -n scripts/verification/test_reasoning_tiers.py 2>/dev/null | sed -n '1,120p'

Repository: sheepdestroyer/LLM-Routing

Length of output: 5477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for p in [
    Path("pytest.ini"),
    Path("pyproject.toml"),
    Path("tox.ini"),
    Path(".github/workflows/test.yml"),
]:
    if p.exists():
        print(f"\n## {p}")
        print(p.read_text())
PY

Repository: sheepdestroyer/LLM-Routing

Length of output: 1535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## top-level files"
git ls-files | sed -n '1,200p' | rg '^(pyproject\.toml|pytest\.ini|tox\.ini|conftest\.py|\.github/workflows/test\.yml|scripts/verification/test_reasoning_tiers\.py)$'

echo
echo "## scripts/verification contents"
git ls-files scripts/verification

echo
echo "## workflow excerpt"
sed -n '1,120p' .github/workflows/test.yml

Repository: sheepdestroyer/LLM-Routing

Length of output: 1748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in pyproject.toml pytest.ini tox.ini .github/workflows/test.yml; do
  if [ -f "$f" ]; then
    echo "### $f"
    nl -ba "$f" | sed -n '1,220p'
    echo
  fi
done

echo "### pytest-related references"
rg -n --hidden --glob '!**/.git/**' 'norecursedirs|testpaths|python_files|python_functions|python_classes|collect_ignore|collect_ignore_glob|--ignore|pytest' .

Repository: sheepdestroyer/LLM-Routing

Length of output: 219


Exclude scripts/verification/ from pytest collection. scripts/verification/test_reasoning_tiers.py matches pytest’s test_*.py pattern, and neither pytest.ini nor the CI workflow ignores that directory. pytest can pick it up and attempt live gateway requests during the unit-test job.

🤖 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/test_reasoning_tiers.py` at line 1, Exclude the
scripts/verification/ test_reasoning_tiers.py helper from pytest discovery so
the unit-test job does not run live gateway requests. Update the pytest
collection settings and/or the CI workflow to ignore the entire
scripts/verification/ directory, and ensure the change is applied wherever
pytest is invoked. Use scripts/verification/test_reasoning_tiers.py as the
anchor when locating the offending test file.

Comment thread start-stack.sh
Comment on lines +23 to +27
escape_env_val() {
local val="$1"
val="${val//\\/\\\\}"
val="${val//\"/\\\"}"
echo "$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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Escape shell-sourced env values safely.

escape_env_val does not escape $, backticks, or command substitutions, so a copied API key written as VAR="..." can be expanded or executed when /config/.env is sourced by the router command.

🛡️ Proposed fix
 escape_env_val() {
     local val="$1"
-    val="${val//\\/\\\\}"
-    val="${val//\"/\\\"}"
+    val="${val//\\/\\\\}"
+    val="${val//\"/\\\"}"
+    val="${val//\$/\\\$}"
+    val="${val//\`/\\\`}"
     echo "$val"
 }

Also applies to: 84-85, 256-257

🤖 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 23 - 27, `escape_env_val` only escapes
backslashes and double quotes, so shell metacharacters can still be expanded
when `/config/.env` is sourced. Update `escape_env_val` in start-stack.sh to
safely escape or neutralize `$`, backticks, and command substitution syntax
before writing env values, and make sure the same escaping is used at all call
sites that generate the sourced env file.

Comment thread start-stack.sh
Comment on lines +84 to +85
escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE"

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

Lock down .env immediately after writing the OpenRouter key.

This append can create $ENV_FILE before the later permission setup, leaving the API key with default umask permissions if the script exits early.

🛡️ Proposed fix
         escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
         echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE"
+        chmod 600 "$ENV_FILE"
📝 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
escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE"
escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE"
chmod 600 "$ENV_FILE"
🤖 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 84 - 85, The OpenRouter API key write in
start-stack.sh can leave the newly created $ENV_FILE with default umask
permissions if the script exits before the later chmod step. Update the flow
around the escaped_key write so the file is locked down immediately after
appending OPENROUTER_API_KEY, using the same permission-setting approach already
used later in the script, and keep the fix localized near the env-file write
path.

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