Mirrowel commited on
Commit
1af1879
·
1 Parent(s): 1ac7bd0

refactor(quota-viewer): 🔨 enhance credential sorting and cooldown display

Browse files

Add natural/numeric sorting for credentials to ensure proper ordering
(e.g., proj-1, proj-2, proj-10 instead of proj-1, proj-10, proj-2).

Improve cooldown display in quota viewer by grouping cooldowns by quota
groups when available, providing clearer visibility into rate limiting
status for grouped models.

Also in this commit:
- refactor(client): improve model stats lookup with alias support
- feat(usage-manager): add quota display formatting for logging

src/proxy_app/quota_viewer.py CHANGED
@@ -6,6 +6,7 @@ Uses only httpx + rich (no heavy rotator_library imports).
6
  """
7
 
8
  import os
 
9
  import sys
10
  import time
11
  from datetime import datetime, timezone
@@ -128,6 +129,19 @@ def format_cooldown(seconds: int) -> str:
128
  return f"{hours}h {mins}m" if mins > 0 else f"{hours}h"
129
 
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  class QuotaViewer:
132
  """Main Quota Viewer TUI class."""
133
 
@@ -548,6 +562,9 @@ class QuotaViewer:
548
  prov_stats = self.cached_stats.get("providers", {}).get(provider, {})
549
  credentials = prov_stats.get("credentials", [])
550
 
 
 
 
551
  if not credentials:
552
  self.console.print(
553
  "[dim]No credentials configured for this provider.[/dim]"
@@ -584,6 +601,8 @@ class QuotaViewer:
584
  if self.cached_stats
585
  else []
586
  )
 
 
587
  for idx, cred in enumerate(credentials, 1):
588
  identifier = cred.get("identifier", f"credential {idx}")
589
  email = cred.get("email", identifier)
@@ -640,6 +659,8 @@ class QuotaViewer:
640
  if self.cached_stats
641
  else []
642
  )
 
 
643
  if 1 <= idx <= len(credentials):
644
  cred = credentials[idx - 1]
645
  cred_id = cred.get("identifier", "")
@@ -717,21 +738,69 @@ class QuotaViewer:
717
  f"[dim]{stats_line}[/dim]",
718
  ]
719
 
720
- # Show model cooldowns if any
721
- if model_cooldowns:
722
- content_lines.append("")
723
- content_lines.append("[yellow]Active Cooldowns:[/yellow]")
724
- for model_name, cooldown_info in model_cooldowns.items():
725
- remaining = cooldown_info.get("remaining_seconds", 0)
726
- if remaining > 0:
727
- # Shorten model name for display
728
- short_model = model_name.split("/")[-1][:35]
729
- content_lines.append(
730
- f" [yellow]⏱️ {short_model}: {format_cooldown(int(remaining))}[/yellow]"
731
- )
732
-
733
  # Model groups (for providers with quota tracking)
734
  model_groups = cred.get("model_groups", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  if model_groups:
736
  content_lines.append("")
737
  for group_name, group_stats in model_groups.items():
 
6
  """
7
 
8
  import os
9
+ import re
10
  import sys
11
  import time
12
  from datetime import datetime, timezone
 
129
  return f"{hours}h {mins}m" if mins > 0 else f"{hours}h"
130
 
131
 
132
+ def natural_sort_key(item: Dict[str, Any]) -> List:
133
+ """
134
+ Generate a sort key for natural/numeric sorting.
135
+
136
+ Sorts credentials like proj-1, proj-2, proj-10 correctly
137
+ instead of alphabetically (proj-1, proj-10, proj-2).
138
+ """
139
+ identifier = item.get("identifier", "")
140
+ # Split into text and numeric parts
141
+ parts = re.split(r"(\d+)", identifier)
142
+ return [int(p) if p.isdigit() else p.lower() for p in parts]
143
+
144
+
145
  class QuotaViewer:
146
  """Main Quota Viewer TUI class."""
147
 
 
562
  prov_stats = self.cached_stats.get("providers", {}).get(provider, {})
563
  credentials = prov_stats.get("credentials", [])
564
 
565
+ # Sort credentials naturally (1, 2, 10 not 1, 10, 2)
566
+ credentials = sorted(credentials, key=natural_sort_key)
567
+
568
  if not credentials:
569
  self.console.print(
570
  "[dim]No credentials configured for this provider.[/dim]"
 
601
  if self.cached_stats
602
  else []
603
  )
