AIstudioProxyAPI / api_utils /utils_ext /usage_tracker.py
peijun1's picture
Deploy AI Studio Proxy API to Hugging Face Spaces
a5784e9
Raw
History Blame Contribute Delete
3.37 kB
import asyncio
import json
import logging
import os
from typing import Dict
logger = logging.getLogger("UsageTracker")
USAGE_FILE = os.path.join("config", "profile_usage.json")
_USAGE_LOCK = asyncio.Lock()
def _load_usage_data() -> Dict[str, int]:
"""Internal function to load usage data from disk."""
if not os.path.exists(USAGE_FILE):
return {}
try:
with open(USAGE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.error(f"Failed to load profile usage data: {e}")
return {}
def _save_usage_data(data: Dict[str, int]) -> None:
"""Internal function to save usage data to disk."""
try:
# Ensure directory exists
os.makedirs(os.path.dirname(USAGE_FILE), exist_ok=True)
with open(USAGE_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
except IOError as e:
logger.error(f"Failed to save profile usage data: {e}")
async def increment_profile_usage(profile_path: str, tokens: int) -> None:
"""
Increments the usage count for a specific profile.
This function is async and thread-safe via asyncio.Lock.
"""
if not profile_path or not os.path.exists(profile_path):
return
# Normalize path to ensure consistency as key
profile_path = os.path.abspath(profile_path)
async with _USAGE_LOCK:
usage_data = _load_usage_data()
# Smart path reconciliation: Check if we have data for this file under a different path
target_key = profile_path
if profile_path not in usage_data:
profile_basename = os.path.basename(profile_path)
for key in list(usage_data.keys()):
if os.path.basename(key) == profile_basename:
# Found a match by filename! Migrate the data to the new path
logger.info(
f"Migrating usage data for '{profile_basename}' from '{key}' to '{profile_path}'"
)
usage_data[profile_path] = usage_data.pop(key)
target_key = profile_path
break
current_usage = usage_data.get(target_key, 0)
usage_data[target_key] = current_usage + tokens
_save_usage_data(usage_data)
logger.debug(
f"Updated usage for {os.path.basename(profile_path)}: +{tokens} tokens (Total: {usage_data[target_key]})"
)
def get_profile_usage(profile_path: str) -> int:
"""
Returns the total usage for a profile.
Reads directly from file (blocking), suitable for auth rotation logic which might run in sync context or low frequency.
For high frequency, consider caching.
Includes fallback logic to find usage by filename if absolute path doesn't match (handles moved files).
"""
profile_path = os.path.abspath(profile_path)
usage_data = _load_usage_data()
# Direct match
if profile_path in usage_data:
return usage_data[profile_path]
# Fallback: Match by filename
# This handles cases where files are moved (e.g. saved -> emergency) but usage data has old path
profile_basename = os.path.basename(profile_path)
for key, usage in usage_data.items():
if os.path.basename(key) == profile_basename:
return usage
return 0