Coverage for src/kimi_code_usage/providers/openrouter.py: 95%

172 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-14 14:57 +0800

1import aiohttp 

2import asyncio 

3import os 

4from typing import List, Optional 

5from . import ( 

6 ProviderUsage, 

7 ActivityTotals, 

8 DailyUsage, 

9 ModelUsage, 

10) 

11 

12 

13def _base_url_v1(base_url: str) -> str: 

14 base = base_url.rstrip('/') 

15 if base.endswith('/v1'): 

16 return base 

17 return base + '/v1' 

18 

19 

20def _get_lang() -> bool: 

21 return "zh" in os.getenv("LANG", "en").lower() 

22 

23 

24def _build_activity(items: List[dict]) -> tuple[ActivityTotals, List[ModelUsage]]: 

25 totals = ActivityTotals() 

26 by_model: dict[str, ModelUsage] = {} 

27 

28 for item in items: 

29 totals.spend += float(item.get("usage") or 0) 

30 totals.requests += float(item.get("requests") or 0) 

31 totals.prompt_tokens += float(item.get("prompt_tokens") or 0) 

32 totals.completion_tokens += float(item.get("completion_tokens") or 0) 

33 totals.reasoning_tokens += float(item.get("reasoning_tokens") or 0) 

34 

35 model_name = item.get("model", "unknown") 

36 if model_name not in by_model: 

37 by_model[model_name] = ModelUsage(model=model_name) 

38 m = by_model[model_name] 

39 m.spend += float(item.get("usage") or 0) 

40 m.requests += float(item.get("requests") or 0) 

41 m.prompt_tokens += float(item.get("prompt_tokens") or 0) 

42 m.completion_tokens += float(item.get("completion_tokens") or 0) 

43 m.reasoning_tokens += float(item.get("reasoning_tokens") or 0) 

44 

45 models = sorted( 

46 by_model.values(), 

47 key=lambda m: (m.spend, m.requests), 

48 reverse=True, 

49 ) 

50 return totals, models 

51 

52 

53def _build_daily_activity(items: List[dict]) -> List[DailyUsage]: 

54 by_date: dict[str, dict[str, ModelUsage]] = {} 

55 

56 for item in items: 

57 date = item.get("date", "") 

58 if not date: 

59 continue 

60 models = by_date.setdefault(date, {}) 

61 model_name = item.get("model", "unknown") 

62 if model_name not in models: 

63 models[model_name] = ModelUsage(model=model_name) 

64 m = models[model_name] 

65 m.spend += float(item.get("usage") or 0) 

66 m.requests += float(item.get("requests") or 0) 

67 m.prompt_tokens += float(item.get("prompt_tokens") or 0) 

68 m.completion_tokens += float(item.get("completion_tokens") or 0) 

69 m.reasoning_tokens += float(item.get("reasoning_tokens") or 0) 

70 

71 daily = [] 

72 for date in sorted(by_date.keys()): 

73 models = sorted( 

74 by_date[date].values(), 

75 key=lambda m: m.spend, 

76 reverse=True, 

77 ) 

78 total = sum(m.spend for m in models) 

79 daily.append(DailyUsage(date=date, models=models, total=total)) 

80 return daily 

81 

82 

83def _model_short_name(model: str) -> str: 

84 if "/" in model: 

85 return model.rsplit("/", 1)[-1] 

86 return model 

87 

88 

89async def fetch_openrouter_usage(api_key: str, base_url: str, management_key: Optional[str] = None) -> List[ProviderUsage]: 

90 base = _base_url_v1(base_url) 

91 key_url = f"{base}/auth/key" 

92 credits_url = f"{base}/credits" 

93 activity_url = f"{base}/activity" 

94 

95 key_headers = {"Authorization": f"Bearer {api_key}"} 

96 activity_headers = {"Authorization": f"Bearer {management_key or api_key}"} 

97 

98 async with aiohttp.ClientSession() as session: 

99 async def fetch_key(): 

100 async with session.get(key_url, headers=key_headers) as resp: 

101 if resp.status != 200: 

102 text = await resp.text() 

103 raise Exception(f"OpenRouter API Error {resp.status}: {text}") 

104 return await resp.json() 

105 

106 async def fetch_credits(): 

107 async with session.get(credits_url, headers=key_headers) as resp: 

108 if resp.status == 200: 

109 return await resp.json() 

