feat: add provider usage adapters for openai and placeholders for others
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user