Skip to content

chore: address CodeRabbit and PR #27 review feedback#28

Closed
sheepdestroyer wants to merge 24 commits into
masterfrom
feat/pr27-review-fixes
Closed

chore: address CodeRabbit and PR #27 review feedback#28
sheepdestroyer wants to merge 24 commits into
masterfrom
feat/pr27-review-fixes

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Overview

This PR addresses all CodeRabbit and PR #27 review feedback. It introduces improvements to database URL handling, positive validation checks for cooldowns, user-specific session ID hashes to prevent chat crossovers, lifespan globals cleanup, mock server body consumption, metrics validation in tests, and reorganizes integration testing documentation.

Key Changes

Router / Gateway

  • DATABASE_URL checks: Avoided hardcoded database credentials by retrieving DATABASE_URL as optional (None default) and checking if set before proceeding with _purge_stale_deployments in both roster sync and Ollama registration lifespans.
  • Positive Cooldown validation: Added validation to raise an exception if OLLAMA_COOLDOWN_SECONDS is parsed as non-positive, ensuring it falls back to the default of 300.
  • Session ID Hashing: Included user/session identifiers from requests (user, session_id, session body fields or x-user-id, x-session-id, x-user headers) in the hash fingerprint. Left session_id as None when no key exists to prevent stateless chat crossovers.
  • Lifespan Shutdown Resets: Reset global variables _http_client and _redis_client to None upon closing them in the application lifespan teardown.

Verification Mocks & Test Scripts

  • mock_rate_limit_server.py: Consumed incoming request bodies using self.rfile and calculated/added explicit Content-Length headers in the 429 response body.
  • verify_direct_ollama_cooldown.py: Updated assertions to verify that cooled requests are rejected by the router with a 429 status code, rather than expecting a zero difference in request metrics. Increased timeout limits to 120s.
  • verify_ollama_cooldown.py: Updated assertions to verify that cooled requests successfully fallback to openrouter-auto (returning the underlying model ID) rather than expecting diff == 0 metrics. Increased timeout limits to 120s.
  • test_antigravity.py: Added check=True and raise handling to propagate subprocess exit code failures to the test suite.

Documentation

  • README.md:
    • Reorganized the flat list of integration tests into categories for better discoverability.
    • Documented that routing agent-reasoning-core to the Pro tier (ollama-deepseek-v4-pro) is an intentional design choice to guarantee high-quality reasoning.

Verification

  • Verified connection tests in test_antigravity.py now fail properly on command failures.
  • Run test_classifier_accuracy.py which passes with 100% accuracy.
  • Run verify_direct_ollama_cooldown.py and verify_ollama_cooldown.py which successfully pass and validate the cooldown and fallback mechanisms.

Summary by CodeRabbit

  • New Features

    • Added Ollama cooldown mechanism with automatic fallback behavior during router outages.
    • Implemented Redis-backed persistence for circuit breaker state and cooldown tracking.
  • Documentation

    • Updated routing modes and telemetry documentation, including Ollama cooldown behavior.
    • Added scripts directory documentation with verification and automation procedures.
  • Chores

    • Updated Dependabot configuration schedule.
    • Modified GitHub Actions workflow to install Python dependencies and support multiple branches.
    • Standardized paths in deployment and script configuration to use dynamic resolution.

LiteLLM Community Edition's deployment cooldown is unreliable for
single-deployment model groups and fallback-target groups. The previous
approach (multi-deployment replicas, allowed_fails_policy) did not
reliably cool down llm-routing-ollama, causing crashloops when Ollama
was rate-limited.

New approach: the triage router manages Ollama cooldowns internally.
When Ollama fails (429/502/503), the router activates a 5-minute
cooldown (configurable via OLLAMA_COOLDOWN_SECONDS env var). During
cooldown, all Ollama requests are immediately rejected:
- Auto modes: silently fall back to the free tier
- Direct/fallback mode: return 429 so LiteLLM skips to openrouter-auto

Changes:
- router/main.py: Add _ollama_cooldown_until state, cooldown check
  before Ollama proxy call, and activation on failure. Add Prometheus
  metrics (ollama_cooldown_active, ollama_cooldown_remaining_seconds).
- litellm/config.yaml: Remove test-fallback-model, remove duplicate
  llm-routing-ollama deployment (127.0.0.2), restore Ollama api_base
  to production (api.ollama.com), remove enterprise-only
  allowed_fails_policy.
- README.md: Update sequence diagrams, Fallback and Cooldown Behavior
  section, and metrics table to document router-side cooldown.
…ing in sed, expected model asserts in tests, synced metrics documentation
…fecycles, and handle transient exception status codes
… breaker Valkey calls, generic error detail messages, SSE stream token counting, secure YAML rendering, and robust verification script exit codes
- Decouple agy_proxy from main by introducing CooldownPersistence protocol and passing client/cooldown_persistence parameters.
- Manage http client lifetime cleanly in agy_proxy.
- Reset Valkey client state and cache last init attempt on exceptions to enforce the 5s retry cooldown.
- Add isinstance(msg, dict) guards to prevent crashes on malformed payloads.
- Use incremental UTF-8 decoder in stream generator to prevent character corruption.
- Remove dead should_close_client variables and conditions.
- Guard test_antigravity_connection when agentapi is missing.
- Fix typo in README.
… Table

- ci: add httpx to test workflow pip install for test_a2_verify.py

