usage_routes.py 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. """用量与计费路由。"""
  2. from __future__ import annotations
  3. import asyncio
  4. from fastapi import APIRouter
  5. from api.models import QuotaUpdateRequest
  6. from billing import guard, pricing
  7. from db import billing_store, db_conn
  8. from obs import get_logger
  9. logger = get_logger(__name__)
  10. router = APIRouter(tags=["用量"])
  11. @router.get("/usage/today")
  12. async def usage_today(user_name: str | None = None):
  13. """当日用量与额度。
  14. 计费库不可用时不报 5xx——这个接口是给用户看「我还剩多少额度」的,
  15. 因为一个次要故障就返回错误会让用户以为整个服务挂了。所以返回
  16. ``available=False`` 并附原因。
  17. """
  18. user = (user_name or "default").strip() or "default"
  19. snapshot = await asyncio.to_thread(guard.snapshot, user, None)
  20. return snapshot
  21. @router.get("/usage/recent")
  22. async def usage_recent(user_name: str | None = None, limit: int = 20):
  23. """最近若干条用量流水。"""
  24. user = (user_name or "default").strip() or "default"
  25. items = await asyncio.to_thread(billing_store.recent_usage, user, min(limit, 200))
  26. return {"user_name": user, "items": items, "count": len(items)}
  27. @router.put("/usage/quota")
  28. async def update_quota(payload: QuotaUpdateRequest):
  29. """调整某用户当日额度(0 = 不限)。"""
  30. user = (payload.user_name or "default").strip() or "default"
  31. day = db_conn.day_key()
  32. quota = await asyncio.to_thread(
  33. billing_store.set_quota_limit, user, day, payload.limit_tokens
  34. )
  35. logger.info("额度调整 user=%s day=%s limit=%d", user, day, payload.limit_tokens)
  36. return quota
  37. @router.get("/usage/pricing")
  38. async def get_pricing():
  39. """模型单价表。"""
  40. items = await asyncio.to_thread(pricing.pricing_table)
  41. return {"items": items, "count": len(items)}
  42. @router.get("/health/billing")
  43. async def billing_health():
  44. """计费子系统自检:单价表是否覆盖当前配置的模型。"""
  45. import config
  46. models = {config.MODEL, config.SUMMARY_MODEL}
  47. table = await asyncio.to_thread(pricing.pricing_table)
  48. priced = {row["model"] for row in table}
  49. missing = sorted(m for m in models if m and m not in priced)
  50. return {
  51. "ok": not missing,
  52. "configured_models": sorted(m for m in models if m),
  53. "priced_models": sorted(priced),
  54. "missing": missing,
  55. "hint": "缺单价的模型会在请求开始时 fail-closed;补 billing_pricing 或设 BILLING_ALLOW_UNPRICED=1",
  56. }