feat: add kimi/minimax usage adapters and update provider docs

This commit is contained in:
zhi
2026-03-11 17:05:18 +00:00
parent d5402f3a70
commit c81654739c
2 changed files with 92 additions and 18 deletions

View File

@@ -25,6 +25,51 @@ def _parse_credential(raw: str) -> Dict[str, Any]:
return {'api_key': raw}
def _parse_reset_at(value) -> datetime | None:
if not value:
return None
if isinstance(value, datetime):
return value
if isinstance(value, (int, float)):
return datetime.fromtimestamp(value, tz=timezone.utc)
if isinstance(value, str):
try:
return datetime.fromisoformat(value.replace('Z', '+00:00'))
except Exception:
return None
return None
def _normalize_usage_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
used = payload.get('used') or payload.get('usage') or payload.get('consumed') or payload.get('total_usage')
limit = payload.get('limit') or payload.get('quota') or payload.get('hard_limit') or payload.get('total')
remaining = payload.get('remain') or payload.get('remaining') or payload.get('left')
usage_pct = payload.get('usage_pct') or payload.get('percent') or payload.get('usage_percent')
window_label = payload.get('window') or payload.get('window_label')
reset_at = payload.get('reset_at') or payload.get('reset_time') or payload.get('reset')
if used is None and remaining is not None and limit is not None:
try:
used = float(limit) - float(remaining)
except Exception:
pass
if usage_pct is None and used is not None and limit:
try:
usage_pct = round(float(used) / float(limit) * 100, 2)
except Exception:
pass
return {
'window_label': window_label,
'used': used,
'limit': limit,
'usage_pct': usage_pct,
'reset_at': _parse_reset_at(reset_at),
'raw': payload,
}
def get_issue_stats_cached(db: Session, ttl_seconds: int = 1800):
key = 'issue_stats_24h'
now = _now()
@@ -78,6 +123,8 @@ def test_provider_connection(provider: str, credential: str):
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 provider == 'kimi' and not usage_url:
usage_url = 'https://www.kimi.com/api/user/usage'
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}'
@@ -130,15 +177,37 @@ def _anthropic_usage(credential: str) -> Tuple[str, Dict[str, Any]]:
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,
}
if isinstance(payload, dict) and 'data' in payload and isinstance(payload['data'], dict):
payload = payload['data']
return 'ok', _normalize_usage_payload(payload)
def _kimi_usage(credential: str) -> Tuple[str, Dict[str, Any]]:
info = _parse_credential(credential)
key = info.get('api_key') or credential
usage_url = info.get('usage_url') or 'https://www.kimi.com/api/user/usage'
r = requests.get(usage_url, headers=_provider_headers('kimi', key, info), timeout=12)
if r.status_code != 200:
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
payload = r.json()
if isinstance(payload, dict) and 'data' in payload and isinstance(payload['data'], dict):
payload = payload['data']
return 'ok', _normalize_usage_payload(payload)
def _minimax_usage(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': 'minimax usage API not configured'}
r = requests.get(usage_url, headers=_provider_headers('minimax', key, info), timeout=12)
if r.status_code != 200:
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
payload = r.json()
if isinstance(payload, dict) and 'data' in payload and isinstance(payload['data'], dict):
payload = payload['data']
return 'ok', _normalize_usage_payload(payload)
def _generic_usage(provider: str, credential: str) -> Tuple[str, Dict[str, Any]]:
@@ -151,14 +220,7 @@ def _generic_usage(provider: str, credential: str) -> Tuple[str, Dict[str, Any]]
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,
}
return 'ok', _normalize_usage_payload(payload)
def refresh_provider_usage_once(db: Session):
@@ -171,7 +233,11 @@ 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'}:
elif a.provider == 'kimi':
status, payload = _kimi_usage(a.credential)
elif a.provider == 'minimax':
status, payload = _minimax_usage(a.credential)
elif a.provider == 'qwen':
status, payload = _generic_usage(a.provider, a.credential)
else:
ok, msg = test_provider_connection(a.provider, a.credential)