diff --git a/app/services/monitoring.py b/app/services/monitoring.py index 9aeb4b3..07055a4 100644 --- a/app/services/monitoring.py +++ b/app/services/monitoring.py @@ -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. - return 'unsupported', {'error': 'anthropic usage API not configured'} + 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,