feat: add kimi/minimax usage adapters and update provider docs
This commit is contained in:
@@ -25,6 +25,51 @@ def _parse_credential(raw: str) -> Dict[str, Any]:
|
|||||||
return {'api_key': raw}
|
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):
|
def get_issue_stats_cached(db: Session, ttl_seconds: int = 1800):
|
||||||
key = 'issue_stats_24h'
|
key = 'issue_stats_24h'
|
||||||
now = _now()
|
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)
|
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}'
|
return r.status_code == 200, f'status={r.status_code}'
|
||||||
usage_url = info.get('usage_url') or info.get('test_url')
|
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:
|
if usage_url:
|
||||||
r = requests.get(usage_url, headers=_provider_headers(provider, key, info), timeout=12)
|
r = requests.get(usage_url, headers=_provider_headers(provider, key, info), timeout=12)
|
||||||
return r.status_code < 500, f'status={r.status_code}'
|
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:
|
if r.status_code != 200:
|
||||||
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
|
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
|
||||||
payload = r.json()
|
payload = r.json()
|
||||||
# Try to normalize
|
if isinstance(payload, dict) and 'data' in payload and isinstance(payload['data'], dict):
|
||||||
return 'ok', {
|
payload = payload['data']
|
||||||
'window_label': payload.get('window') or payload.get('window_label'),
|
return 'ok', _normalize_usage_payload(payload)
|
||||||
'used': payload.get('used'),
|
|
||||||
'limit': payload.get('limit'),
|
|
||||||
'usage_pct': payload.get('usage_pct'),
|
def _kimi_usage(credential: str) -> Tuple[str, Dict[str, Any]]:
|
||||||
'reset_at': None,
|
info = _parse_credential(credential)
|
||||||
'raw': payload,
|
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]]:
|
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:
|
if r.status_code != 200:
|
||||||
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
|
return 'error', {'error': f'usage:{r.status_code}', 'raw': r.text}
|
||||||
payload = r.json()
|
payload = r.json()
|
||||||
return 'ok', {
|
return 'ok', _normalize_usage_payload(payload)
|
||||||
'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):
|
def refresh_provider_usage_once(db: Session):
|
||||||
@@ -171,7 +233,11 @@ def refresh_provider_usage_once(db: Session):
|
|||||||
status, payload = _openai_usage(a.credential)
|
status, payload = _openai_usage(a.credential)
|
||||||
elif a.provider == 'anthropic':
|
elif a.provider == 'anthropic':
|
||||||
status, payload = _anthropic_usage(a.credential)
|
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)
|
status, payload = _generic_usage(a.provider, a.credential)
|
||||||
else:
|
else:
|
||||||
ok, msg = test_provider_connection(a.provider, a.credential)
|
ok, msg = test_provider_connection(a.provider, a.credential)
|
||||||
|
|||||||
@@ -53,3 +53,11 @@
|
|||||||
"auth_scheme": "Bearer"
|
"auth_scheme": "Bearer"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Kimi
|
||||||
|
推荐 usage_url: https://www.kimi.com/api/user/usage
|
||||||
|
Authorization: Bearer <API_KEY>
|
||||||
|
|
||||||
|
## Minimax
|
||||||
|
推荐 usage_url: https://platform.minimax.io/v1/api/openplatform/coding_plan/remains?GroupId=YOUR_GROUP_ID
|
||||||
|
Authorization: Bearer <API_KEY>
|
||||||
|
|||||||
Reference in New Issue
Block a user