604
+ # Sort credentials naturally
605
+ credentials = sorted(credentials, key=natural_sort_key)
606
  for idx, cred in enumerate(credentials, 1):
607
  identifier = cred.get("identifier", f"credential {idx}")
608
  email = cred.get("email", identifier)
 
659
  if self.cached_stats
660
  else []
661
  )
662
+ # Sort credentials naturally to match display order
663
+ credentials = sorted(credentials, key=natural_sort_key)
664
  if 1 <= idx <= len(credentials):
665
  cred = credentials[idx - 1]
666
  cred_id = cred.get("identifier", "")
 
738
  f"[dim]{stats_line}[/dim]",
739
  ]
740
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  # Model groups (for providers with quota tracking)
742
  model_groups = cred.get("model_groups", {})
743
+
744
+ # Show cooldowns grouped by quota group (if model_groups exist)
745
+ if model_cooldowns:
746
+ if model_groups:
747
+ # Group cooldowns by quota group
748
+ group_cooldowns: Dict[
749
+ str, int
750
+ ] = {} # group_name -> max_remaining_seconds
751
+ ungrouped_cooldowns: List[Tuple[str, int]] = []
752
+
753
+ for model_name, cooldown_info in model_cooldowns.items():
754
+ remaining = cooldown_info.get("remaining_seconds", 0)
755
+ if remaining <= 0:
756
+ continue
757
+
758
+ # Find which group this model belongs to
759
+ clean_model = model_name.split("/")[-1]
760
+ found_group = None
761
+ for group_name, group_info in model_groups.items():
762
+ group_models = group_info.get("models", [])
763
+ if clean_model in group_models:
764
+ found_group = group_name
765
+ break
766
+
767
+ if found_group:
768
+ group_cooldowns[found_group] = max(
769
+ group_cooldowns.get(found_group, 0), remaining
770
+ )
771
+ else:
772
+ ungrouped_cooldowns.append((model_name, remaining))
773
+
774
+ if group_cooldowns or ungrouped_cooldowns:
775
+ content_lines.append("")
776
+ content_lines.append("[yellow]Active Cooldowns:[/yellow]")
777
+
778
+ # Show grouped cooldowns
779
+ for group_name in sorted(group_cooldowns.keys()):
780
+ remaining = group_cooldowns[group_name]
781
+ content_lines.append(
782
+ f" [yellow]⏱️ {group_name}: {format_cooldown(remaining)}[/yellow]"
783
+ )
784
+
785
+ # Show ungrouped (shouldn't happen often)
786
+ for model_name, remaining in ungrouped_cooldowns:
787
+ short_model = model_name.split("/")[-1][:35]
788
+ content_lines.append(
789
+ f" [yellow]⏱️ {short_model}: {format_cooldown(remaining)}[/yellow]"
790
+ )
791
+ else:
792
+ # No model groups - show per-model cooldowns
793
+ content_lines.append("")
794
+ content_lines.append("[yellow]Active Cooldowns:[/yellow]")
795
+ for model_name, cooldown_info in model_cooldowns.items():
796
+ remaining = cooldown_info.get("remaining_seconds", 0)
797
+ if remaining > 0:
798
+ short_model = model_name.split("/")[-1][:35]
799
+ content_lines.append(
800
+ f" [yellow]⏱️ {short_model}: {format_cooldown(int(remaining))}[/yellow]"
801
+ )
802
+
803
+ # Display model groups with quota info
804
  if model_groups:
805
  content_lines.append("")
806
  for group_name, group_stats in model_groups.items():
src/rotator_library/client.py CHANGED
@@ -2664,27 +2664,25 @@ class RotatingClient:
2664
  models_data = cred.get("models", {})
2665
  group_stats["credentials_total"] += 1
2666
 
2667
- # Find any model from this group
 
2668
  for model in group_models:
2669
- # Try with and without provider prefix
2670
- prefixed_model = f"{provider}/{model}"
2671
- model_stats = models_data.get(
2672
- prefixed_model
2673
- ) or models_data.get(model)
2674
-
2675
  if model_stats:
2676
- baseline = model_stats.get(
2677
- "baseline_remaining_fraction"
2678
- )
2679
- if baseline is not None:
2680
- remaining_pct = int(baseline * 100)
2681
- group_stats["total_remaining_pcts"].append(
2682
- remaining_pct
2683
- )
2684
- if baseline <= 0:
2685
- group_stats["credentials_exhausted"] += 1
2686
  break
