✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

✨ Display full free roster on dashboard - #324

Merged
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268
Aug 5, 2026
Merged

✨ Display full free roster on dashboard#324
sheepdestroyer merged 3 commits into
masterfrom
feature/dashboard-free-roster-display-950286063644622268

Conversation

@sheepdestroyer

@sheepdestroyersheepdestroyer commented Jul 13, 2026

Copy link
Copy Markdown
Owner

🎯 What:

  • Enhanced the router dashboard to display a comprehensive list of available free models (the "Free Roster").
  • Included metrics: Score (0-100), Context Window, Tool Use capability, and Current Status.
  • Implemented "Opportunistic Refresh": if a free-tier model returns a 429 (Rate Limit), a background roster sync is triggered immediately to update model availability.

📊 Coverage:

  • Backend: Verified sync_adaptive_router_roster, get_best_free_model, and execute_proxy logic via existing and new test scenarios.
  • Frontend: Captured screenshots and video via Playwright confirming the new "Free Model Roster" card renders correctly and updates via AJAX.

Result:

  • Users can now see which free models are currently favored by the router and why (based on scoring).
  • The router is more resilient to rate-limiting by proactively refreshing its roster when errors occur.

Fixes#290


PR created automatically by Jules for task 950286063644622268 started by @sheepdestroyer

Summary by CodeRabbit

  • New Features

    • Added automatic discovery and registration of available free AI models.
    • Added a dashboard card showing model scores, context capacity, tool support, and activation status by tier.
    • Added automatic roster refresh after rate-limit errors to help restore model availability.
    • Added fallback model selection when preferred tiers are unavailable.
  • Bug Fixes

    • Improved selection of the best available free model using the shared discovery process.
    • Prevented unnecessary stale-deployment cleanup when database configuration is unavailable.

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-aisourcery-aiBot 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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in:56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b89f821-1e2d-41ae-a793-4185137604f0

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2606 and 378a241.

📒 Files selected for processing (1)
  • router/main.py
📝 Walkthrough

Walkthrough

A new OpenRouter inspection script and centralized free-model discovery flow support adaptive roster registration, rate-limit-triggered refreshes, best-model caching, and dashboard display of model scores, capabilities, and activation status.

Changes

Free model roster