110 return None 

111 

112 async def fetch_activity(): 

113 try: 

114 async with session.get(activity_url, headers=activity_headers) as resp: 

115 if resp.status == 200: 

116 return await resp.json() 

117 except Exception: 

118 pass 

119 return None 

120 

121 key_payload = await fetch_key() 

122 if isinstance(key_payload, Exception): 

123 raise key_payload 

124 

125 # Fetch activity for all keys; only management keys typically have access, 

126 # so failures are silently ignored. 

127 results = await asyncio.gather(fetch_credits(), fetch_activity(), return_exceptions=True) 

128 

129 credits_payload = results[0] 

130 activity_payload = results[1] 

131 

132 if isinstance(credits_payload, Exception): 

133 credits_payload = None 

134 if isinstance(activity_payload, Exception): 

135 activity_payload = None 

136 

137 key_data = key_payload.get("data", {}) 

138 is_management = bool(key_data.get("is_management_key")) 

139 

140 res: List[ProviderUsage] = [] 

141 is_zh = _get_lang() 

142 

143 key_usage = key_data.get("usage") 

144 key_limit = key_data.get("limit") 

145 key_used_val = float(key_usage) if key_usage is not None else 0.0 

146 key_limit_val = float(key_limit) if key_limit is not None else None 

147 

148 credits_data = credits_payload.get("data", {}) if credits_payload else None 

149 

150 if credits_data: 

151 total_credits = credits_data.get("total_credits") 

152 total_usage = credits_data.get("total_usage") 

153 if total_credits is not None and total_usage is not None: 

154 t_credits = float(total_credits) 

155 t_usage = float(total_usage) 

156 remaining_val = t_credits - t_usage 

157 percent_val = (t_usage / t_credits * 100) if t_credits > 0 else None 

158 

159 res.append(ProviderUsage( 

160 provider="openrouter", 

161 label="Credits", 

162 used=t_usage, 

163 limit=t_credits, 

164 remaining=remaining_val, 

165 percent=percent_val, 

166 reset_at=None, 

167 unit="$" 

168 )) 

169 

170 k_remaining = (key_limit_val - key_used_val) if key_limit_val is not None else None 

171 k_percent = (key_used_val / key_limit_val * 100) if (key_limit_val is not None and key_limit_val > 0) else None 

172 

173 res.append(ProviderUsage( 

174 provider="openrouter", 

175 label="Key Usage", 

176 used=key_used_val, 

177 limit=key_limit_val, 

178 remaining=k_remaining, 

179 percent=k_percent, 

180 reset_at=None, 

181 unit="$" 

182 )) 

183 

184 if not res: 

185 remaining_val = (key_limit_val - key_used_val) if key_limit_val is not None else None 

186 percent_val = (key_used_val / key_limit_val * 100) if (key_limit_val is not None and key_limit_val > 0) else None 

187 

188 res.append(ProviderUsage( 

189 provider="openrouter", 

190 label="Credits", 

191 used=key_used_val, 

192 limit=key_limit_val, 

193 remaining=remaining_val, 

194 percent=percent_val, 

195 reset_at=None, 

196 unit="$" 

197 )) 

198 

199 key_label = key_data.get("label") 

200 if key_label: 

201 if is_management: 

202 display_label = "Management Key" 

203 else: 

204 display_label = "Key Name" 

205 display_value = key_label.replace("...", "*********") if "..." in key_label else key_label 

206 

207 res.append(ProviderUsage( 

208 provider="openrouter", 

209 label=display_label, 

210 used=0.0, 

211 limit=None, 

212 remaining=None, 

213 percent=None, 

214 reset_at=None, 

215 unit="text", 

216 text_value=display_value 

217 )) 

218 

219 is_free_tier = key_data.get("is_free_tier") 

220 if is_free_tier is not None: 

221 res.append(ProviderUsage( 

222 provider="openrouter", 

223 label="Free Tier", 

224 used=0.0, 

225 limit=None, 

226 remaining=None, 

227 percent=None, 

228 reset_at=None, 

229 unit="text", 

230 text_value="Yes" if is_free_tier else "No" 

231 )) 

232 

233 limit_reset = key_data.get("limit_reset") 

234 if limit_reset: 

235 res.append(ProviderUsage( 

236 provider="openrouter", 

237 label="Limit Reset", 

238 used=0.0, 

239 limit=None, 

240 remaining=None, 

241 percent=None, 

242 reset_at=None, 

243 unit="text", 

244 text_value=str(limit_reset) 

245 )) 

