Skip to content

Commit 0945dfc

Browse files
committed
feat(firmware): credit balance tracking and dollar displays
1 parent 4a9fe38 commit 0945dfc

3 files changed

Lines changed: 82 additions & 49 deletions

File tree

src/rotator_library/client/rotating_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,25 @@ async def get_quota_stats(
524524

525525
stats = await manager.get_stats_for_endpoint()
526526

527+
# Filter out stale quota groups that no longer exist in the provider's
528+
# current model_quota_groups (e.g. after a group rename like
529+
# firmware_global → credits($))
530+
plugin = self._get_provider_instance(provider)
531+
if plugin and hasattr(plugin, "model_quota_groups"):
532+
valid_groups = set(plugin.model_quota_groups.keys())
533+
if valid_groups:
534+
stale_groups = [
535+
g
536+
for g in stats.get("quota_groups", {})
537+
if g not in valid_groups
538+
]
539+
for g in stale_groups:
540+
del stats["quota_groups"][g]
541+
# Also clean stale groups from per-credential group_usage
542+
for cred_data in stats.get("credentials", {}).values():
543+
for g in stale_groups:
544+
cred_data.get("group_usage", {}).pop(g, None)
545+
527546
# Skip providers with no activity AND no quota data
528547
# (filters out invalid/unused providers, but keeps quota-tracked providers visible)
529548
has_requests = stats.get("total_requests", 0) > 0

src/rotator_library/providers/firmware_provider.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
22
Firmware.ai Provider with Quota Tracking
33
4-
Provider implementation for the Firmware.ai API with 5-hour rolling window quota tracking.
5-
Uses the FirmwareQuotaTracker mixin to fetch quota usage from their API.
4+
Provider implementation for the Firmware.ai API with credit-based quota tracking.
5+
Uses the FirmwareQuotaTracker mixin to fetch credit balance from their API.
66
77
Environment variables:
88
FIRMWARE_API_BASE: API base URL (default: https://app.firmware.ai/api/v1)
@@ -34,10 +34,12 @@ class FirmwareProvider(FirmwareQuotaTracker, ProviderInterface):
3434
Provider implementation for the Firmware.ai API with quota tracking.
3535
"""
3636

37-
# Quota groups for tracking 5-hour rolling window limits
37+
# Quota groups for tracking credit-based limits
3838
# Uses a virtual model "firmware/_quota" for credential-level quota tracking
39+
# Single quota group: all models share the same credential-level credit balance.
40+
# Named 'credits($)' so the TUI auto-formats values as dollars.
3941
model_quota_groups = {
40-
"firmware_global": ["firmware/_quota"],
42+
"credits($)": ["firmware/_quota"],
4143
}
4244

4345
def __init__(self, *args, **kwargs):
@@ -74,22 +76,22 @@ def get_model_quota_group(self, model: str) -> Optional[str]:
7476
Returns:
7577
Quota group identifier for shared credential-level tracking
7678
"""
77-
return "firmware_global"
79+
return "credits($)"
7880

7981
def get_models_in_quota_group(self, group: str) -> List[str]:
8082
"""
8183
Get all models in a quota group.
8284
8385
For Firmware.ai, we use a virtual model "firmware/_quota" to track the
84-
credential-level 5-hour rolling window quota.
86+
credential-level credit-based quota.
8587
8688
Args:
8789
group: Quota group name
8890
8991
Returns:
9092
List of model names in the group
9193
"""
92-
if group == "firmware_global":
94+
if group == "credits($)":
9395
return ["firmware/_quota"]
9496
return []
9597

@@ -98,17 +100,16 @@ def get_usage_reset_config(self, credential: str) -> Optional[Dict[str, Any]]:
98100
Return usage reset configuration for Firmware.ai credentials.
99101
100102
Firmware.ai uses per_model mode to track usage at the model level,
101-
with 5-hour rolling window quotas managed via the background job.
103+
with credit-based quota managed via the background job.
102104
103105
Args:
104106
credential: The API key (unused, same config for all)
105107
106108
Returns:
107-
Configuration with per_model mode and 5-hour window
109+
Configuration with per_model mode
108110
"""
109111
return {
110112
"mode": "per_model",
111-
"window_seconds": 18000, # 5 hours (5-hour rolling window)
112113
"field_name": "models",
113114
}
114115

@@ -179,12 +180,16 @@ async def refresh_single_credential(
179180
self._quota_cache[api_key] = usage_data
180181

181182
# Calculate values for usage manager
183+
credits_balance = usage_data.get("credits", 0.0)
182184
remaining_fraction = usage_data.get("remaining_fraction", 0.0)
183-
reset_ts = usage_data.get("reset_at")
184185

185186
# Store baseline in usage manager
186187
# Since Firmware.ai uses credential-level quota, we use a virtual model name
187-
if remaining_fraction <= 0.0 and reset_ts:
188+
# Express credits as synthetic request-count values (in cents)
189+
# so the quota group window gets a non-zero limit for visibility
190+
credits_as_cents = int(credits_balance * 100)
191+
192+
if remaining_fraction <= 0.0:
188193
stable_id = usage_manager.registry.get_stable_id(
189194
api_key, usage_manager.provider
190195
)
@@ -193,20 +198,22 @@ async def refresh_single_credential(
193198
await usage_manager.tracking.apply_cooldown(
194199
state=state,
195200
reason="quota_exhausted",
196-
until=reset_ts,
201+
duration=3600, # 1 hour cooldown
197202
model_or_group="firmware/_quota",
198203
source="api_quota",
199204
)
200205
await usage_manager.update_quota_baseline(
201206
api_key,
202207
"firmware/_quota", # Virtual model for credential-level tracking
203-
quota_reset_ts=reset_ts,
208+
quota_max_requests=credits_as_cents,
209+
quota_used=0,
210+
quota_reset_ts=None,
211+
force=True,
204212
)
205213

206214
lib_logger.debug(
207215
f"Updated Firmware.ai quota baseline: "
208-
f"{remaining_fraction * 100:.1f}% remaining, "
209-
f"active_window={usage_data.get('has_active_window', False)}"
216+
f"{credits_balance:.2f} credits remaining"
210217
)
211218

212219
except Exception as e:
@@ -220,3 +227,6 @@ async def refresh_single_credential(
220227
refresh_single_credential(api_key, client) for api_key in credentials
221228
]
222229
await asyncio.gather(*tasks, return_exceptions=True)
230+
231+
232+

src/rotator_library/providers/utilities/firmware_quota_tracker.py

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
Firmware.ai Quota Tracking Mixin
33
44
Provides quota tracking for the Firmware.ai provider using their quota usage API.
5-
Firmware.ai uses a 5-hour rolling window quota system where:
6-
- `used` is already a ratio (0 to 1) indicating quota utilization
7-
- `reset` is an ISO 8601 UTC timestamp, or null when no active window
5+
Firmware.ai uses a credit-based quota system where:
6+
- `credits` is the remaining credit balance (absolute value, e.g. 31.95)
87
98
API Details:
109
- Endpoint: GET https://app.firmware.ai/api/v1/quota
1110
- Auth: Authorization: Bearer <api_key>
12-
- Response: { used: float, reset: string|null }
11+
- Response: { credits: float }
1312
1413
Required from provider:
1514
- self.api_base: str (API base URL)
@@ -33,9 +32,8 @@ class FirmwareQuotaTracker:
3332
Mixin class providing quota tracking functionality for Firmware.ai provider.
3433
3534
This mixin adds the following capabilities:
36-
- Fetch quota usage from the Firmware.ai API
37-
- Track 5-hour rolling window quota limits
38-
- Parse ISO 8601 reset timestamps
35+
- Fetch credit balance from the Firmware.ai API
36+
- Track remaining credits
3937
4038
Usage:
4139
class FirmwareProvider(FirmwareQuotaTracker, ProviderInterface):
@@ -76,10 +74,10 @@ async def fetch_quota_usage(
7674
{
7775
"status": "success" | "error",
7876
"error": str | None,
79-
"used": float, # 0.0 to 1.0 (from API directly)
80-
"remaining_fraction": float, # 1.0 - used
81-
"reset_at": float | None, # Unix timestamp (seconds)
82-
"has_active_window": bool, # True if reset is not null
77+
"credits": float, # Remaining credit balance
78+
"remaining_fraction": float, # 1.0 if credits > 0, else 0.0
79+
"reset_at": float | None, # Always None (no rolling window)
80+
"has_active_window": bool, # Always False (credit-based)
8381
"fetched_at": float,
8482
}
8583
"""
@@ -103,35 +101,30 @@ async def fetch_quota_usage(
103101
response.raise_for_status()
104102
data = response.json()
105103

106-
# Parse response - API returns ratio directly
107-
used_raw = data.get("used")
108-
# Validate used is numeric
109-
if not isinstance(used_raw, (int, float)):
104+
# Parse response - API returns absolute credit balance
105+
credits_raw = data.get("credits")
106+
# Validate credits is numeric
107+
if not isinstance(credits_raw, (int, float)):
110108
lib_logger.warning(
111-
f"Firmware.ai quota API returned non-numeric 'used' value: {used_raw}"
109+
f"Firmware.ai quota API returned non-numeric 'credits' value: {credits_raw}"
112110
)
113-
used = 0.0
111+
credits_balance = 0.0
114112
else:
115-
used = float(used_raw)
116-
reset_iso = data.get("reset")
113+
credits_balance = float(credits_raw)
117114

118-
# Calculate remaining (inverse of used), clamped to 0.0-1.0
119-
remaining_fraction = max(0.0, min(1.0, 1.0 - used))
115+
# Credit-based system: available if credits > 0
116+
# remaining_fraction is 1.0 if credits available, 0.0 if exhausted
117+
remaining_fraction = 1.0 if credits_balance > 0.0 else 0.0
120118

121-
# Parse ISO 8601 reset timestamp
122-
reset_at = None
123-
if reset_iso is not None:
124-
reset_at = self._parse_iso_timestamp(reset_iso)
125-
# Only mark active window if we successfully parsed the timestamp
126-
has_active_window = reset_at is not None
119+
# Parse optional reset/reload date (ISO 8601 string)
120+
reset_date = data.get("reset") # e.g., "2023-04-01T00:00:00.000Z"
127121

128122
return {
129123
"status": "success",
130124
"error": None,
131-
"used": used,
125+
"credits": credits_balance,
132126
"remaining_fraction": remaining_fraction,
133-
"reset_at": reset_at,
134-
"has_active_window": has_active_window,
127+
"reset_date": reset_date, # ISO 8601 string or None
135128
"fetched_at": time.time(),
136129
}
137130

@@ -193,10 +186,22 @@ def get_remaining_fraction(self, usage_data: Dict[str, Any]) -> float:
193186
usage_data: Response from fetch_quota_usage()
194187
195188
Returns:
196-
Remaining fraction (0.0 to 1.0)
189+
Remaining fraction (0.0 or 1.0 for credit-based system)
197190
"""
198191
return usage_data.get("remaining_fraction", 0.0)
199192

193+
def get_credits_balance(self, usage_data: Dict[str, Any]) -> float:
194+
"""
195+
Get the remaining credit balance from usage data.
196+
197+
Args:
198+
usage_data: Response from fetch_quota_usage()
199+
200+
Returns:
201+
Remaining credits (absolute value)
202+
"""
203+
return usage_data.get("credits", 0.0)
204+
200205
def get_reset_timestamp(self, usage_data: Dict[str, Any]) -> Optional[float]:
201206
"""
202207
Get the next reset timestamp from usage data.
@@ -235,8 +240,7 @@ async def refresh_quota_usage(
235240

236241
lib_logger.debug(
237242
f"Firmware.ai quota for {credential_identifier}: "
238-
f"{usage_data['remaining_fraction'] * 100:.1f}% remaining, "
239-
f"active_window={usage_data['has_active_window']}"
243+
f"{usage_data.get('credits', 0.0):.2f} credits remaining"
240244
)
241245

242246
return usage_data

0 commit comments

Comments
 (0)