2687
 
 
 
 
 
 
 
 
 
 
 
2688
  # Calculate average remaining percentage
2689
  if group_stats["total_remaining_pcts"]:
2690
  group_stats["avg_remaining_pct"] = int(
@@ -2701,56 +2699,53 @@ class RotatingClient:
2701
  models_data = cred.get("models", {})
2702
 
2703
  for group_name, group_models in quota_groups.items():
2704
- # Find representative model from this group
 
2705
  for model in group_models:
2706
- prefixed_model = f"{provider}/{model}"
2707
- model_stats = models_data.get(
2708
- prefixed_model
2709
- ) or models_data.get(model)
2710
-
2711
  if model_stats:
2712
- baseline = model_stats.get(
2713
- "baseline_remaining_fraction"
2714
- )
2715
- max_req = model_stats.get("quota_max_requests")
2716
- req_count = model_stats.get("request_count", 0)
2717
- reset_ts = model_stats.get("quota_reset_ts")
2718
-
2719
- remaining_pct = (
2720
- int(baseline * 100)
2721
- if baseline is not None
2722
- else None
2723
- )
2724
- is_exhausted = baseline is not None and baseline <= 0
2725
-
2726
- # Format reset time
2727
- reset_iso = None
2728
- if reset_ts:
2729
- try:
2730
- from datetime import datetime, timezone
2731
-
2732
- reset_iso = datetime.fromtimestamp(
2733
- reset_ts, tz=timezone.utc
2734
- ).isoformat()
2735
- except (ValueError, OSError):
2736
- pass
2737
-
2738
- cred["model_groups"][group_name] = {
2739
- "remaining_pct": remaining_pct,
2740
- "requests_used": req_count,
2741
- "requests_max": max_req,
2742
- "display": f"{req_count}/{max_req}"
2743
- if max_req
2744
- else f"{req_count}/?",
2745
- "is_exhausted": is_exhausted,
2746
- "reset_time_iso": reset_iso,
2747
- "models": group_models,
2748
- "confidence": self._get_baseline_confidence(
2749
- model_stats
2750
- ),
2751
- }
2752
  break
2753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2754
  # Try to get email from provider's cache
2755
  cred_path = cred.get("full_path", "")
2756
  if hasattr(provider_instance, "project_tier_cache"):
@@ -2760,6 +2755,46 @@ class RotatingClient:
2760
 
2761
  return stats
2762
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2763
  def _get_baseline_confidence(self, model_stats: Dict) -> str:
2764
  """
2765
  Determine confidence level based on baseline age.
 
2664
  models_data = cred.get("models", {})
2665
  group_stats["credentials_total"] += 1
2666
 
2667
+ # Find any model from this group (try all with alias fallback)
2668
+ model_stats = None
2669
  for model in group_models:
2670
+ model_stats = self._find_model_stats_in_data(
2671
+ models_data, model, provider, provider_instance
2672
+ )
 
 
 
2673
  if model_stats:
 
 
 
 
 
 
 
 
 
 
2674
  break
2675
 
2676
+ if model_stats:
2677
+ baseline = model_stats.get("baseline_remaining_fraction")
2678
+ if baseline is not None:
2679
+ remaining_pct = int(baseline * 100)
2680
+ group_stats["total_remaining_pcts"].append(
2681
+ remaining_pct
2682
+ )
2683
+ if baseline <= 0:
2684
+ group_stats["credentials_exhausted"] += 1
2685
+
2686
  # Calculate average remaining percentage
2687
  if group_stats["total_remaining_pcts"]:
2688
  group_stats["avg_remaining_pct"] = int(
 
2699
  models_data = cred.get("models", {})
2700
 
2701
  for group_name, group_models in quota_groups.items():
2702
+ # Find representative model from this group (try all with alias fallback)
2703
+ model_stats = None
2704
  for model in group_models:
2705
+ model_stats = self._find_model_stats_in_data(
2706
+ models_data, model, provider, provider_instance
2707
+ )
 
 
2708
  if model_stats:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2709
  break
2710
 
2711
+ if model_stats:
2712
+ baseline = model_stats.get("baseline_remaining_fraction")
2713
+ max_req = model_stats.get("quota_max_requests")
2714
+ req_count = model_stats.get("request_count", 0)
2715
+ reset_ts = model_stats.get("quota_reset_ts")
2716
+
2717
+ remaining_pct = (
2718
+ int(baseline * 100) if baseline is not None else None
2719
+ )
2720
+ is_exhausted = baseline is not None and baseline <= 0
2721
+
2722
+ # Format reset time
2723
+ reset_iso = None
2724
+ if reset_ts:
2725
+ try:
2726
+ from datetime import datetime, timezone
2727
+
2728
+ reset_iso = datetime.fromtimestamp(
2729
+ reset_ts, tz=timezone.utc
2730
+ ).isoformat()
2731
+ except (ValueError, OSError):
2732
+ pass
2733
+
2734
+ cred["model_groups"][group_name] = {
2735
+ "remaining_pct": remaining_pct,
2736
+ "requests_used": req_count,
2737
+ "requests_max": max_req,
2738
+ "display": f"{req_count}/{max_req}"
2739
+ if max_req
2740
+ else f"{req_count}/?",
2741
+ "is_exhausted": is_exhausted,
2742
+ "reset_time_iso": reset_iso,
2743
+ "models": group_models,
2744
+ "confidence": self._get_baseline_confidence(
2745
+ model_stats
2746
+ ),
2747
+ }
2748
+
2749
  # Try to get email from provider's cache
2750
  cred_path = cred.get("full_path", "")
2751
  if hasattr(provider_instance, "project_tier_cache"):
 
2755
 
2756
  return stats
2757
 
2758
+ def _find_model_stats_in_data(
2759
+ self,
2760
+ models_data: Dict[str, Any],
2761
+ model: str,
2762
+ provider: str,
2763
+ provider_instance: Any,
2764
+ ) -> Optional[Dict[str, Any]]:
2765
+ """
2766
+ Find model stats in models_data, trying various name variants.
2767
+
2768
+ Handles aliased model names (e.g., gemini-3-pro-preview -> gemini-3-pro-high)
2769
+ by using the provider's _user_to_api_model() mapping.
2770
+
2771
+ Args:
2772
+ models_data: Dict of model_name -> stats from credential
2773
+ model: Model name to look up (user-facing name)
2774
+ provider: Provider name for prefixing
2775
+ provider_instance: Provider instance for alias methods
2776
+
2777
+ Returns:
2778
+ Model stats dict if found, None otherwise
2779
+ """
2780
+ # Try direct match with and without provider prefix
2781
+ prefixed_model = f"{provider}/{model}"
2782
+ model_stats = models_data.get(prefixed_model) or models_data.get(model)
2783
+
2784
+ if model_stats:
2785
+ return model_stats
2786
+
2787
+ # Try with API model name (e.g., gemini-3-pro-preview -> gemini-3-pro-high)
2788
+ if hasattr(provider_instance, "_user_to_api_model"):
2789
+ api_model = provider_instance._user_to_api_model(model)
2790
+ if api_model != model:
2791
+ prefixed_api = f"{provider}/{api_model}"
2792
+ model_stats = models_data.get(prefixed_api) or models_data.get(
2793
+ api_model
2794
+ )
2795
+
2796
+ return model_stats
2797
+
2798
  def _get_baseline_confidence(self, model_stats: Dict) -> str:
2799
  """
2800
  Determine confidence level based on baseline age.
src/rotator_library/usage_manager.py CHANGED
@@ -392,6 +392,49 @@ class UsageManager:
392
  # Not grouped - return individual model usage (no weight applied)
393
  return self._get_usage_count(key, model, usage_field)
394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  def _get_usage_field_name(self, credential: str) -> str:
396
  """
397
  Get the usage tracking field name for a credential.
@@ -1285,9 +1328,10 @@ class UsageManager:
1285
  if credential_tier_names
1286
  else "unknown"
1287
  )
 
1288
  lib_logger.info(
1289
  f"Acquired key {mask_credential(key)} for model {model} "
1290
- f"(tier: {tier_name}, priority: {priority_level}, selection: {selection_method}, usage: {usage})"
1291
  )
1292
  return key
1293
 
@@ -1303,9 +1347,10 @@ class UsageManager:
1303
  if credential_tier_names
1304
  else "unknown"
1305
  )
 
1306
  lib_logger.info(
1307
  f"Acquired key {mask_credential(key)} for model {model} "
1308
- f"(tier: {tier_name}, priority: {priority_level}, selection: {selection_method}, concurrent: {state['models_in_use'][model]}/{effective_max_concurrent}, usage: {usage})"
1309
  )
1310
  return key
1311
 
@@ -1421,9 +1466,10 @@ class UsageManager:
1421
  else None
1422
  )
1423
  tier_info = f"tier: {tier_name}, " if tier_name else ""
 
1424
  lib_logger.info(
1425
  f"Acquired key {mask_credential(key)} for model {model} "
1426
- f"({tier_info}selection: {selection_method}, usage: {usage})"
1427
  )
1428
  return key
1429
 
@@ -1440,9 +1486,10 @@ class UsageManager:
1440
  else None
1441
  )
1442
  tier_info = f"tier: {tier_name}, " if tier_name else ""
 
1443
  lib_logger.info(
1444
  f"Acquired key {mask_credential(key)} for model {model} "
1445
- f"({tier_info}selection: {selection_method}, concurrent: {state['models_in_use'][model]}/{effective_max_concurrent}, usage: {usage})"
1446
  )
1447
  return key
1448
 
 
392
  # Not grouped - return individual model usage (no weight applied)
393
  return self._get_usage_count(key, model, usage_field)
394
 
395
+ def _get_quota_display(self, key: str, model: str) -> str:
396
+ """
397
+ Get a formatted quota display string for logging.
398
+
399
+ For antigravity (providers in _REQUEST_COUNT_PROVIDERS), returns:
400
+ "quota: 170/250 [32%]" format
401
+
402
+ For other providers, returns:
403
+ "usage: 170" format (no max available)
404
+
405
+ Args:
406
+ key: Credential identifier
407
+ model: Model name (with provider prefix)
408
+
409
+ Returns:
410
+ Formatted string for logging
411
+ """
412
+ provider = self._get_provider_from_credential(key)
413
+
414
+ if provider not in self._REQUEST_COUNT_PROVIDERS:
415
+ # Non-antigravity: just show usage count
416
+ usage = self._get_usage_count(key, model, "success_count")
417
+ return f"usage: {usage}"
418
+
419
+ # Antigravity: show quota display with remaining percentage
420
+ if self._usage_data is None:
421
+ return "quota: 0/? [100%]"
422
+
423
+ key_data = self._usage_data.get(key, {})
424
+ model_data = key_data.get("models", {}).get(model, {})
425
+
426
+ request_count = model_data.get("request_count", 0)
427
+ max_requests = model_data.get("quota_max_requests")
428
+
429
+ if max_requests:
430
+ remaining = max_requests - request_count
431
+ remaining_pct = (
432
+ int((remaining / max_requests) * 100) if max_requests > 0 else 0
433
+ )
434
+ return f"quota: {request_count}/{max_requests} [{remaining_pct}%]"
435
+ else:
436
+ return f"quota: {request_count}"
437
+
438
  def _get_usage_field_name(self, credential: str) -> str:
439
  """
440
  Get the usage tracking field name for a credential.
 
1328
  if credential_tier_names
1329
  else "unknown"
1330
  )