246 

247 expires_at = key_data.get("expires_at") 

248 if expires_at: 

249 res.append(ProviderUsage( 

250 provider="openrouter", 

251 label="Expires At", 

252 used=0.0, 

253 limit=None, 

254 remaining=None, 

255 percent=None, 

256 reset_at=None, 

257 unit="text", 

258 text_value=str(expires_at) 

259 )) 

260 

261 is_provisioning = key_data.get("is_provisioning_key") 

262 # NOTE: Is Provisioning is intentionally not displayed by default. 

263 # It remains available in key_data for future configuration if needed. 

264 _ = is_provisioning 

265 

266 rate_limit = key_data.get("rate_limit", {}) 

267 if isinstance(rate_limit, dict): 

268 reqs = rate_limit.get("requests") 

269 interval = rate_limit.get("interval") 

270 if reqs is not None and interval is not None: 

271 if reqs == -1: 

272 if is_zh: 

273 interval_zh = str(interval).replace("s", "秒").replace("m", "分钟").replace("h", "小时") 

274 val_str = f"无限制/{interval_zh}" 

275 else: 

276 val_str = f"Unlimited/{interval}" 

277 else: 

278 if is_zh: 

279 interval_zh = str(interval).replace("s", "秒").replace("m", "分钟").replace("h", "小时") 

280 val_str = f"{reqs}次/{interval_zh}" 

281 else: 

282 val_str = f"{reqs} req/{interval}" 

283 

284 res.append(ProviderUsage( 

285 provider="openrouter", 

286 label="Rate Limit", 

287 used=0.0, 

288 limit=None, 

289 remaining=None, 

290 percent=None, 

291 reset_at=None, 

292 unit="text", 

293 text_value=val_str 

294 )) 

295 

296 u_daily = float(key_data.get("usage_daily") if key_data.get("usage_daily") is not None else 0.0) 

297 u_weekly = float(key_data.get("usage_weekly") if key_data.get("usage_weekly") is not None else 0.0) 

298 u_monthly = float(key_data.get("usage_monthly") if key_data.get("usage_monthly") is not None else 0.0) 

299 

300 label_str = "周期已用" if is_zh else "Usage" 

301 if is_zh: 

302 text_val = f"今日: ${u_daily:.4f} | 本周: ${u_weekly:.4f} | 本月: ${u_monthly:.4f}" 

303 else: 

304 text_val = f"Daily: ${u_daily:.4f} | Weekly: ${u_weekly:.4f} | Monthly: ${u_monthly:.4f}" 

305 

306 res.append(ProviderUsage( 

307 provider="openrouter", 

308 label=label_str, 

309 used=0.0, 

310 limit=None, 

311 remaining=None, 

312 percent=None, 

313 reset_at=None, 

314 unit="text", 

315 text_value=text_val 

316 )) 

317 

318 if activity_payload and isinstance(activity_payload, dict): 

319 items = activity_payload.get("data", []) 

320 if isinstance(items, list) and items: 

321 totals, top_models = _build_activity(items) 

322 daily_activity = _build_daily_activity(items) 

323 

324 res.append(ProviderUsage( 

325 provider="openrouter", 

326 label="Activity" if not is_zh else "活动", 

327 used=0.0, 

328 limit=None, 

329 remaining=None, 

330 percent=None, 

331 reset_at=None, 

332 unit="text", 

333 text_value=None, 

334 activity_totals=totals, 

335 )) 

336 

337 if daily_activity: 

338 res.append(ProviderUsage( 

339 provider="openrouter", 

340 label="Daily Spend" if not is_zh else "每日支出", 

341 used=0.0, 

342 limit=None, 

343 remaining=None, 

344 percent=None, 

345 reset_at=None, 

346 unit="text", 

347 text_value=None, 

348 daily_activity=daily_activity, 

349 )) 

350 

351 if top_models: 

352 res.append(ProviderUsage( 

353 provider="openrouter", 

354 label="Top Models" if not is_zh else "Top 模型", 

355 used=0.0, 

356 limit=None, 

357 remaining=None, 

358 percent=None, 

359 reset_at=None, 

360 unit="text", 

361 text_value=None, 

362 top_models=top_models, 

363 )) 

364 

365 return res