feat: add provider usage adapters for openai and placeholders for others

This commit is contained in:
zhi
2026-03-11 13:08:58 +00:00
parent c0ec70c64f
commit 5b8f84d87d

View File

@@ -1,6 +1,6 @@
import json import json
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Dict from typing import Any, Dict, Tuple
import requests import requests
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -66,21 +66,69 @@ def test_provider_connection(provider: str, credential: str):
return False, str(e) 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): def refresh_provider_usage_once(db: Session):
accounts = db.query(ProviderAccount).filter(ProviderAccount.is_enabled == True).all() accounts = db.query(ProviderAccount).filter(ProviderAccount.is_enabled == True).all()
now = _now() now = _now()
for a in accounts: 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( snap = ProviderUsageSnapshot(
account_id=a.id, account_id=a.id,
window_label='provider-default', window_label=payload.get('window_label'),
used=None, used=payload.get('used'),
limit=None, limit=payload.get('limit'),
usage_pct=None, usage_pct=payload.get('usage_pct'),
reset_at=None, reset_at=payload.get('reset_at'),
status='ok' if ok else 'error', status=status,
error=None if ok else msg, error=payload.get('error'),
raw_payload=json.dumps({'message': msg}, ensure_ascii=False), raw_payload=json.dumps(payload.get('raw') or payload, ensure_ascii=False),
fetched_at=now, fetched_at=now,
) )
db.add(snap) db.add(snap)
@@ -100,7 +148,7 @@ def get_provider_usage_view(db: Session):
'usage_pct': snap.usage_pct if snap else None, 'usage_pct': snap.usage_pct if snap else None,
'used': snap.used if snap else None, 'used': snap.used if snap else None,
'limit': snap.limit 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', 'status': snap.status if snap else 'pending',
'error': snap.error if snap else None, 'error': snap.error if snap else None,
'fetched_at': snap.fetched_at.isoformat() if snap and snap.fetched_at else None, 'fetched_at': snap.fetched_at.isoformat() if snap and snap.fetched_at else None,