1331
+ quota_display = self._get_quota_display(key, model)
1332
  lib_logger.info(
1333
  f"Acquired key {mask_credential(key)} for model {model} "
1334
+ f"(tier: {tier_name}, priority: {priority_level}, selection: {selection_method}, {quota_display})"
1335
  )
1336
  return key
1337
 
 
1347
  if credential_tier_names
1348
  else "unknown"
1349
  )
1350
+ quota_display = self._get_quota_display(key, model)
1351
  lib_logger.info(
1352
  f"Acquired key {mask_credential(key)} for model {model} "
1353
+ f"(tier: {tier_name}, priority: {priority_level}, selection: {selection_method}, concurrent: {state['models_in_use'][model]}/{effective_max_concurrent}, {quota_display})"
1354
  )
1355
  return key
1356
 
 
1466
  else None
1467
  )
1468
  tier_info = f"tier: {tier_name}, " if tier_name else ""
1469
+ quota_display = self._get_quota_display(key, model)
1470
  lib_logger.info(
1471
  f"Acquired key {mask_credential(key)} for model {model} "
1472
+ f"({tier_info}selection: {selection_method}, {quota_display})"
1473
  )
1474
  return key
1475
 
 
1486
  else None
1487
  )
1488
  tier_info = f"tier: {tier_name}, " if tier_name else ""
1489
+ quota_display = self._get_quota_display(key, model)
1490
  lib_logger.info(
1491
  f"Acquired key {mask_credential(key)} for model {model} "
1492
+ f"({tier_info}selection: {selection_method}, concurrent: {state['models_in_use'][model]}/{effective_max_concurrent}, {quota_display})"
1493
  )
1494
  return key
1495