usage.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. from datetime import date as Date
  2. from typing import List, Optional
  3. from pydantic import BaseModel, Field, computed_field, field_validator
  4. from gpustack.api.exceptions import InvalidException
  5. from gpustack.schemas.common import Pagination
  6. USAGE_METRIC_INPUT_TOKENS = "input_tokens"
  7. USAGE_METRIC_OUTPUT_TOKENS = "output_tokens"
  8. USAGE_METRIC_INPUT_CACHED_TOKENS = "input_cached_tokens"
  9. USAGE_METRIC_TOTAL_TOKENS = "total_tokens"
  10. USAGE_METRIC_API_REQUESTS = "api_requests"
  11. USAGE_METRIC_MODELS_CALLED = "models_called"
  12. USAGE_METRIC_API_KEYS_USED = "api_keys_used"
  13. USAGE_METRIC_AVG_TOKENS_PER_REQUEST = "avg_tokens_per_request"
  14. USAGE_METRIC_LAST_ACTIVE = "last_active"
  15. USAGE_METRIC_DATE = "date"
  16. USAGE_GROUP_BY_DATE = "date"
  17. USAGE_GROUP_BY_MODEL = "model"
  18. USAGE_GROUP_BY_USER = "user"
  19. USAGE_GROUP_BY_API_KEY = "api_key"
  20. USAGE_GRANULARITY_DAY = "day"
  21. USAGE_GRANULARITY_WEEK = "week"
  22. USAGE_GRANULARITY_MONTH = "month"
  23. USAGE_SORT_ASC = "asc"
  24. USAGE_SORT_DESC = "desc"
  25. # Usage view scope. ``self`` filters to the caller's own rows
  26. # (``user_id = self``); ``all`` filters to the current Org's rows
  27. # (``owner_principal_id = current_principal_id``), or — for platform admin in
  28. # cross-org context — to every Org. ``all`` is reserved for admin /
  29. # Org owner / manager; others are forced to ``self``.
  30. USAGE_SCOPE_SELF = "self"
  31. USAGE_SCOPE_ALL = "all"
  32. USAGE_SCOPES = {USAGE_SCOPE_SELF, USAGE_SCOPE_ALL}
  33. USAGE_GROUP_BYS = {
  34. USAGE_GROUP_BY_DATE,
  35. USAGE_GROUP_BY_MODEL,
  36. USAGE_GROUP_BY_USER,
  37. USAGE_GROUP_BY_API_KEY,
  38. }
  39. USAGE_GRANULARITIES = {
  40. USAGE_GRANULARITY_DAY,
  41. USAGE_GRANULARITY_WEEK,
  42. USAGE_GRANULARITY_MONTH,
  43. }
  44. USAGE_SORTABLE_FIELDS = {
  45. USAGE_METRIC_INPUT_TOKENS,
  46. USAGE_METRIC_OUTPUT_TOKENS,
  47. USAGE_METRIC_INPUT_CACHED_TOKENS,
  48. USAGE_METRIC_TOTAL_TOKENS,
  49. USAGE_METRIC_API_REQUESTS,
  50. USAGE_METRIC_AVG_TOKENS_PER_REQUEST,
  51. USAGE_METRIC_MODELS_CALLED,
  52. USAGE_METRIC_API_KEYS_USED,
  53. USAGE_METRIC_LAST_ACTIVE,
  54. USAGE_METRIC_DATE,
  55. }
  56. class UsageOption(BaseModel):
  57. key: str
  58. label: str
  59. class UsageIdentityValue(BaseModel):
  60. cluster_name: Optional[str] = None
  61. model_name: Optional[str] = None
  62. provider_name: Optional[str] = None
  63. provider_type: Optional[str] = None
  64. user_name: Optional[str] = None
  65. api_key_name: Optional[str] = None
  66. access_key: Optional[str] = None
  67. api_key_is_custom: Optional[bool] = None
  68. class UsageIdentityCurrent(BaseModel):
  69. model_id: Optional[int] = None
  70. provider_id: Optional[int] = None
  71. user_id: Optional[int] = None
  72. api_key_id: Optional[int] = None
  73. class UsageIdentity(BaseModel):
  74. value: UsageIdentityValue
  75. current: Optional[UsageIdentityCurrent] = None
  76. class UsageFilterItem(BaseModel):
  77. identity: UsageIdentity
  78. class UsageFilterOption(UsageFilterItem):
  79. label: str
  80. deleted: bool
  81. class UsageFilters(BaseModel):
  82. models: List[UsageFilterOption] = Field(default_factory=list)
  83. users: List[UsageFilterOption] = Field(default_factory=list)
  84. api_keys: List[UsageFilterOption] = Field(default_factory=list)
  85. class UsageMetaResponse(BaseModel):
  86. metrics: List[UsageOption]
  87. granularities: List[UsageOption]
  88. group_bys: List[UsageOption]
  89. filters: UsageFilters
  90. class UsageFilterRequest(BaseModel):
  91. models: List[UsageFilterItem] = Field(default_factory=list)
  92. users: List[UsageFilterItem] = Field(default_factory=list)
  93. api_keys: List[UsageFilterItem] = Field(default_factory=list)
  94. class UsageBaseRequest(BaseModel):
  95. start_date: Date
  96. end_date: Date
  97. filters: UsageFilterRequest = Field(default_factory=UsageFilterRequest)
  98. # See USAGE_SCOPE_* constants. Defaults to "all" so that managers /
  99. # admins who omit the parameter get the org-wide provider view; the
  100. # endpoint downgrades to "self" automatically when the caller has
  101. # no managerial role (and rejects the request if they explicitly
  102. # asked for "all").
  103. scope: str = USAGE_SCOPE_ALL
  104. @field_validator("end_date")
  105. @classmethod
  106. def validate_date_range(cls, value: Date, info) -> Date:
  107. start_date = info.data.get("start_date")
  108. if start_date and value < start_date:
  109. raise ValueError("end_date must be on or after start_date")
  110. return value
  111. @field_validator("scope")
  112. @classmethod
  113. def validate_scope(cls, value: str) -> str:
  114. if value not in USAGE_SCOPES:
  115. raise ValueError(f"Unsupported scope: {value}")
  116. return value
  117. class UsageBreakdownRequest(UsageBaseRequest):
  118. group_by: List[str]
  119. granularity: Optional[str] = None
  120. sort_by: Optional[str] = f"-{USAGE_METRIC_TOTAL_TOKENS}"
  121. page: int = 1
  122. perPage: int = 20
  123. @field_validator("group_by")
  124. @classmethod
  125. def validate_group_by(cls, value: List[str]) -> List[str]:
  126. if not value:
  127. raise ValueError("group_by must not be empty")
  128. unsupported = [item for item in value if item not in USAGE_GROUP_BYS]
  129. if unsupported:
  130. raise ValueError(f"Unsupported group_by: {', '.join(unsupported)}")
  131. if len(value) != len(set(value)):
  132. raise ValueError("group_by must not contain duplicate values")
  133. return value
  134. @field_validator("granularity")
  135. @classmethod
  136. def validate_granularity(cls, value: Optional[str]) -> Optional[str]:
  137. if value is not None and value not in USAGE_GRANULARITIES:
  138. raise ValueError(f"Unsupported granularity: {value}")
  139. return value
  140. @field_validator("sort_by")
  141. @classmethod
  142. def validate_sort_by(cls, value: Optional[str]) -> Optional[str]:
  143. if not value:
  144. return value
  145. for field in value.split(","):
  146. field = field.strip()
  147. if not field:
  148. continue
  149. field_name = field[1:] if field.startswith("-") else field
  150. if field_name not in USAGE_SORTABLE_FIELDS:
  151. raise InvalidException(
  152. f"Field '{field_name}' is not sortable. "
  153. f"Allowed fields: {', '.join(sorted(USAGE_SORTABLE_FIELDS))}"
  154. )
  155. return value
  156. @field_validator("page", "perPage")
  157. @classmethod
  158. def validate_positive_int(cls, value: int) -> int:
  159. if value < 1:
  160. raise ValueError("page and perPage must be positive")
  161. return value
  162. @computed_field
  163. @property
  164. def order_by(self) -> List[tuple[str, str]]:
  165. if not self.sort_by:
  166. return []
  167. order_by = []
  168. for field in self.sort_by.split(","):
  169. field = field.strip()
  170. if not field:
  171. continue
  172. if field.startswith("-"):
  173. order_by.append((field[1:], USAGE_SORT_DESC))
  174. else:
  175. order_by.append((field, USAGE_SORT_ASC))
  176. return order_by
  177. class UsageSummary(BaseModel):
  178. input_tokens: int = 0
  179. output_tokens: int = 0
  180. input_cached_tokens: int = 0
  181. total_tokens: int = 0
  182. api_requests: int = 0
  183. models_called: int = 0
  184. class UsageBreakdownDimension(BaseModel):
  185. identity: Optional[UsageIdentity] = None
  186. label: str
  187. deleted: bool
  188. class UsageBreakdownDateDimension(BaseModel):
  189. value: Date
  190. label: str
  191. deleted: bool = False
  192. class UsageBreakdownItem(BaseModel):
  193. date: Optional[UsageBreakdownDateDimension] = None
  194. model: Optional[UsageBreakdownDimension] = None
  195. user: Optional[UsageBreakdownDimension] = None
  196. api_key: Optional[UsageBreakdownDimension] = None
  197. input_tokens: int = 0
  198. output_tokens: int = 0
  199. input_cached_tokens: int = 0
  200. total_tokens: int = 0
  201. api_requests: int = 0
  202. avg_tokens_per_request: float = 0
  203. models_called: Optional[int] = None
  204. api_keys_used: Optional[int] = None
  205. last_active: Optional[Date] = None
  206. class UsageBreakdownResponse(BaseModel):
  207. summary: UsageSummary
  208. group_by: List[str]
  209. granularity: Optional[str] = None
  210. pagination: Pagination
  211. items: List[UsageBreakdownItem]