diff --git a/app/services/monitoring.py b/app/services/monitoring.py index 9ea73e5..9aeb4b3 100644 --- a/app/services/monitoring.py +++ b/app/services/monitoring.py @@ -1,6 +1,6 @@ import json from datetime import datetime, timedelta, timezone -from typing import Any, Dict +from typing import Any, Dict, Tuple import requests from sqlalchemy.orm import Session @@ -66,21 +66,69 @@ def test_provider_connection(provider: str, credential: str): return False, str(e) +def _openai_usage(credential: str) -> Tuple[str, Dict[str, Any]]: + # Uses legacy billing endpoints; may be disabled in some orgs. + headers = _provider_headers('openai', credential) + today = _now().date() + start = (today - timedelta(days=7)).isoformat() + end = today.isoformat() + usage_url = f'https://api.openai.com/v1/dashboard/billing/usage?start_date={start}&end_date={end}' + sub_url = 'https://api.openai.com/v1/dashboard/billing/subscription' + u = requests.get(usage_url, headers=headers, timeout=12) + s = requests.get(sub_url, headers=headers, timeout=12) + if u.status_code != 200 or s.status_code != 200: + return 'error', {'error': f'usage:{u.status_code}, subscription:{s.status_code}'} + usage = u.json() + sub = s.json() + total_usage = usage.get('total_usage') + hard_limit = sub.get('hard_limit_usd') + reset_at = sub.get('billing_cycle_anchor') + if reset_at: + reset_at = datetime.fromtimestamp(reset_at, tz=timezone.utc).isoformat() + usage_pct = None + if total_usage is not None and hard_limit: + usage_pct = round(total_usage / hard_limit * 100, 2) + return 'ok', { + 'window_label': '7d', + 'used': total_usage, + 'limit': hard_limit, + 'usage_pct': usage_pct, + 'reset_at': reset_at, + 'raw': {'usage': usage, 'subscription': sub}, + } + + +def _anthropic_usage(credential: str) -> Tuple[str, Dict[str, Any]]: + # Anthropic usage endpoints are enterprise-specific; keep placeholder. + # We return accepted status; detail will be filled once API confirmed. + return 'unsupported', {'error': 'anthropic usage API not configured'} + + def refresh_provider_usage_once(db: Session): accounts = db.query(ProviderAccount).filter(ProviderAccount.is_enabled == True).all() now = _now() for a in accounts: - ok, msg = test_provider_connection(a.provider, a.credential) + status = 'pending' + payload: Dict[str, Any] = {} + if a.provider == 'openai': + status, payload = _openai_usage(a.credential) + elif a.provider == 'anthropic': + status, payload = _anthropic_usage(a.credential) + else: + ok, msg = test_provider_connection(a.provider, a.credential) + status = 'ok' if ok else 'error' + payload = {'error': None if ok else msg} + snap = ProviderUsageSnapshot( account_id=a.id, - window_label='provider-default', - used=None, - limit=None, - usage_pct=None, - reset_at=None, - status='ok' if ok else 'error', - error=None if ok else msg, - raw_payload=json.dumps({'message': msg}, ensure_ascii=False), + window_label=payload.get('window_label'), + used=payload.get('used'), + limit=payload.get('limit'), + usage_pct=payload.get('usage_pct'), + reset_at=payload.get('reset_at'), + status=status, + error=payload.get('error'), + raw_payload=json.dumps(payload.get('raw') or payload, ensure_ascii=False), fetched_at=now, ) db.add(snap) @@ -100,7 +148,7 @@ def get_provider_usage_view(db: Session): 'usage_pct': snap.usage_pct if snap else None, 'used': snap.used if snap else None, 'limit': snap.limit if snap else None, - 'reset_at': snap.reset_at.isoformat() if snap and snap.reset_at else None, + 'reset_at': snap.reset_at if snap else None, 'status': snap.status if snap else 'pending', 'error': snap.error if snap else None, 'fetched_at': snap.fetched_at.isoformat() if snap and snap.fetched_at else None,