feat: support provider usage via configurable JSON credentials

This commit is contained in:
zhi
2026-03-11 13:13:07 +00:00
parent 5b8f84d87d
commit ff4baf6113

View File

@@ -15,6 +15,16 @@ def _now():
return datetime.now(timezone.utc)
def _parse_credential(raw: str) -> Dict[str, Any]:
raw = (raw or '').strip()
if raw.startswith('{'):
try:
return json.loads(raw)
except Exception:
return {'api_key': raw}
return {'api_key': raw}
def get_issue_stats_cached(db: Session, ttl_seconds: int = 1800):
key = 'issue_stats_24h'
now = _now()
@@ -41,25 +51,37 @@ def get_issue_stats_cached(db: Session, ttl_seconds: int = 1800):
return data
def _provider_headers(provider: str, credential: str):
def _provider_headers(provider: str, credential: str, extra: Dict[str, Any] | None = None):
extra = extra or {}
if extra.get('auth_header'):
val = extra.get('auth_value')
if not val:
scheme = extra.get('auth_scheme')
val = f"{scheme} {credential}" if scheme else credential
return {extra['auth_header']: val}
if provider == 'openai':
return {'Authorization': f'Bearer {credential}'}
if provider == 'anthropic':
return {'x-api-key': credential, 'anthropic-version': '2023-06-01'}
return None
return {}
def test_provider_connection(provider: str, credential: str):
provider = provider.lower()
info = _parse_credential(credential)
key = info.get('api_key') or credential
try:
if provider == 'openai':
r = requests.get('https://api.openai.com/v1/models', headers=_provider_headers(provider, credential), timeout=12)
r = requests.get('https://api.openai.com/v1/models', headers=_provider_headers(provider, key, info), timeout=12)
return r.status_code == 200, f'status={r.status_code}'
if provider == 'anthropic':
r = requests.get('https://api.anthropic.com/v1/models', headers=_provider_headers(provider, credential), timeout=12)
r = requests.get('https://api.anthropic.com/v1/models', headers=_provider_headers(provider, key, info), timeout=12)
return r.status_code == 200, f'status={r.status_code}'
usage_url = info.get('usage_url') or info.get('test_url')
if usage_url:
r = requests.get(usage_url, headers=_provider_headers(provider, key, info), timeout=12)
return r.status_code < 500, f'status={r.status_code}'
if provider in {'minimax', 'kimi', 'qwen'}:
# Endpoints/usage API vary by deployment; keep as accepted-but-unverified for now.
return True, 'accepted (connectivity check pending provider-specific adapter)'
return False, 'unsupported provider'
except Exception as e:
@@ -67,13 +89,14 @@ def test_provider_connection(provider: str, credential: str):
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)
info = _parse_credential(credential)
key = info.get('api_key') or credential
headers = _provider_headers('openai', key, info)
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'
usage_url = info.get('usage_url') or f'https://api.openai.com/v1/dashboard/billing/usage?start_date={start}&end_date={end}'
sub_url = info.get('subscription_url') or '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:
@@ -82,9 +105,8 @@ def _openai_usage(credential: str) -> Tuple[str, Dict[str, Any]]:
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()
reset_at_ts = sub.get('billing_cycle_anchor')
reset_at = datetime.fromtimestamp(reset_at_ts, tz=timezone.utc) if reset_at_ts else None
usage_pct = None
if total_usage is not None and hard_limit:
usage_pct = round(total_usage / hard_limit * 100, 2)
@@ -99,9 +121,44 @@ def _openai_usage(credential: str) -> Tuple[str, Dict[str, Any]]:
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.
info = _parse_credential(credential)
key = info.get('api_key') or credential
usage_url = info.get('usage_url')
if not usage_url:
return 'unsupported', {'error': 'anthropic usage API not configured'}
r = requests.get(usage_url, headers=_provider_headers('anthropic', key, info), timeout=12)
if r.status_code != 200:
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
payload = r.json()
# Try to normalize
return 'ok', {
'window_label': payload.get('window') or payload.get('window_label'),
'used': payload.get('used'),
'limit': payload.get('limit'),
'usage_pct': payload.get('usage_pct'),
'reset_at': None,
'raw': payload,
}
def _generic_usage(provider: str, credential: str) -> Tuple[str, Dict[str, Any]]:
info = _parse_credential(credential)
key = info.get('api_key') or credential
usage_url = info.get('usage_url')
if not usage_url:
return 'unsupported', {'error': f'{provider} usage API not configured'}
r = requests.get(usage_url, headers=_provider_headers(provider, key, info), timeout=12)
if r.status_code != 200:
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
payload = r.json()
return 'ok', {
'window_label': payload.get('window') or payload.get('window_label'),
'used': payload.get('used'),
'limit': payload.get('limit'),
'usage_pct': payload.get('usage_pct'),
'reset_at': None,
'raw': payload,
}
def refresh_provider_usage_once(db: Session):
@@ -114,6 +171,8 @@ def refresh_provider_usage_once(db: Session):
status, payload = _openai_usage(a.credential)
elif a.provider == 'anthropic':
status, payload = _anthropic_usage(a.credential)
elif a.provider in {'minimax', 'kimi', 'qwen'}:
status, payload = _generic_usage(a.provider, a.credential)
else:
ok, msg = test_provider_connection(a.provider, a.credential)
status = 'ok' if ok else 'error'
@@ -148,7 +207,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 if snap else None,
'reset_at': snap.reset_at.isoformat() if snap and snap.reset_at 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,