Skip to content

Valkey Global Cooldowns, Secure Deployments, and Review Resolutions#13

Closed
sheepdestroyer wants to merge 35 commits into
masterfrom
valkey-global-cooldown-cache-and-cleanups-resolved
Closed

Valkey Global Cooldowns, Secure Deployments, and Review Resolutions#13
sheepdestroyer wants to merge 35 commits into
masterfrom
valkey-global-cooldown-cache-and-cleanups-resolved

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Comprehensive Improvements: Valkey Global Cooldowns, Secure Deployments, Multimodal Safety, and Code Quality Refinements

This pull request consolidates worktree leak resolutions, LiteLLM authentication fixes, and updates addressing CodeRabbit, Sourcery, and bot review feedback.


🚀 Key Improvements & Features

1. Valkey (Redis) Global Cooldown Cache Sync

  • Multi-Worker Concurrency Support: Stored Ollama cooldowns (cooldown:ollama key with a 300s TTL) and circuit breaker tier hashes (circuit_breaker:{name}) in Valkey to guarantee consistent routing states across multi-worker Uvicorn and container environments.
  • Robust Local Fallback: Seamlessly falls back to local in-memory tracking if Valkey is offline or unreachable, ensuring local/offline unit test suites pass without database dependencies.
  • Concurrent Valkey I/O: Executed Google and Vendor circuit breaker updates concurrently using asyncio.gather for optimal performance.
  • Exception Visibility: Replaced bare except Exception: pass suppression blocks inside agy_proxy.py with detailed warning logs.

2. Multi-modal / List-based Content Safety

  • Added type checks (isinstance(content, list)) to extract string text from multimodal/structured message payloads safely, preventing AttributeError and TypeError crashes when resolving last_user_message for triage classification, last_prompt for agy proxying, and request fingerprints for session IDs.

3. Precise SSE Stream Token Measurement & Sanitized Errors

  • Structured SSE Line Parser: Replaced raw streaming chunk byte-size accumulation (which vastly overestimates tokens due to SSE metadata overhead) with a structured line-by-line SSE chunk decoder. It parses the JSON delta structures and accumulates the exact character lengths of generated text (choices[0].delta.content).
  • Standardized Token Estimator: Replaced the inline character-count division formula in the native agy generator with the global helper estimate_prompt_tokens(body).
  • Sanitized Downstream Errors: Sanitized downstream errors returned to API clients in HTTPException responses to generic messages ("LiteLLM upstream request failed") to prevent diagnostic leaks.

4. Secure Key Substitution & Deployment Robustness

  • Secure YAML Renderer: Replaced the vulnerable command-line sed argument pipeline in start-stack.sh with a Python-based heredoc renderer. By passing the secret LITELLM_MASTER_KEY through the environment (os.environ["LITELLM_MASTER_KEY"]) into Python's stdin, the secret is completely hidden from visible process listing arguments (argv).
  • Path Escaping & Cleanup: Removed brittle sed escaping logic in start-stack.sh by transitioning to pythonic string replacements.

5. Worktree Path & Configuration Leaks Resolved

  • Dynamic Paths: Configured all verification and helper scripts (verify_ollama_cooldown.py, verify_direct_ollama_cooldown.py, backup.sh, sync_gemini_token.py, test_antigravity.py, and test_classifier_accuracy.py) to resolve the workspace root dynamically via relative locations, preventing hardcoded home directories from leaking.
  • LiteLLM Admin UI Authorization: Fixed the admin UI 401 Unauthorized errors on dashboard spend logs by substituting the hardcoded placeholder key in pod.yaml with the active LITELLM_MASTER_KEY at deployment time.