- config: add public_model_groups list to litellm_settings so all agent-*,
  openrouter-auto, llm-routing-ollama, and ollama-deepseek-* groups appear
  in the LiteLLM Model Hub Table UI

- config: add full model_info to all model_list entries (supports_vision,
  supports_reasoning, supports_function_calling, mode, max_tokens,
  max_input_tokens, is_public_model_group)

- config: set llm-routing-ollama and ollama-deepseek-v4-{pro,flash} context
  windows to 512K (524288 tokens), up from 131K/262K

- config: add DeepSeek API equivalent per-token costs to ollama models for
  Langfuse cost tracking:
    ollama-deepseek-v4-pro:   $1.74/1M input, $3.48/1M output
    ollama-deepseek-v4-flash: $0.14/1M input, $0.28/1M output

- router: enrich /model/new roster sync payload with model_info (features,
  context length from OpenRouter API, is_public_model_group) for all
  dynamically registered agent-* tiers

- router: add _register_ollama_models_in_db() — registers ollama-deepseek
  models via /model/new at startup so their model_info wins over LiteLLM's
  internal ollama_chat provider lookup (which returns null/false for unknown
  models in /model_group/info aggregation)
…t capabilities, align returned context lengths, and refactor verify scripts to httpx)
…_status in metrics fetch, detailed HTTP routing diagnostics, and defensive config type checking)

@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 Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 47 minutes and 17 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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

🚦 How do rate limits work?

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

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, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1f058c1-5013-4b1a-b675-8badfc2271e8

📥 Commits

Reviewing files that changed from the base of the PR and between 5424cda and 1619e25.

📒 Files selected for processing (4)
  • litellm/config.yaml
  • router/main.py
  • start-stack.sh
  • test_antigravity.py
📝 Walkthrough

Walkthrough

Adds router-side Ollama cooldown management with Valkey/Redis-backed persistence for circuit breaker and cooldown state. Refactors the AGY proxy to accept a shared HTTP client and cooldown persistence hook. Updates LiteLLM config with env-var secrets, rewired fallback chains, and new Ollama/agent-tier model entries. Adds three Ollama verification scripts, a mock 429 server, and applies path portability fixes across scripts, CI, and the stack launcher.

Changes

Ollama Cooldown, Valkey Persistence, and Router Hardening

