Spaces:
Paused
feat(usage): ✨ add human-readable timestamp fields to usage data for debugging
Browse filesThis commit introduces helper methods to automatically generate and persist human-readable timestamp fields alongside Unix timestamps in the usage tracking data.
- Add `_format_timestamp_local()` method to convert Unix timestamps to local time strings with timezone offset
- Add `_add_readable_timestamps()` method to enrich usage data with 'window_started' and 'quota_resets' fields
- Integrate timestamp formatting into the save flow, automatically updating readable fields before persisting to disk
- Set `quota_reset_ts` when initializing new model windows based on provider's window configuration
The readable timestamps improve observability and debugging by making it easier to understand when quota windows started and when they will reset, without requiring manual timestamp conversion.
|
@@ -297,6 +297,69 @@ class UsageManager:
|
|
| 297 |
.get("success_count", 0)
|
| 298 |
)
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
def _select_sequential(
|
| 301 |
self,
|
| 302 |
candidates: List[Tuple[str, int]],
|
|
@@ -377,6 +440,8 @@ class UsageManager:
|
|
| 377 |
if self._usage_data is None:
|
| 378 |
return
|
| 379 |
async with self._data_lock:
|
|
|
|
|
|
|
| 380 |
async with aiofiles.open(self.file_path, "w") as f:
|
| 381 |
await f.write(json.dumps(self._usage_data, indent=2))
|
| 382 |
|
|
@@ -1251,11 +1316,15 @@ class UsageManager:
|
|
| 1251 |
# Start window on first request for this model
|
| 1252 |
if model_data.get("window_start_ts") is None:
|
| 1253 |
model_data["window_start_ts"] = now_ts
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
else 0
|
| 1258 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1259 |
lib_logger.info(
|
| 1260 |
f"Started {window_hours:.1f}h window for model {model} on {mask_credential(key)}"
|
| 1261 |
)
|
|
|
|
| 297 |
.get("success_count", 0)
|
| 298 |
)
|
| 299 |
|
| 300 |
+
# =========================================================================
|
| 301 |
+
# TIMESTAMP FORMATTING HELPERS
|
| 302 |
+
# =========================================================================
|
| 303 |
+
|
| 304 |
+
def _format_timestamp_local(self, ts: Optional[float]) -> Optional[str]:
|
| 305 |
+
"""
|
| 306 |
+
Format Unix timestamp as local time string with timezone offset.
|
| 307 |
+
|
| 308 |
+
Args:
|
| 309 |
+
ts: Unix timestamp or None
|
| 310 |
+
|
| 311 |
+
Returns:
|
| 312 |
+
Formatted string like "2025-12-07 14:30:17 +0100" or None
|
| 313 |
+
"""
|
| 314 |
+
if ts is None:
|
| 315 |
+
return None
|
| 316 |
+
try:
|
| 317 |
+
dt = datetime.fromtimestamp(ts).astimezone() # Local timezone
|
| 318 |
+
# Use UTC offset for conciseness (works on all platforms)
|
| 319 |
+
return dt.strftime("%Y-%m-%d %H:%M:%S %z")
|
| 320 |
+
except (OSError, ValueError, OverflowError):
|
| 321 |
+
return None
|
| 322 |
+
|
| 323 |
+
def _add_readable_timestamps(self, data: Dict) -> Dict:
|
| 324 |
+
"""
|
| 325 |
+
Add human-readable timestamp fields to usage data before saving.
|
| 326 |
+
|
| 327 |
+
Adds 'window_started' and 'quota_resets' fields derived from
|
| 328 |
+
Unix timestamps for easier debugging and monitoring.
|
| 329 |
+
|
| 330 |
+
Args:
|
| 331 |
+
data: The usage data dict to enhance
|
| 332 |
+
|
| 333 |
+
Returns:
|
| 334 |
+
The same dict with readable timestamp fields added
|
| 335 |
+
"""
|
| 336 |
+
for key, key_data in data.items():
|
| 337 |
+
# Handle per-model structure
|
| 338 |
+
models = key_data.get("models", {})
|
| 339 |
+
for model_name, model_stats in models.items():
|
| 340 |
+
if not isinstance(model_stats, dict):
|
| 341 |
+
continue
|
| 342 |
+
|
| 343 |
+
# Add readable window start time
|
| 344 |
+
window_start = model_stats.get("window_start_ts")
|
| 345 |
+
if window_start:
|
| 346 |
+
model_stats["window_started"] = self._format_timestamp_local(
|
| 347 |
+
window_start
|
| 348 |
+
)
|
| 349 |
+
elif "window_started" in model_stats:
|
| 350 |
+
del model_stats["window_started"]
|
| 351 |
+
|
| 352 |
+
# Add readable reset time
|
| 353 |
+
quota_reset = model_stats.get("quota_reset_ts")
|
| 354 |
+
if quota_reset:
|
| 355 |
+
model_stats["quota_resets"] = self._format_timestamp_local(
|
| 356 |
+
quota_reset
|
| 357 |
+
)
|
| 358 |
+
elif "quota_resets" in model_stats:
|
| 359 |
+
del model_stats["quota_resets"]
|
| 360 |
+
|
| 361 |
+
return data
|
| 362 |
+
|
| 363 |
def _select_sequential(
|
| 364 |
self,
|
| 365 |
candidates: List[Tuple[str, int]],
|
|
|
|
| 440 |
if self._usage_data is None:
|
| 441 |
return
|
| 442 |
async with self._data_lock:
|
| 443 |
+
# Add human-readable timestamp fields before saving
|
| 444 |
+
self._add_readable_timestamps(self._usage_data)
|
| 445 |
async with aiofiles.open(self.file_path, "w") as f:
|
| 446 |
await f.write(json.dumps(self._usage_data, indent=2))
|
| 447 |
|
|
|
|
| 1316 |
# Start window on first request for this model
|
| 1317 |
if model_data.get("window_start_ts") is None:
|
| 1318 |
model_data["window_start_ts"] = now_ts
|
| 1319 |
+
|
| 1320 |
+
# Set expected quota reset time from provider config
|
| 1321 |
+
window_seconds = (
|
| 1322 |
+
reset_config.get("window_seconds", 0) if reset_config else 0
|
| 1323 |
)
|
| 1324 |
+
if window_seconds > 0:
|
| 1325 |
+
model_data["quota_reset_ts"] = now_ts + window_seconds
|
| 1326 |
+
|
| 1327 |
+
window_hours = window_seconds / 3600 if window_seconds else 0
|
| 1328 |
lib_logger.info(
|
| 1329 |
f"Started {window_hours:.1f}h window for model {model} on {mask_credential(key)}"
|
| 1330 |
)
|