6. Fail-Fast Verification & Code Cleanups

  • Metric Float Parsing: Configured verify_ollama_cooldown.py and verify_direct_ollama_cooldown.py to parse Prometheus counter outputs as floats first (int(float(...))), preventing crashes when gauges return decimal formats (e.g. 2.0).
  • Clean Exit Statuses: Added sys.exit(1) calls inside all three verification utilities to guarantee non-zero exit codes on failures.
  • Correct HTTPX API Usage: Configured agy_proxy.py to specify custom request timeouts during client.build_request(...) instead of passing them directly to client.send(...) (which is unsupported).
  • Ruff Linting: Renamed shadowed format variable to fmt in mock_rate_limit_server.py.

🛠️ Code Review Adjustments (PR #12 Comments Resolved)

  • Decoupled agy_proxy Architecture: Extracted dynamic imports of router functions and HTTP client from agy_proxy.py into a type-safe CooldownPersistence protocol, allowing cleaner injection of dependencies.
  • Client Lifetime Safety: Structured try_agy_proxy to safely close locally created HTTP clients on early non-streaming exits, and delegate closure to token_generator's finally block on streaming paths.
  • Outage-Safe Redis Cache: Configured sync_cooldowns_from_valkey and save_cooldowns_to_valkey to reset _redis_client = None and record attempt times upon command errors, preventing 1.0s blocking timeouts during Valkey outages.
  • Message List Payload Safety: Added isinstance(msg, dict) guards to prevent crashes if a client passes a non-dictionary message payload to estimate_prompt_tokens or detect_active_tool.
  • Incremental UTF-8 Decoder: Utilized codecs.getincrementaldecoder in stream_generator() to safely buffer and decode streaming bytes, avoiding corrupted text when multi-byte characters cross TCP boundaries.
  • Removed Dead Code: Eliminated unused should_close_client variables and branches.
  • Environment-Safe Test Guard: Updated test_antigravity.py to skip health check testing gracefully when the agentapi binary is missing.

🔍 Verification & Test Results

  1. Local Test Suites:
    • test_circuit_breaker.py, verify_breaker.py, test_a2_verify.py, test_agy_behavior.py, test_agy_tiers.py, and test_antigravity.py all compile and pass successfully.
  2. Dynamic Deployment & Health Checks:
    • Gateway stack built and deployed cleanly (./start-stack.sh --replace) using the secure Python pod.yaml renderer.
  3. Routing Verification:
    • Running verify_ollama_routing.py succeeds for all 6 auto and direct routing test cases.
  4. Valkey Cooldown Integration:
    • Confirmed that running verify_direct_ollama_cooldown.py with mock rate-limit server failures successfully triggers the Valkey-synced cooldown, returns 429 immediately, and exits with code 0.

Summary by Sourcery

Add Valkey-backed global cooldown and circuit-breaker persistence for Ollama and agy routing, tighten Ollama routing/cooldown semantics across router and LiteLLM configs, and improve deployment, scripting, and test robustness.

New Features:

  • Introduce Valkey-based synchronization of Ollama cooldowns and circuit breaker state across router workers.
  • Add shared HTTP client management and precise SSE-based token accounting for LiteLLM proxy streams.
  • Provide verification scripts for Ollama routing behavior and cooldown enforcement.

Bug Fixes:

  • Harden message and content handling for multimodal/list payloads to avoid crashes in routing and tool detection.
  • Fix LiteLLM master key handling in configs and deployments to eliminate hardcoded secrets and authorization issues.
  • Ensure classifier and backup utilities resolve workspace paths dynamically instead of using hardcoded home directories.

Enhancements:

  • Refine Ollama/agy routing logic, including classifier-gated Ollama usage and improved fallback behavior on errors and cooldowns.
  • Persist circuit breaker state to Valkey and expose richer Prometheus metrics, including Ollama cooldown gauges.
  • Make dashboard, visualizer, and annotation handling more robust with async-safe file I/O and locking.
  • Update docs to explain new Ollama routing, fallback trees, cooldown semantics, and metrics.
  • Tighten timeouts and router settings for faster failure detection and to rely on router-managed cooldowns instead of LiteLLM policies.

Build:

  • Update router container to install the Redis/Valkey client dependency.
  • Switch LiteLLM master key configuration to use environment variable injection and add llm-routing-ollama as a routed model in config.

Documentation:

  • Expand README with detailed Ollama routing tables, fallback trees, cooldown behavior, and updated metrics documentation.
  • Add a scripts README documenting orchestration, verification, and classifier maintenance tooling.

Tests:

  • Add verification scripts for Ollama routing, Valkey-backed cooldown behavior, and direct llm-routing-ollama cooldown.
  • Relax test_antigravity.py to skip gracefully when the external agentapi binary is missing.

Chores:

  • Remove obsolete hello/goose test files and unused dataset-loading paths in scripts.
  • Clean up various scripts for safer JSON parsing, atomic writes, and clearer logging output.

google-labs-jules Bot and others added 30 commits June 18, 2026 10:27
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
- Move _last_stats_save inside try block (only advance on successful write)
- Add save_persisted_stats(force=True) + timeline flush in lifespan shutdown
- Remove redundant import time inside save_persisted_stats
- Fix misleading comment about independent throttle timers
- Use time.monotonic() instead of time.time() for throttle timestamps
- Add logger.warning to bare except in timeline write
- Add logging to bare except Exception: pass in shutdown timeline flush
- Move asyncio.create_task(push_aggregate_scores()) before yield so it runs during app lifetime
- Use atomic file writes (temp file + os.replace) in save_persisted_stats
- Use atomic file writes in shutdown timeline flush and record_tool_usage timeline write
- Restructure lifespan to single yield point (remove else: yield; return pattern)
- Capture time.monotonic() once in record_tool_usage and reuse for guard + assignment
- Extract shared _atomic_write_json_sync / _atomic_write_json_async helpers
- Make save_persisted_stats async, using _atomic_write_json_async with
  run_in_executor + deep-copy to prevent concurrent modification
- Update all 4 async call sites (lifespan, classify_request x2, chat_completions)
  to await save_persisted_stats()
- In record_tool_usage (sync), fire-and-forget both stats and timeline writes
  via loop.run_in_executor with deep-copied data; fall back to sync write
  when no event loop is running (early startup)
- Replace duplicated atomic write logic in lifespan shutdown with shared helper
- router/main.py: wrap lifespan yield in try/finally so stats flush always
  runs; add _background_tasks set to prevent fire-and-forget task GC (RUF006)
- router/static/visualizer.html: add keyboard accessibility (tabindex, role,
  onkeydown) to prompt list items; switch annotation keys from array index to
  stable djb2 prompt hash with backward-compatible index fallback
- scripts/benchmark_classifier.py: safe tier field lookup (tier/llm_tier/clf_tier),
  guard per_tier keying to skip ERROR/unknown labels, safe choices[0] access
- scripts/extract_complex.py: use forward iteration for first user message,
  add system-note filtering ([System: / [Note:])
- scripts/extract_gapfill.py: same as extract_complex.py
- scripts/retry_errors.py: safe status.get() to prevent AttributeError,
  schema-aware ERROR detection (tier + clf_tier), only increment fixed on success
- scripts/reclassify_all.py: safe choices[0] fallback to prevent IndexError
- test_circuit_breaker.py: replace == True/False with idiomatic assert (14 sites)
- verify_breaker.py: replace == True/False with idiomatic assert (5 sites)
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.
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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 58 minutes and 50 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: e0f2003a-1249-4238-a3c1-97fa1986ba8d

📥 Commits

Reviewing files that changed from the base of the PR and between 865f5f7 and 3ff83f3.

📒 Files selected for processing (23)
  • README.md
  • hello.py
  • litellm/config.yaml
  • router/Containerfile
  • 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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch valkey-global-cooldown-cache-and-cleanups-resolved

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.

@sourcery-ai

sourcery-ai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements Valkey-backed global cooldown and circuit breaker persistence for multi-worker router deployments, refactors agy proxy to use shared HTTP clients and a pluggable persistence interface, adds precise SSE/token accounting and safer multimodal handling, secures deployment/key injection and paths, and introduces verification scripts plus documentation updates for Ollama routing/cooldowns and LiteLLM configuration.

Sequence diagram for Ollama routing with Valkey-backed cooldown

sequenceDiagram
    actor Client
    participant Router as router.main.chat_completions
    participant Valkey as Valkey(get_redis)
    participant LiteLLM as LiteLLM_execute_proxy

    Client->>Router: POST /v1/chat/completions (model=llm-routing-ollama)
    Router->>Router: sync_cooldowns_from_valkey()
    alt Valkey available
        Router->>Valkey: get("cooldown:ollama")
        Valkey-->>Router: epoch_until
        Router->>Router: update _ollama_cooldown_until
    end

    Router->>Router: classify_request() (for gating)
    Router->>Router: determine should_try_ollama

    alt _ollama_cooldown_until in future
        Router-->>Client: HTTP 429 ("Ollama backend cooled down")
    else No active cooldown
        Router->>LiteLLM: execute_proxy(target_model)
        alt LiteLLM/Ollama transient failure (429/5xx)
            LiteLLM-->>Router: HTTP error
            Router->>Router: _ollama_cooldown_until = now + OLLAMA_COOLDOWN_SECONDS
            Router->>Valkey: save_cooldowns_to_valkey()
            Router-->>Client: HTTP 429 or fallback to free tier
        else Success
            LiteLLM-->>Router: JSON completion
            Router-->>Client: Response
        end
    end
Loading

File-Level Changes

Change Details Files
Add Valkey-backed global cooldown and circuit breaker persistence with router-wide Ollama cooldown logic and shared HTTP client management.
  • Introduce lazy-initialized async Valkey client with retry/backoff and safe teardown in router lifespan hooks
  • Persist and restore Ollama cooldown timestamps and dual circuit breaker state to Valkey, resetting the client on errors to avoid repeated timeouts
  • Implement router-side Ollama cooldown gating that short-circuits requests, differentiates auto vs direct models, and exposes Prometheus metrics for cooldown status
  • Refactor LiteLLM proxy path into an execute_proxy helper using a shared httpx.AsyncClient, standardized prompt token estimation, and context-aware max_tokens clamping
  • Update metrics, dashboard, and static file reads to be async-friendly and to sync Valkey state on access
router/main.py
router/circuit_breaker.py
router/Containerfile
router/free_models_roster.json
Harden agy proxy architecture, streaming, and persistence with shared clients, Valkey-backed circuit breaker sync, and safer message handling.
  • Extend try_agy_proxy to accept an injected AsyncClient and a CooldownPersistence interface, wiring it from the router via ValkeyCooldownPersistence
  • Use shared HTTP client for agy daemon calls, ensuring proper closure only when the client is locally created and not for streaming responses
  • Add Valkey-based sync/save hooks around circuit breaker success/failure paths, avoiding bare exception suppression and improving logging
  • Make prompt reconstruction and session handling more defensive by checking message types and contents before use
router/agy_proxy.py
router/main.py
Improve robustness for multimodal/list content, tool detection, and prompt token estimation across router paths.
  • Guard message iterations in detect_active_tool, triage, and agy paths with isinstance checks to tolerate non-dict payloads
  • Normalize list-based message content by concatenating text blocks when computing last_user_message, last_prompt, and fingerprints
  • Centralize prompt token estimation into estimate_prompt_tokens, reusing it for native agy metrics and LiteLLM proxy pre-screening
router/main.py
Implement precise SSE stream token measurement, sanitize upstream error messages, and adjust Langfuse spans in LiteLLM proxying.
  • Replace naive byte-length counting for streaming responses with an incremental UTF-8 SSE line parser that accumulates content deltas only
  • Standardize HTTPException messages for LiteLLM upstream failures to a generic string to avoid leaking diagnostic details
  • Propagate Langfuse trace IDs through headers and finalize observations with richer metadata for both streaming and non-streaming paths
router/main.py
Rework secure deployment and configuration: dynamic paths, safe YAML rendering, and environment-driven secret handling.
  • Replace hardcoded workspace paths with dynamic WORKDIR resolution in start-stack.sh, backup.sh, sync_gemini_token.py, and tests
  • Introduce a Python-based pod.yaml renderer that injects LITELLM_MASTER_KEY and normalized paths without exposing secrets in process args
  • Switch LiteLLM config to read master_key and router model API key from LITELLM_MASTER_KEY, and adjust router_settings to avoid enterprise-only cooldown policies
  • Ensure LITELLM_MASTER_KEY is generated or present and bail out early if it cannot be set
start-stack.sh
scripts/backup.sh
sync_gemini_token.py
test_antigravity.py
litellm/config.yaml
test_classifier_accuracy.py
Document and configure Ollama routing, cooldown behavior, and LiteLLM fallback chains, with supporting verification utilities.
  • Update README routing diagrams, tables, and Ollama sections to describe classifier-gated llm-routing-ollama/auto-ollama behavior and router-side cooldowns
  • Extend LiteLLM fallback chains to include llm-routing-ollama as a premium escalation step from agent tiers and register llm-routing-ollama as a local model
  • Adjust agent tier default free models and timeouts to newer OpenRouter offerings with shorter request timeouts
  • Add verification scripts for Ollama routing/cooldown and a mock 429 server, plus a scripts README cataloging orchestration and test tools
README.md
litellm/config.yaml
scripts/verification/verify_ollama_routing.py
scripts/verification/verify_ollama_cooldown.py
scripts/verification/verify_direct_ollama_cooldown.py
scripts/verification/mock_rate_limit_server.py
scripts/README.md
General robustness, test, and tooling cleanups to remove hardcoding and improve failure signaling.
  • Make backup, verification, and extraction scripts path-agnostic and safer (dynamic workspace resolution, atomic writes, stricter JSON error handling)
  • Enhance verification scripts to parse float Prometheus counters, enforce non-zero exit codes on failure, and skip tests cleanly when external binaries are absent
  • Fix minor lint/readability issues (e.g., variable renames, print formatting) and remove dead code and unused prompt-loading logic in reclassify/retry scripts
scripts/backup.sh
scripts/retry_errors.py
scripts/reclassify_all.py
scripts/extract_gapfill.py
scripts/extract_prompts.py
scripts/verification/verify_ollama_cooldown.py
scripts/verification/verify_direct_ollama_cooldown.py
test_antigravity.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The new execute_proxy error handling sometimes returns HTTPException(status_code=502, detail=f"Proxy call failed: {exc}"), which leaks internal exception details to clients; consider standardizing these to the sanitized message you use elsewhere (e.g. "LiteLLM upstream request failed").
  • In try_agy_proxy, cooldown_persistence.save() is awaited on every success and quota failure, which adds an extra network round-trip on the hot path; you may want to debounce or fire-and-forget these Valkey syncs to avoid impacting latency while still keeping state reasonably fresh.
  • The SSE parsing in execute_proxy's streaming branch manually scans for "data:" lines in a string buffer; if LiteLLM changes its framing or introduces multi-line events, the token accounting could silently break—consider using a small dedicated SSE parser or at least guarding against multi-line data: blocks.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `execute_proxy` error handling sometimes returns `HTTPException(status_code=502, detail=f"Proxy call failed: {exc}")`, which leaks internal exception details to clients; consider standardizing these to the sanitized message you use elsewhere (e.g. `"LiteLLM upstream request failed"`).
- In `try_agy_proxy`, `cooldown_persistence.save()` is awaited on every success and quota failure, which adds an extra network round-trip on the hot path; you may want to debounce or fire-and-forget these Valkey syncs to avoid impacting latency while still keeping state reasonably fresh.
- The SSE parsing in `execute_proxy`'s streaming branch manually scans for `"data:"` lines in a string buffer; if LiteLLM changes its framing or introduces multi-line events, the token accounting could silently break—consider using a small dedicated SSE parser or at least guarding against multi-line `data:` blocks.

## Individual Comments

### Comment 1
<location path="router/agy_proxy.py" line_range="251-254" />
<code_context>
+
+        # Build context-aware prompt from message history
+        proxy_prompt = prompt
+        if messages:
+            context_parts = []
+            for msg in messages[-10:]:
+                if not isinstance(msg, dict):
+                    continue
+                role = msg.get("role", "user")
+                content = msg.get("content") or ""
+                if role == "user":
+                    context_parts.append(f"User: {content}")
</code_context>
<issue_to_address>
**suggestion:** Context reconstruction for agy prompts does not handle structured `content` blocks like the main router does.

In `try_agy_proxy`, `content` is still assumed to be a string when building `proxy_prompt`. In `router/main.py` you already normalize cases where `content` is a list of `{type: 'text', text: ...}` blocks. Please reuse or mirror that normalization here so structured `content` is flattened to text and we don’t end up with raw list/dict reprs in the `User:` / `Assistant:` lines.

```suggestion
                role = msg.get("role", "user")
                raw_content = msg.get("content", "")
                # Normalize structured content blocks (e.g. [{"type": "text", "text": "..."}]) to plain text
                if isinstance(raw_content, list):
                    text_chunks = []
                    for block in raw_content:
                        if not isinstance(block, dict):
                            continue
                        if block.get("type") == "text":
                            text = block.get("text") or ""
                            if isinstance(text, str):
                                text_chunks.append(text)
                    content = "\n".join(text_chunks).strip()
                elif isinstance(raw_content, str):
                    content = raw_content
                else:
                    content = str(raw_content) if raw_content is not None else ""
                if role == "user":
                    context_parts.append(f"User: {content}")
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread router/agy_proxy.py
Comment on lines +251 to +254
role = msg.get("role", "user")
content = msg.get("content") or ""
if role == "user":
context_parts.append(f"User: {content}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Context reconstruction for agy prompts does not handle structured content blocks like the main router does.

In try_agy_proxy, content is still assumed to be a string when building proxy_prompt. In router/main.py you already normalize cases where content is a list of {type: 'text', text: ...} blocks. Please reuse or mirror that normalization here so structured content is flattened to text and we don’t end up with raw list/dict reprs in the User: / Assistant: lines.

Suggested change
role = msg.get("role", "user")
content = msg.get("content") or ""
if role == "user":
context_parts.append(f"User: {content}")
role = msg.get("role", "user")
raw_content = msg.get("content", "")
# Normalize structured content blocks (e.g. [{"type": "text", "text": "..."}]) to plain text
if isinstance(raw_content, list):
text_chunks = []
for block in raw_content:
if not isinstance(block, dict):
continue
if block.get("type") == "text":
text = block.get("text") or ""
if isinstance(text, str):
text_chunks.append(text)
content = "\n".join(text_chunks).strip()
elif isinstance(raw_content, str):
content = raw_content
else:
content = str(raw_content) if raw_content is not None else ""
if role == "user":
context_parts.append(f"User: {content}")

@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 Ollama cooldown mechanism, integrates Valkey/Redis for state synchronization across multi-worker deployments, and updates fallback chains, metrics, and documentation. It also cleans up hardcoded paths in scripts and orchestration files to use dynamic home/workspace directories. The review feedback highlights several critical edge cases in router/main.py where explicitly null payload values (such as text, max_tokens, or streaming delta objects) could trigger TypeError or AttributeError exceptions, and suggests a more accurate fallback method for estimating completion tokens.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread router/main.py
elif isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
tokens += len(block.get("text", "")) // 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.

high

If the message payload contains a block with \"text\": null, block.get(\"text\", \"\") will return None instead of \"\". This will cause a TypeError when calling len(None). Using block.get(\"text\") or \"\" safely defaults to an empty string in all cases.

                    tokens += len(block.get(\"text\") or \"\") // 4

Comment thread router/main.py
last_user_message = msg.get("content", "")
content = msg.get("content") or ""
if isinstance(content, list):
content = "".join(block.get("text", "") for block in content if isinstance(block, dict) and block.get("type") == "text")

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

If the message payload contains a block with \"text\": null, block.get(\"text\", \"\") will return None instead of \"\". This will cause a TypeError when calling \"\".join(). Using block.get(\"text\") or \"\" safely defaults to an empty string in all cases.

                content = \"\".join(block.get(\"text\") or \"\" for block in content if isinstance(block, dict) and block.get(\"type\") == \"text\")

Comment thread router/main.py
last_prompt = msg.get("content", "")
content = msg.get("content") or ""
if isinstance(content, list):
content = "".join(block.get("text", "") for block in content if isinstance(block, dict) and block.get("type") == "text")

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

If the message payload contains a block with \"text\": null, block.get(\"text\", \"\") will return None instead of \"\". This will cause a TypeError when calling \"\".join(). Using block.get(\"text\") or \"\" safely defaults to an empty string in all cases.

                        content = \"\".join(block.get(\"text\") or \"\" for block in content if isinstance(block, dict) and block.get(\"type\") == \"text\")

Comment thread router/main.py
continue
c = msg.get("content") or ""
if isinstance(c, list):
c = "".join(block.get("text", "") for block in c if isinstance(block, dict) and block.get("type") == "text")

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

If the message payload contains a block with \"text\": null, block.get(\"text\", \"\") will return None instead of \"\". This will cause a TypeError when calling \"\".join(). Using block.get(\"text\") or \"\" safely defaults to an empty string in all cases.

                        c = \"\".join(block.get(\"text\") or \"\" for block in c if isinstance(block, dict) and block.get(\"type\") == \"text\")

Comment thread router/main.py
Comment on lines +1645 to +1646
requested_max_tokens = body_to_send.get("max_tokens", 4096)
if requested_max_tokens > 32768: # Only intervene for unusually large max_tokens

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

If max_tokens is explicitly passed as None (null in JSON), body_to_send.get(\"max_tokens\", 4096) will return None. This will cause a TypeError when evaluating requested_max_tokens > 32768. Adding a None check prevents this crash.

                requested_max_tokens = body_to_send.get(\"max_tokens\")\n                if requested_max_tokens is not None and requested_max_tokens > 32768:  # Only intervene for unusually large max_tokens

Comment thread router/main.py
Comment on lines +1701 to +1702
delta = choices[0].get("delta", {})
content = delta.get("content") or ""

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

If choices[0][\"delta\"] is explicitly None (null) in some streaming chunks, delta.get(\"content\") will raise an AttributeError. Defaulting delta to an empty dictionary if it is falsy prevents this crash.

                                                    delta = choices[0].get(\"delta\") or {}\n                                                    content = delta.get(\"content\") or \"\"

Comment thread router/main.py
resp_json = response.json()
usage = resp_json.get("usage", {})
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 overestimates the token count because it includes all the JSON metadata, headers, and formatting. It is much more accurate to estimate based on the actual generated message content.

                    completion_tokens = usage.get(\"completion_tokens\")\n                    if completion_tokens is None:\n                        choices = resp_json.get(\"choices\", [])\n                        content = choices[0].get(\"message\", {}).get(\"content\") or \"\" if choices else \"\"\n                        completion_tokens = len(content) // 4

@sheepdestroyer
sheepdestroyer deleted the valkey-global-cooldown-cache-and-cleanups-resolved 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