Layer / File(s)Summary
Centralized free-model discovery
check_openrouter.py, router/main.py
Adds asynchronous OpenRouter model inspection and a shared helper that filters and scores free models for roster and best-model selection.
Adaptive roster registration
router/main.py
Updates tier assignment, cascading fallbacks, deployment registration tracking, and conditional stale-registration cleanup.
Rate-limit-triggered refresh
router/main.py
Refreshes the roster after HTTP 429 responses for agent-* models, subject to cooldowns, and invalidates cached best-model data.
Dashboard roster rendering
router/main.py
Adds a Free Model Roster card that shows model metadata and active or excluded status during dashboard updates.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Router
participant LiteLLM
participant OpenRouter
Client->>Router: Request agent-* model
Router->>LiteLLM: Proxy request
LiteLLM-->>Router: Return HTTP 429
Router->>OpenRouter: Fetch free-model metadata
Router->>LiteLLM: Synchronize roster
Router->>Router: Invalidate best-model cache
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe standalone check_openrouter.py script is not clearly required for the dashboard roster or opportunistic refresh objectives [#290].Remove check_openrouter.py or document its direct role in implementing and validating the linked issue.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the primary change: displaying the full free-model roster on the dashboard.
Linked Issues check✅ PassedThe changes implement the requested roster fields and opportunistic refresh after rate-limit responses [#290].
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/dashboard-free-roster-display-950286063644622268
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dashboard-free-roster-display-950286063644622268

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assistgemini-code-assistBot 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 refactors the OpenRouter free model roster synchronization logic by extracting model fetching into a dedicated helper, implementing opportunistic roster syncs on rate limits (HTTP 429), and adding a Free Model Roster table to the dashboard UI. The review feedback highlights several critical improvements: introducing an asyncio.Lock to serialize and throttle concurrent roster syncs to prevent hammering the API, using a safe fallback value for context_length instead of defaulting to 0 (which would break upstream requests), and applying defensive .get() calls with fallbacks when rendering the dashboard table to avoid potential KeyError or TypeError exceptions.

Important

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

Comment threadrouter/main.py
FREE_MODEL_CACHE_TTL = 3600 # Refresh cache every 1 hour

_registered_free_models: Dict[str, Set[str]] = {}
_last_roster_sync: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Define an asyncio.Lock to prevent concurrent roster sync executions when multiple requests trigger opportunistic syncs simultaneously.

Suggested change
_last_roster_sync: float=0.0
_last_roster_sync: float=0.0
_roster_sync_lock=asyncio.Lock()

Comment threadrouter/main.py Outdated
Comment on lines +1911 to +1925
async def maybe_trigger_roster_sync(force: bool = False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""
global _last_roster_sync
now = time.monotonic()
# 5-minute throttle for roster sync
if not force and (now - _last_roster_sync < 300):
return

master_key = os.getenv("LITELLM_MASTER_KEY")
if master_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
await sync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh data
global free_model_cache
free_model_cache["data"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use the _roster_sync_lock to serialize and throttle opportunistic roster syncs. This prevents multiple concurrent requests from hammering the OpenRouter API and causing database deadlocks or duplicate model registrations during a rate-limit storm.

asyncdefmaybe_trigger_roster_sync(force: bool=False):
"""Opportunistically refresh the OpenRouter roster if ratelimited or after TTL."""global_last_roster_syncnow=time.monotonic()
# 5-minute throttle for roster syncifnotforceand (now-_last_roster_sync<300):
returnif_roster_sync_lock.locked():
logger.info("Roster sync already in progress — skipping opportunistic trigger")
returnasyncwith_roster_sync_lock:
# Re-check throttle inside lock in case another task just finished syncingnow=time.monotonic()
ifnotforceand (now-_last_roster_sync<300):
returnmaster_key=os.getenv("LITELLM_MASTER_KEY")
ifmaster_key:
logger.info(f"Triggering opportunistic roster sync (force={force})")
awaitsync_adaptive_router_roster(master_key)
# Invalidate cache to ensure dashboard gets fresh dataglobalfree_model_cachefree_model_cache["data"] =None

Comment threadrouter/main.py
"id": mid,
"name": m.get("name", mid),
"score": score,
"context_length": m.get("context_length") or 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Defaulting context_length to 0 when it is missing or falsy will cause the model to be registered in LiteLLM with max_tokens: 0 and max_input_tokens: 0. This will break all upstream requests to these models. Default to a safe fallback value like 262144 instead.

Suggested change
"context_length": m.get("context_length") or0,
"context_length": m.get("context_length") or262144,

Comment threadrouter/main.py Outdated
Comment on lines +3471 to +3473
<td style="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><span style="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<td style="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<td style="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>

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

Use defensive .get() calls with safe fallbacks when rendering the dashboard table to prevent potential KeyError or TypeError if any fields are missing or malformed in the persisted roster JSON file.

Suggested change
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m['name']}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m['score']:.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{m['context_length']//1000}k</td>
<tdstyle="padding:10px 8px;font-size:12px;font-weight:600;">{m.get('name', mid)}<br><spanstyle="font-size:10px;opacity:0.4;font-family:monospace;">{mid}</span></td>
<tdstyle="padding:10px 8px;text-align:center;font-weight:bold;color:#fbbf24;">{m.get('score', 0.0):.1f}</td>
<tdstyle="padding:10px 8px;text-align:center;opacity:0.7;font-size:11px;">{(m.get('context_length') or262144)//1000}k</td>

@coderabbitaicoderabbitaiBot 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: 4

🤖 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/main.py`:
- Around line 3469-3477: Update the HTML row construction to apply html.escape
to the external model fields m['name'] and mid before interpolating them into
the markup. Preserve their displayed values and existing formatting while
ensuring both fields are safely escaped.
- Around line 2679-2682: Remove the unreachable status_code 429 handling from
the stream error handler around maybe_trigger_roster_sync, including the
model_name check there. Preserve or relocate roster synchronization for 429
responses within the existing non-200 response handling branch, where the
response status is available.
- Around line 1630-1652: Update the model filtering logic in
sync_adaptive_router_roster to skip entries where supported_parameters does not
contain "tools". Place the has_tools check before pricing and free_models.append
so non-tool models are not registered in agent-* deployments, while preserving
the existing denylist and tool-capable model handling.
- Around line 1911-1927: The maybe_trigger_roster_sync function needs
single-flight protection because force=True bypasses the throttle and can start
overlapping sync_adaptive_router_roster runs. Add an in-progress guard around
the sync and ensure it is cleared reliably after completion, so concurrent
forced calls coalesce or return while one sync is active while preserving the
existing throttle and cache invalidation behavior.
🪄 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: 82b037f1-8c66-4383-b897-ca9ad6f3775c

📥 Commits

Reviewing files that changed from the base of the PR and between 33dd1b6 and fa20ba8.

⛔ Files ignored due to path filters (2)
  • mock.log is excluded by !**/*.log
  • server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • check_openrouter.py
  • router/main.py

Comment threadrouter/main.py
Comment threadrouter/main.py
Comment threadrouter/main.py
Comment on lines +2679 to +2682
if hasattr(ex, "status_code") and getattr(ex, "status_code") == 429:
if model_name.startswith("agent-"):
await maybe_trigger_roster_sync(force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
# Inspect the relevant ranges in router/main.py
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for start, end in [(2588, 2615), (2665, 2695), (2718, 2745)]: print(f"\n--- {p}:{start}-{end} ---") for i in range(start, min(end, len(lines)) + 1): print(f"{i:5d}: {lines[i-1]}")PY# Find other uses of status_code in streaming/error handling nearby
rg -n 'status_code|aiter_bytes|maybe_trigger_roster_sync|429' router/main.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10027


🏁 Script executed:

#!/bin/bashset -euo pipefail
# Identify the HTTP client and related imports/usages
python3 - <<'PY'from pathlib import Pathp = Path("router/main.py")lines = p.read_text().splitlines()for i, line in enumerate(lines, 1): if 'import httpx' in line or 'from httpx' in line or 'AsyncClient' in line or 'client =' in line: print(f"{i:5d}: {line}")PY# Search for any exception/status_code patterns in the repository
rg -n 'hasattr\(.*status_code|getattr\(.*status_code|\.status_code\)' router main.py .

Repository: sheepdestroyer/LLM-Routing

Length of output: 2089


🌐 Web query:

httpx streaming response aiter_bytes exception status_code transport decode errors

💡 Result:

When using httpx streaming responses with aiter_bytes, you may encounter several categories of exceptions and behaviors related to transport, decoding, and stream lifecycle management. Exceptions and Error Handling DecodingError: This exception is raised when the response content is malformed or cannot be decompressed (e.g., gzip, brotli, or zstd errors) [1][2][3]. If you are working with compressed streams, ensure the necessary dependencies (like httpx[zstd] or brotli) are installed [2]. StreamError Subclasses: - StreamConsumed: Raised if you attempt to iterate over a stream that has already been consumed [4][3]. Note that aiter_raw and aiter_bytes consume the stream, and aiter_raw is typically only available once [4][5]. - StreamClosed: Raised if you try to stream content after the underlying request connection has been closed [3]. - ResponseNotRead: This occurs if you attempt to access properties like response.content after a streaming request without having fully read the content (e.g., via aread) [6][3]. Transport and Asyncio Exceptions: - asyncio.CancelledError: When using asynchronous streaming, it is important to handle asyncio.CancelledError. Recent versions of httpx have addressed issues where cancellation during stream iteration required explicit handling to ensure proper generator cleanup [7]. - Connection/Read Errors: Standard transport errors (e.g., ReadError, ConnectError) may be raised during iteration if the network connection is interrupted while fetching chunks [1][3]. Key Differences in Streaming Methods - aiter_bytes: Iterates over the decoded (decompressed) content of the response [4][5]. It automatically handles Content-Encoding (e.g., gzip, deflate) [8][5]. - aiter_raw: Iterates over the raw, compressed bytes-on-the-wire [4][5]. This is generally not needed for end-users and should be used with caution as it is only available once [4]. Status Codes The status_code of the response is available regardless of whether the body has been streamed [9]. To enforce status code validation (e.g., raising an exception for 4xx or 5xx errors), you should explicitly call response.raise_for_status before or after processing the stream [9][1]. Best Practices - Always use a context manager (e.g., async with client.stream(...) as response:) to ensure the connection is closed properly after streaming completes [6]. - If you need to access response.content or response.text after streaming, you must either call await response.aread or manually buffer the chunks [6]. - Use try-except blocks around your iteration to catch specific httpx exceptions like DecodingError or network-related ReadError [1][2].

Citations:


Remove the dead 429 check in the stream error handler.r.aiter_bytes() failures won’t expose a status_code, so this branch can’t fire here, and the upstream 429 path is already handled below. If roster sync should happen on 429, keep it in the non-200 response branch instead.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 2679-2679: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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 2679 - 2682, Remove the unreachable status_code
429 handling from the stream error handler around maybe_trigger_roster_sync,
including the model_name check there. Preserve or relocate roster
synchronization for 429 responses within the existing non-200 response handling
branch, where the response status is available.

Source: Linters/SAST tools

Comment threadrouter/main.py
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation github-actions PRs that update GitHub Actions scripts tests litellm labels Aug 5, 2026
@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 0a85fdb to 7a5b945CompareAugust 5, 2026 20:11
google-labs-julesBotand others added 2 commits August 5, 2026 22:12
- Integrated OpenRouter free model fetching with Artificial Analysis scoring.
- Added detailed "Free Model Roster" table to the dashboard UI.
- Implemented opportunistic roster refresh on HTTP 429 rate-limits.
- Refactored model selection to use a unified scoring and tool-detection logic.
- Verified backend with pytest and frontend with Playwright.
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>

@coderabbitaicoderabbitaiBot 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: 1

🤖 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/main.py`:
- Line 2019: Remove the unresolved merge-conflict markers around the affected
section of router/main.py, including the matching ======= and >>>>>>> markers,
and retain the correct implementation from the conflicting branches so the
module parses as valid Python.
🪄 Autofix

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 Plus

Run ID: 03aab94d-73d6-40b3-b2e4-8dc63ac1c085

📥 Commits

Reviewing files that changed from the base of the PR and between fa20ba8 and 7a5b945.

📒 Files selected for processing (1)
  • router/main.py

Comment threadrouter/main.py Outdated
raise HTTPException(status_code=502, detail="Model proxy failed")


<<<<<<< HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the unresolved merge conflict marker.

Line 2019 contains <<<<<<< HEAD. This is not valid Python. The whole router/main.py module fails to parse, so the router cannot start. Ruff reports invalid-syntax at this exact line. Check for the matching ======= and >>>>>>> markers further down and resolve the conflict.

🐛 Proposed fix
-<<<<<<< HEAD
async def maybe_trigger_roster_sync(force: bool = False):
🧰 Tools
🪛 GitHub Actions: Run Tests / 0_test.txt

[error] 2019-2019: Pytest collection failed because router/main.py contains an unresolved Git merge-conflict marker '<<<<<<< HEAD', causing SyntaxError: invalid syntax. Resolve the conflict markers before rerunning 'CONFIG_PATH=router/config.yaml PYTHONPATH=. pytest --ignore=tests/test_agy_behavior.py --ignore=tests/test_agy_tiers.py --ignore=tests/test_antigravity.py'.

🪛 Ruff (0.16.1)

[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)


[warning] 2019-2019: Expected a statement

(invalid-syntax)

🤖 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` at line 2019, Remove the unresolved merge-conflict markers
around the affected section of router/main.py, including the matching =======
and >>>>>>> markers, and retain the correct implementation from the conflicting
branches so the module parses as valid Python.

Source: Linters/SAST tools

@sheepdestroyer
sheepdestroyerforce-pushed the feature/dashboard-free-roster-display-950286063644622268 branch from 7a5b945 to 0c1a7c4CompareAugust 5, 2026 20:13
@sheepdestroyer
sheepdestroyer merged commit 4a74c8c into masterAug 5, 2026
8 checks passed
@sheepdestroyer
sheepdestroyer deleted the feature/dashboard-free-roster-display-950286063644622268 branch August 5, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationgithub-actionsPRs that update GitHub Actionslitellmrouterscriptstests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display full free roster on dashboard

1 participant

@sheepdestroyer