Layer / File(s) Summary
Valkey persistence for circuit breaker state
router/circuit_breaker.py
Adds sync_from_valkey and save_to_valkey to PerModelBreaker and DualCircuitBreaker, enabling Valkey hash-backed hydration and persistence of breaker tier, cooldown, and trip fields.
AGY proxy: shared HTTP client and cooldown persistence
router/agy_proxy.py
Introduces CooldownPersistence Protocol, injects shared httpx.AsyncClient into _run_agy_print, extends try_agy_proxy with optional client and cooldown_persistence parameters, and adds sync/save hooks at quota failure and success points.
Router: Valkey singletons, token estimation, and Ollama cooldown state
router/main.py
Adds aioredis import, lazy get_redis/get_http_client singletons, estimate_prompt_tokens, sync_cooldowns_from_valkey/save_cooldowns_to_valkey, ValkeyCooldownPersistence adapter, and _ollama_cooldown_until state with OLLAMA_COOLDOWN_SECONDS env init.
Router: roster sync and Ollama DB registration
router/main.py
Adds _purge_stale_deployments, enhances sync_adaptive_router_roster to track per-model context_length/supported_parameters, updates OpenRouter registration, and adds _register_ollama_models_in_db to load/purge/re-register Ollama DeepSeek entries in LiteLLM DB.
Router: chat completion routing, Ollama execution, and metrics
router/main.py
Updates chat_completions to sync cooldowns early, expands llm-routing-ollama handling, refactors Ollama execute_proxy with Langfuse span/max_tokens clamping/cooldown enforcement, extends /metrics with cooldown gauges, and hardens /visualizer and save-annotations concurrency.
LiteLLM config: env secrets, fallbacks, and agent tier models
litellm/config.yaml
Moves master_key to env var, rewrites fallback chains through llm-routing-ollama and openrouter-auto, adds full Ollama DeepSeek entries with api_base/api_key/costs, replaces agent tier safety-net entries with specific OpenRouter models, and sets allowed_fails to 0.
Pod and container wiring
pod.yaml, router/Dockerfile
Adds LITELLM_CONFIG_PATH and DATABASE_URL env vars and litellm-config volume mount to the triage router container; adds redis to the router Dockerfile pip install.
start-stack.sh: dynamic paths and runtime pod YAML rendering
start-stack.sh
Derives WORKDIR dynamically, adds LITELLM_MASTER_KEY generation/validation, introduces render_pod_yaml() to rewrite path placeholders and inject master key at deploy time, and switches to router/Dockerfile with podman play kube via stdin.
Ollama cooldown and routing verification scripts
scripts/verification/*
Adds mock_rate_limit_server.py (HTTP 429 server), verify_direct_ollama_cooldown.py, verify_ollama_cooldown.py, and verify_ollama_routing.py to verify cooldown short-circuit and routing fallback behavior end-to-end.
Scripts portability and dataset fixes
scripts/backup.sh, scripts/extract_gapfill.py, scripts/extract_prompts.py, scripts/reclassify_all.py, scripts/retry_errors.py, test_antigravity.py
Makes paths script-relative, narrows JSON exception handling, fixes loop variable, removes unused prompt_map, switches retry input to classified_dataset.json with atomic write, and updates antigravity test to use dynamic path with pytest.skip.
README, scripts docs, and free model roster
README.md, scripts/README.md, router/free_models_roster.json, sync_gemini_token.py, test_classifier_accuracy.py, hello.py, test_goose.py
Updates README with cooldown sequence diagram, revised routing-mode and dispatch tables, expanded LiteLLM fallback docs, and new Ollama cooldown behavior section. Adds scripts/README.md. Adds cohere/north-mini-code:free to roster. Applies home-dir portability fixes. Removes stray print statements.
CI workflow and Dependabot schedule
.github/workflows/test.yml, .github/dependabot.yml
Removes master-branch push filter, adds httpx==0.28.1 install step, and shifts Dependabot docker schedule time.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant chat_completions as chat_completions (router/main.py)
  participant Valkey
  participant try_agy_proxy as try_agy_proxy (agy_proxy.py)
  participant LiteLLM as LiteLLM execute_proxy

  Client->>chat_completions: POST /v1/chat/completions (llm-routing-ollama or auto-ollama)
  chat_completions->>Valkey: sync_cooldowns_from_valkey (circuit_breaker hashes)
  Valkey-->>chat_completions: breaker state hydrated

  alt AGY tier eligible
    chat_completions->>try_agy_proxy: client=get_http_client(), cooldown_persistence=ValkeyCooldownPersistence()
    try_agy_proxy->>Valkey: cooldown_persistence.sync()
    try_agy_proxy-->>chat_completions: SSE stream or None (tier exhausted)
    try_agy_proxy->>Valkey: cooldown_persistence.save() on quota/success
  end

  alt Ollama tier eligible
    chat_completions->>LiteLLM: execute_proxy(model_name) with clamped max_tokens
    LiteLLM-->>chat_completions: response or transient failure
    alt Transient failure
      chat_completions->>Valkey: save_cooldowns_to_valkey (set _ollama_cooldown_until)
      chat_completions-->>Client: 429 (direct) or cascade fallback (auto)
    else Success
      chat_completions-->>Client: streamed response
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • sheepdestroyer/LLM-Routing#4: Shares direct code-level overlap in router/main.py visualizer annotation handling and router/circuit_breaker.py cooldown/tier logic, which this PR significantly extends.
  • sheepdestroyer/LLM-Routing#17: Both PRs modify .github/dependabot.yml docker ecosystem scheduling configuration.

Poem

🐇 A cooldown hops in, the Ollama rests,
Valkey remembers each breaker's requests.
The router now renders its pod YAML live,
No hardcoded paths — watch the rabbits thrive!
Five minutes of peace, then back to the queue,
With shared HTTP clients and metrics brand new. 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: address CodeRabbit and PR #27 review feedback' clearly describes the main purpose of the changeset—addressing review feedback—which is relevant to the extensive modifications across database handling, validation, session management, and testing throughout the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pr27-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 and usage tips.

@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 robust router-side cooldown and fallback mechanism for Ollama backends, integrates Valkey/Redis for shared state persistence of cooldowns and circuit breakers, and refactors configuration handling and deployment scripts to use dynamic paths and environment variables. Feedback on these changes highlights a few critical issues: a database password mismatch in pod.yaml that will cause startup failures, a session fingerprinting bug in router/main.py that breaks session continuity, an overestimation of completion tokens in the fallback telemetry, and a potential crash in test_antigravity.py when running the test script directly outside of pytest.

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 pod.yaml
Comment on lines +95 to +96
- name: DATABASE_URL
value: postgresql://postgres:***@127.0.0.1:5432/postgres

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 DATABASE_URL password is set to *** in the environment variables for the llm-triage-router container. However, the postgres-db container is configured with the password postgres-local-pw-2026. Since start-stack.sh does not replace *** in the database URL, this will cause database connection and authentication failures on startup.

    - name: DATABASE_URL
      value: postgresql://postgres:postgres-local-pw-2026@127.0.0.1:5432/postgres

Comment thread router/main.py Outdated
Comment on lines 1555 to 1567
if user_key and len(messages) >= 2:
import hashlib
fingerprint_parts = []
fingerprint_parts = [str(user_key)]
for msg in messages[:4]:
c = msg.get("content", "") or ""
if c:
if not isinstance(msg, dict):
continue
c = msg.get("content") or ""
if isinstance(c, list):
c = "".join(block.get("text") or "" for block in c if isinstance(block, dict) and block.get("type") == "text")
if isinstance(c, str) and c:
fingerprint_parts.append(c[:200])
fingerprint = "|".join(fingerprint_parts)
session_id = hashlib.md5(fingerprint.encode()).hexdigest()

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 session fingerprinting logic uses messages[:4] to generate the session_id. Because the number of messages included in the hash increases on each turn (from 1 to 3, then to 5, etc.), the computed session_id will change on the first few turns of the conversation. This breaks session continuity in agy_proxy because the session ID won't match the previously saved session in _session_store.

To ensure a stable session_id across the entire conversation, the fingerprint should be anchored to the user_key and the very first user message, which remains constant throughout the session.

Suggested change
if user_key and len(messages) >= 2:
import hashlib
fingerprint_parts = []
fingerprint_parts = [str(user_key)]
for msg in messages[:4]:
c = msg.get("content", "") or ""
if c:
if not isinstance(msg, dict):
continue
c = msg.get("content") or ""
if isinstance(c, list):
c = "".join(block.get("text") or "" for block in c if isinstance(block, dict) and block.get("type") == "text")
if isinstance(c, str) and c:
fingerprint_parts.append(c[:200])
fingerprint = "|".join(fingerprint_parts)
session_id = hashlib.md5(fingerprint.encode()).hexdigest()
if user_key and len(messages) >= 1:
import hashlib
# Find the first user message to use as a stable session anchor
first_user_content = ""
for msg in messages:
if isinstance(msg, dict) and msg.get("role") == "user":
c = msg.get("content") or ""
if isinstance(c, list):
c = "".join(block.get("text") or "" for block in c if isinstance(block, dict) and block.get("type") == "text")
if isinstance(c, str):
first_user_content = c
break
fingerprint_parts = [str(user_key), first_user_content[:200]]
fingerprint = "|".join(fingerprint_parts)
session_id = hashlib.md5(fingerprint.encode()).hexdigest()

Comment thread router/main.py Outdated
Comment on lines +1915 to +1916
prompt_tokens = usage.get("prompt_tokens", estimate_prompt_tokens(body_to_send))
completion_tokens = usage.get("completion_tokens", len(json.dumps(resp_json)) // 4)

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

Using len(json.dumps(resp_json)) // 4 as a fallback for completion_tokens will severely overestimate the token count because it serializes the entire OpenAI-compatible response JSON (including prompt echo, choices metadata, system fingerprint, etc.) rather than just the generated message content.

A more accurate fallback is to extract the content of the first choice and estimate its length, or default to 0 if no choices are present.

Suggested change
prompt_tokens = usage.get("prompt_tokens", estimate_prompt_tokens(body_to_send))
completion_tokens = usage.get("completion_tokens", len(json.dumps(resp_json)) // 4)
prompt_tokens = usage.get("prompt_tokens") or estimate_prompt_tokens(body_to_send)
choices = resp_json.get("choices") or []
fallback_completion = (len(choices[0].get("message", {}).get("content") or "") // 4) if choices else 0
completion_tokens = usage.get("completion_tokens") or fallback_completion

Comment thread test_antigravity.py Outdated
Comment on lines +16 to +22
if not os.path.exists(agentapi_path):
try:
import pytest
pytest.skip(f"agentapi binary not found at {agentapi_path}; skipping health check")
except ImportError:
print(f"agentapi binary not found at {agentapi_path}; skipping health check")
return

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

Calling pytest.skip() outside of an active pytest test session (for example, when running python test_antigravity.py directly) raises a pytest.exceptions.Failed exception, causing the script to crash.

To prevent this, we should only call pytest.skip if the script is not being run as the main module.

    if not os.path.exists(agentapi_path):
        print(f"agentapi binary not found at {agentapi_path}; skipping health check")
        if __name__ != "__main__":
            try:
                import pytest
                pytest.skip(f"agentapi binary not found at {agentapi_path}; skipping health check")
            except ImportError:
                pass
        return

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
router/agy_proxy.py (1)

319-323: 💤 Low value

Silent exception swallowing hides connection issues.

Exceptions when reading the first line are silently discarded. While the first_line is None check handles this gracefully, logging at debug level would aid troubleshooting transient stream failures.

🔧 Suggested improvement
                 try:
                     lines_iter = r.aiter_lines()
                     first_line = await anext(lines_iter)
-                except (StopAsyncIteration, Exception):
-                    pass
+                except StopAsyncIteration:
+                    pass  # Empty stream, handled below
+                except Exception as e:
+                    logger.debug(f"Failed to read first stream line: {e}")
🤖 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/agy_proxy.py` around lines 319 - 323, The except clause in the code
block that reads the first line from the response stream (where lines_iter is
created from r.aiter_lines() and anext is called) silently swallows exceptions
without logging. Replace the pass statement with a debug-level log message that
captures and logs the actual exception that occurred, so transient stream
failures can be properly debugged while still maintaining graceful error
handling.

Source: Linters/SAST tools

router/main.py (1)

1566-1568: ⚡ Quick win

Use a collision-resistant hash for session fingerprinting.

Line 1567 uses MD5 for a key that isolates session continuity and prevents chat crossover. Switching to SHA-256 materially lowers collision risk for this boundary.

Suggested fix
-                session_id = hashlib.md5(fingerprint.encode()).hexdigest()
+                session_id = hashlib.sha256(fingerprint.encode("utf-8")).hexdigest()
🤖 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/main.py` around lines 1566 - 1568, The session_id generation in the
fingerprinting logic currently uses MD5 hashing which has known collision
vulnerabilities. Replace the hashlib.md5() call with hashlib.sha256() to improve
collision resistance for session boundary isolation. Locate the line where
session_id is assigned using hashlib.md5(fingerprint.encode()).hexdigest() and
change the hash algorithm to SHA-256 while keeping the rest of the logic intact.
start-stack.sh (1)

300-313: 💤 Low value

The broad string replacement may over-match in unexpected contexts.

The render_pod_yaml() function uses simple str.replace() which replaces all occurrences. The pattern /home/gpav/ could match unintended locations if it appears in configuration values or comments. Consider using more targeted replacements or YAML-aware templating if this becomes an issue.

Additionally, if LITELLM_MASTER_KEY contains YAML-special characters (though unlikely with the sk-... format), it could break parsing. The current approach is acceptable for the controlled key format.

🤖 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 300 - 313, The render_pod_yaml() function uses
broad str.replace() calls that may over-match in unintended contexts such as
within configuration values or comments. To fix this, implement more targeted
replacements by either using regex with word boundaries or context-aware
patterns, or consider switching to YAML-aware templating that respects the
structure of the pod.yaml file. This will prevent the patterns like /home/gpav/
from being replaced in unexpected locations while maintaining the intended
substitutions for the WORKDIR, HOME, and LITELLM_MASTER_KEY values.
pod.yaml (1)

50-51: ⚡ Quick win

Consider externalizing OLLAMA_API_KEY similar to LITELLM_MASTER_KEY.

The LITELLM_MASTER_KEY is externalized via os.environ in the LiteLLM config and replaced at deploy time by render_pod_yaml(), but OLLAMA_API_KEY is hardcoded in the pod YAML template. For consistent secrets management, consider storing this in .env and replacing it dynamically, especially if this repository may be shared or the key rotated.

If this is intentional for local development simplicity, a brief comment in the YAML would help clarify.

Also applies to: 107-108

🤖 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 `@pod.yaml` around lines 50 - 51, The OLLAMA_API_KEY in the pod.yaml template
is hardcoded, while LITELLM_MASTER_KEY is externalized through os.environ and
replaced dynamically at deploy time via render_pod_yaml(). Either externalize
OLLAMA_API_KEY by replacing the hardcoded value with an os.environ reference
(similar to how LITELLM_MASTER_KEY is handled) and ensure render_pod_yaml()
replaces it at deployment, or add a clarifying comment in the YAML explaining
that this hardcoded value is intentional for local development purposes. This
ensures consistent secrets management across the configuration.
🤖 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 `@router/agy_proxy.py`:
- Around line 361-398: The nested async generator function token_generator
captures loop variables actual_tier_idx and lines_iter by reference, which can
cause unexpected behavior as these variables are redefined in each loop
iteration. To fix this, modify the token_generator function signature to include
these loop variables as default arguments (e.g.,
actual_tier_idx=actual_tier_idx, lines_iter=lines_iter) so their current values
are captured at the time the generator is defined, not when it executes. This
ensures each generator instance has its own fixed reference to these values and
satisfies linter rules like B023.

In `@router/main.py`:
- Around line 1416-1422: Add the model name "llm-routing-ollama" to the
DIRECT_TIERS set defined in the diff so that it bypasses the classifier just
like the other direct tier models ("agent-simple-core", "agent-medium-core",
etc.). This will prevent direct Ollama requests from going through
classify_request(...) at line 1449, eliminating unnecessary classifier latency
and dependency coupling for these direct routing calls.

In `@scripts/verification/verify_direct_ollama_cooldown.py`:
- Around line 60-67: The code is using string matching to check for "429" in the
error message (response_msg2), which is fragile and formatting-dependent.
Instead, directly access the HTTP status code from the exception object: use
e.response.status_code and compare it directly to the integer 429. This approach
is more reliable and doesn't depend on how the error message is formatted.
Update the check at line 106-107 to compare the status code directly rather than
searching for "429" in the response text.

---

Nitpick comments:
In `@pod.yaml`:
- Around line 50-51: The OLLAMA_API_KEY in the pod.yaml template is hardcoded,
while LITELLM_MASTER_KEY is externalized through os.environ and replaced
dynamically at deploy time via render_pod_yaml(). Either externalize
OLLAMA_API_KEY by replacing the hardcoded value with an os.environ reference
(similar to how LITELLM_MASTER_KEY is handled) and ensure render_pod_yaml()
replaces it at deployment, or add a clarifying comment in the YAML explaining
that this hardcoded value is intentional for local development purposes. This
ensures consistent secrets management across the configuration.

In `@router/agy_proxy.py`:
- Around line 319-323: The except clause in the code block that reads the first
line from the response stream (where lines_iter is created from r.aiter_lines()
and anext is called) silently swallows exceptions without logging. Replace the
pass statement with a debug-level log message that captures and logs the actual
exception that occurred, so transient stream failures can be properly debugged
while still maintaining graceful error handling.

In `@router/main.py`:
- Around line 1566-1568: The session_id generation in the fingerprinting logic
currently uses MD5 hashing which has known collision vulnerabilities. Replace
the hashlib.md5() call with hashlib.sha256() to improve collision resistance for
session boundary isolation. Locate the line where session_id is assigned using
hashlib.md5(fingerprint.encode()).hexdigest() and change the hash algorithm to
SHA-256 while keeping the rest of the logic intact.

In `@start-stack.sh`:
- Around line 300-313: The render_pod_yaml() function uses broad str.replace()
calls that may over-match in unintended contexts such as within configuration
values or comments. To fix this, implement more targeted replacements by either
using regex with word boundaries or context-aware patterns, or consider
switching to YAML-aware templating that respects the structure of the pod.yaml
file. This will prevent the patterns like /home/gpav/ from being replaced in
unexpected locations while maintaining the intended substitutions for the
WORKDIR, HOME, and LITELLM_MASTER_KEY values.
🪄 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: 816e9985-c6bc-4ccf-bd3e-b140f31188d1

📥 Commits

Reviewing files that changed from the base of the PR and between 8dbd38b and 5424cda.

📒 Files selected for processing (26)
  • .github/dependabot.yml
  • .github/workflows/test.yml
  • README.md
  • hello.py
  • litellm/config.yaml
  • pod.yaml
  • router/Dockerfile
  • router/agy_proxy.py
  • router/circuit_breaker.py
  • router/free_models_roster.json
  • router/main.py
  • scripts/README.md
  • scripts/backup.sh
  • scripts/extract_gapfill.py
  • scripts/extract_prompts.py
  • scripts/reclassify_all.py
  • scripts/retry_errors.py
  • scripts/verification/mock_rate_limit_server.py
  • scripts/verification/verify_direct_ollama_cooldown.py
  • scripts/verification/verify_ollama_cooldown.py
  • scripts/verification/verify_ollama_routing.py
  • start-stack.sh
  • sync_gemini_token.py
  • test_antigravity.py
  • test_classifier_accuracy.py
  • test_goose.py
💤 Files with no reviewable changes (3)
  • hello.py
  • test_goose.py
  • scripts/reclassify_all.py

Comment thread router/agy_proxy.py
Comment on lines +361 to +398
async def token_generator(stream_resp, httpx_client, initial_line, current_conv_id, close_client):
"""Asynchronously yields tokens from the agy daemon stream and manages session state updates."""
# Yield the initial token if it was a token
init_data = json.loads(initial_line)
if init_data.get("type") == "token" and init_data.get("content"):
yield init_data["content"]
elif init_data.get("type") == "conversation_id" and init_data.get("id"):
current_conv_id = init_data["id"]
if session_id:
_session_store[session_id] = {
"conversation_id": current_conv_id,
"current_tier_index": actual_tier_idx,
}

try:
async for line in lines_iter:
if not line.strip():
continue
data = json.loads(line)
if data.get("type") == "token" and data.get("content"):
yield data["content"]
elif data.get("type") == "conversation_id" and data.get("id"):
current_conv_id = data["id"]
if session_id:
_session_store[session_id] = {
"conversation_id": current_conv_id,
"current_tier_index": actual_tier_idx,
}
finally:
await stream_resp.aclose()
if close_client:
await httpx_client.aclose()

stream_returned = True
return {
"stream": token_generator(r, client, first_line, last_conv_id, should_close_client),
"model": tier["model_name"]
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Loop variables captured by reference in nested generator.

actual_tier_idx and lines_iter are referenced inside token_generator but defined in the enclosing loop. Python closures capture variables by reference, not value. Currently safe because the function returns immediately after defining the generator, but this is fragile and flagged by linters (B023).

Bind loop variables as default arguments to capture their current values.

🔧 Proposed fix to bind loop variables
-                async def token_generator(stream_resp, httpx_client, initial_line, current_conv_id, close_client):
+                async def token_generator(stream_resp, httpx_client, initial_line, current_conv_id, close_client,
+                                          _actual_tier_idx=actual_tier_idx, _lines_iter=lines_iter):
                     """Asynchronously yields tokens from the agy daemon stream and manages session state updates."""
                     # Yield the initial token if it was a token
                     init_data = json.loads(initial_line)
                     if init_data.get("type") == "token" and init_data.get("content"):
                         yield init_data["content"]
                     elif init_data.get("type") == "conversation_id" and init_data.get("id"):
                         current_conv_id = init_data["id"]
                         if session_id:
                             _session_store[session_id] = {
                                 "conversation_id": current_conv_id,
-                                "current_tier_index": actual_tier_idx,
+                                "current_tier_index": _actual_tier_idx,
                             }
                     
                     try:
-                        async for line in lines_iter:
+                        async for line in _lines_iter:
                             if not line.strip():
                                 continue
                             data = json.loads(line)
                             if data.get("type") == "token" and data.get("content"):
                                 yield data["content"]
                             elif data.get("type") == "conversation_id" and data.get("id"):
                                 current_conv_id = data["id"]
                                 if session_id:
                                     _session_store[session_id] = {
                                         "conversation_id": current_conv_id,
-                                        "current_tier_index": actual_tier_idx,
+                                        "current_tier_index": _actual_tier_idx,
                                     }
📝 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
async def token_generator(stream_resp, httpx_client, initial_line, current_conv_id, close_client):
"""Asynchronously yields tokens from the agy daemon stream and manages session state updates."""
# Yield the initial token if it was a token
init_data = json.loads(initial_line)
if init_data.get("type") == "token" and init_data.get("content"):
yield init_data["content"]
elif init_data.get("type") == "conversation_id" and init_data.get("id"):
current_conv_id = init_data["id"]
if session_id:
_session_store[session_id] = {
"conversation_id": current_conv_id,
"current_tier_index": actual_tier_idx,
}
try:
async for line in lines_iter:
if not line.strip():
continue
data = json.loads(line)
if data.get("type") == "token" and data.get("content"):
yield data["content"]
elif data.get("type") == "conversation_id" and data.get("id"):
current_conv_id = data["id"]
if session_id:
_session_store[session_id] = {
"conversation_id": current_conv_id,
"current_tier_index": actual_tier_idx,
}
finally:
await stream_resp.aclose()
if close_client:
await httpx_client.aclose()
stream_returned = True
return {
"stream": token_generator(r, client, first_line, last_conv_id, should_close_client),
"model": tier["model_name"]
}
async def token_generator(stream_resp, httpx_client, initial_line, current_conv_id, close_client,
_actual_tier_idx=actual_tier_idx, _lines_iter=lines_iter):
"""Asynchronously yields tokens from the agy daemon stream and manages session state updates."""
# Yield the initial token if it was a token
init_data = json.loads(initial_line)
if init_data.get("type") == "token" and init_data.get("content"):
yield init_data["content"]
elif init_data.get("type") == "conversation_id" and init_data.get("id"):
current_conv_id = init_data["id"]
if session_id:
_session_store[session_id] = {
"conversation_id": current_conv_id,
"current_tier_index": _actual_tier_idx,
}
try:
async for line in _lines_iter:
if not line.strip():
continue
data = json.loads(line)
if data.get("type") == "token" and data.get("content"):
yield data["content"]
elif data.get("type") == "conversation_id" and data.get("id"):
current_conv_id = data["id"]
if session_id:
_session_store[session_id] = {
"conversation_id": current_conv_id,
"current_tier_index": _actual_tier_idx,
}
finally:
await stream_resp.aclose()
if close_client:
await httpx_client.aclose()
stream_returned = True
return {
"stream": token_generator(r, client, first_line, last_conv_id, should_close_client),
"model": tier["model_name"]
}
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 372-372: Function definition does not bind loop variable actual_tier_idx

(B023)


[warning] 376-376: Function definition does not bind loop variable lines_iter

(B023)


[warning] 387-387: Function definition does not bind loop variable actual_tier_idx

(B023)

🤖 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/agy_proxy.py` around lines 361 - 398, The nested async generator
function token_generator captures loop variables actual_tier_idx and lines_iter
by reference, which can cause unexpected behavior as these variables are
redefined in each loop iteration. To fix this, modify the token_generator
function signature to include these loop variables as default arguments (e.g.,
actual_tier_idx=actual_tier_idx, lines_iter=lines_iter) so their current values
are captured at the time the generator is defined, not when it executes. This
ensures each generator instance has its own fixed reference to these values and
satisfies linter rules like B023.

Source: Linters/SAST tools

Comment thread router/main.py
Comment on lines 1416 to 1422
# Known tier names that can be routed directly (bypass classifier)
DIRECT_TIERS = {
"agent-simple-core", "agent-medium-core",
"agent-complex-core", "agent-reasoning-core",
"agent-advanced-core",
"llm-routing-agy", "llm-routing-ollama",
"llm-routing-agy",
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Direct Ollama requests are still classifier-gated.

Line 1449 routes llm-routing-ollama through classify_request(...), so this “direct” model path still depends on classifier latency/availability (including its long timeout behavior) instead of bypassing triage like other direct tiers at Line 1416. That can add avoidable delay and failure coupling for direct Ollama calls.

Suggested fix
-    if client_model in AUTO_MODELS or client_model == "llm-routing-ollama":
+    if client_model in AUTO_MODELS:
         # Full pipeline: classify → route to best tier
         bypass_cache = request.headers.get("x-bypass-cache") == "true"
         target_model, triage_latency, was_cache_hit, raw_classification = await classify_request(
             last_user_message, bypass_cache=bypass_cache, langfuse_trace_id=langfuse_trace_id
         )
         logger.info(f"Triage decision (auto/gated): Routing to -> '{target_model}'")
     DIRECT_TIERS = {
         "agent-simple-core", "agent-medium-core",
         "agent-complex-core", "agent-reasoning-core",
         "agent-advanced-core",
         "llm-routing-agy",
+        "llm-routing-ollama",
     }

Also applies to: 1449-1454

🤖 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/main.py` around lines 1416 - 1422, Add the model name
"llm-routing-ollama" to the DIRECT_TIERS set defined in the diff so that it
bypasses the classifier just like the other direct tier models
("agent-simple-core", "agent-medium-core", etc.). This will prevent direct
Ollama requests from going through classify_request(...) at line 1449,
eliminating unnecessary classifier latency and dependency coupling for these
direct routing calls.

Comment on lines +60 to +67
except httpx.HTTPStatusError as e:
err_msg = f"{e} - {e.response.text}"
print(f"Failed in {time.time() - start_time:.1f}s: {err_msg}")
return False, err_msg
except httpx.HTTPError as e:
err_msg = str(e)
print(f"Failed in {time.time() - start_time:.1f}s: {err_msg}")
return False, err_msg

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert HTTP status code directly instead of matching error text.

Line 106 relies on "429" in response_msg2, which is formatting-dependent and can make this verification flaky. Return and check e.response.status_code explicitly.

Suggested fix
-def send_litellm_request(model: str, prompt: str):
+def send_litellm_request(model: str, prompt: str):
@@
-        return True, model_returned
+        return True, 200, model_returned
@@
-        return False, err_msg
+        return False, e.response.status_code, err_msg
@@
-        return False, err_msg
+        return False, None, err_msg
@@
-        return False, err_msg
+        return False, None, err_msg
@@
-    success2, response_msg2 = send_litellm_request("llm-routing-ollama", "Design a distributed pub/sub system with Valkey and describe failover states")
+    success2, status2, response_msg2 = send_litellm_request("llm-routing-ollama", "Design a distributed pub/sub system with Valkey and describe failover states")
@@
-        if not success2 and "429" in response_msg2:
+        if not success2 and status2 == 429:

Also applies to: 106-107

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

In `@scripts/verification/verify_direct_ollama_cooldown.py` around lines 60 - 67,
The code is using string matching to check for "429" in the error message
(response_msg2), which is fragile and formatting-dependent. Instead, directly
access the HTTP status code from the exception object: use
e.response.status_code and compare it directly to the integer 429. This approach
is more reliable and doesn't depend on how the error message is formatted.
Update the check at line 106-107 to compare the status code directly rather than
searching for "429" in the response text.

sheepdestroyer added a commit that referenced this pull request Jun 20, 2026
* Configure gated Ollama routing and set llm-routing-ollama as free tier fallback

* Address code review comments: Fix annotations race condition and handle null content safely

* Address new code reviews: Break Ollama fallback loop, use env key, and fix SAST/lint comments

* fix: sanitize triage router 429 detail and force immediate LiteLLM cooldown for llm-routing-ollama on rate limit

* docs: update fallback diagrams and cooldown behavior for Ollama models

* fix: implement router-side Ollama cooldown to prevent crashloop

LiteLLM Community Edition's deployment cooldown is unreliable for
single-deployment model groups and fallback-target groups. The previous
approach (multi-deployment replicas, allowed_fails_policy) did not
reliably cool down llm-routing-ollama, causing crashloops when Ollama
was rate-limited.

New approach: the triage router manages Ollama cooldowns internally.
When Ollama fails (429/502/503), the router activates a 5-minute
cooldown (configurable via OLLAMA_COOLDOWN_SECONDS env var). During
cooldown, all Ollama requests are immediately rejected:
- Auto modes: silently fall back to the free tier
- Direct/fallback mode: return 429 so LiteLLM skips to openrouter-auto

Changes:
- router/main.py: Add _ollama_cooldown_until state, cooldown check
  before Ollama proxy call, and activation on failure. Add Prometheus
  metrics (ollama_cooldown_active, ollama_cooldown_remaining_seconds).
- litellm/config.yaml: Remove test-fallback-model, remove duplicate
  llm-routing-ollama deployment (127.0.0.2), restore Ollama api_base
  to production (api.ollama.com), remove enterprise-only
  allowed_fails_policy.
- README.md: Update sequence diagrams, Fallback and Cooldown Behavior
  section, and metrics table to document router-side cooldown.

* chore: tidy up repository, remove hello world dummies, move and document verification scripts

* fix: resolve hardcoded worktree leaks and LiteLLM auth errors

* Address PR comments: robust httpx client teardown, dynamic path escaping in sed, expected model asserts in tests, synced metrics documentation

* Implement Valkey global cooldown cache sync, standard HTTPX client lifecycles, and handle transient exception status codes

* Address PR#11 code reviews: multimodal message extraction, concurrent breaker Valkey calls, generic error detail messages, SSE stream token counting, secure YAML rendering, and robust verification script exit codes

* Address PR #12 code review comments

- Decouple agy_proxy from main by introducing CooldownPersistence protocol and passing client/cooldown_persistence parameters.
- Manage http client lifetime cleanly in agy_proxy.
- Reset Valkey client state and cache last init attempt on exceptions to enforce the 5s retry cooldown.
- Add isinstance(msg, dict) guards to prevent crashes on malformed payloads.
- Use incremental UTF-8 decoder in stream generator to prevent character corruption.
- Remove dead should_close_client variables and conditions.
- Guard test_antigravity_connection when agentapi is missing.
- Fix typo in README.

* feat: expose model capabilities, token limits, and costs in Model Hub Table

- ci: add httpx to test workflow pip install for test_a2_verify.py

- config: add public_model_groups list to litellm_settings so all agent-*,
  openrouter-auto, llm-routing-ollama, and ollama-deepseek-* groups appear
  in the LiteLLM Model Hub Table UI

- config: add full model_info to all model_list entries (supports_vision,
  supports_reasoning, supports_function_calling, mode, max_tokens,
  max_input_tokens, is_public_model_group)

- config: set llm-routing-ollama and ollama-deepseek-v4-{pro,flash} context
  windows to 512K (524288 tokens), up from 131K/262K

- config: add DeepSeek API equivalent per-token costs to ollama models for
  Langfuse cost tracking:
    ollama-deepseek-v4-pro:   $1.74/1M input, $3.48/1M output
    ollama-deepseek-v4-flash: $0.14/1M input, $0.28/1M output

- router: enrich /model/new roster sync payload with model_info (features,
  context length from OpenRouter API, is_public_model_group) for all
  dynamically registered agent-* tiers

- router: add _register_ollama_models_in_db() — registers ollama-deepseek
  models via /model/new at startup so their model_info wins over LiteLLM's
  internal ollama_chat provider lookup (which returns null/false for unknown
  models in /model_group/info aggregation)

* chore: address review feedback on PR #14 (DRY purge helper, safety-net capabilities, align returned context lengths, and refactor verify scripts to httpx)

* chore: address PR #15 code review fixes and fix CI workflow triggers

* chore(deps): adjust dependabot root docker update time to 02:55 UTC (04:55 local)

* chore: address code review feedback (YAML list indentation, raise_for_status in metrics fetch, detailed HTTP routing diagnostics, and defensive config type checking)

* chore: trigger CI

* Address code reviews for PR #25

* Address new PR reviews and CodeRabbit feedback

* Address Gemini Code Assist review feedback on null text handling and empty choices fallback

* chore: address PR review feedback on DATABASE_URL, fallback password, and max_tokens clamping

* chore: address CodeRabbit and PR #27 review feedback

* chore: address Gemini Code Assist review feedback on PR #28

* fix: change session fingerprint hash to SHA-256 to satisfy CodeQL

* perf: upgrade session fingerprint hash function to SOTA blake2b

* fix: safely handle usage returned as null in api response

* fix: resolve missing LiteLLM request logs on Admin UI and fix Langfuse bad requests

* fix: address PR 30 review feedback including postgres password and circuit breaker fixes

* fix: address PR 31 review feedback
@sheepdestroyer
sheepdestroyer deleted the feat/pr27-review-fixes branch June 20, 2026 21:12
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