| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- from datetime import date as Date
- from typing import List, Optional
- from pydantic import BaseModel, Field, computed_field, field_validator
- from gpustack.api.exceptions import InvalidException
- from gpustack.schemas.common import Pagination
- USAGE_METRIC_INPUT_TOKENS = "input_tokens"
- USAGE_METRIC_OUTPUT_TOKENS = "output_tokens"
- USAGE_METRIC_INPUT_CACHED_TOKENS = "input_cached_tokens"
- USAGE_METRIC_TOTAL_TOKENS = "total_tokens"
- USAGE_METRIC_API_REQUESTS = "api_requests"
- USAGE_METRIC_MODELS_CALLED = "models_called"
- USAGE_METRIC_API_KEYS_USED = "api_keys_used"
- USAGE_METRIC_AVG_TOKENS_PER_REQUEST = "avg_tokens_per_request"
- USAGE_METRIC_LAST_ACTIVE = "last_active"
- USAGE_METRIC_DATE = "date"
- USAGE_GROUP_BY_DATE = "date"
- USAGE_GROUP_BY_MODEL = "model"
- USAGE_GROUP_BY_USER = "user"
- USAGE_GROUP_BY_API_KEY = "api_key"
- USAGE_GRANULARITY_DAY = "day"
- USAGE_GRANULARITY_WEEK = "week"
- USAGE_GRANULARITY_MONTH = "month"
- USAGE_SORT_ASC = "asc"
- USAGE_SORT_DESC = "desc"
- # Usage view scope. ``self`` filters to the caller's own rows
- # (``user_id = self``); ``all`` filters to the current Org's rows
- # (``owner_principal_id = current_principal_id``), or — for platform admin in
- # cross-org context — to every Org. ``all`` is reserved for admin /
- # Org owner / manager; others are forced to ``self``.
- USAGE_SCOPE_SELF = "self"
- USAGE_SCOPE_ALL = "all"
- USAGE_SCOPES = {USAGE_SCOPE_SELF, USAGE_SCOPE_ALL}
- USAGE_GROUP_BYS = {
- USAGE_GROUP_BY_DATE,
- USAGE_GROUP_BY_MODEL,
- USAGE_GROUP_BY_USER,
- USAGE_GROUP_BY_API_KEY,
- }
- USAGE_GRANULARITIES = {
- USAGE_GRANULARITY_DAY,
- USAGE_GRANULARITY_WEEK,
- USAGE_GRANULARITY_MONTH,
- }
- USAGE_SORTABLE_FIELDS = {
- USAGE_METRIC_INPUT_TOKENS,
- USAGE_METRIC_OUTPUT_TOKENS,
- USAGE_METRIC_INPUT_CACHED_TOKENS,
- USAGE_METRIC_TOTAL_TOKENS,
- USAGE_METRIC_API_REQUESTS,
- USAGE_METRIC_AVG_TOKENS_PER_REQUEST,
- USAGE_METRIC_MODELS_CALLED,
- USAGE_METRIC_API_KEYS_USED,
- USAGE_METRIC_LAST_ACTIVE,
- USAGE_METRIC_DATE,
- }
- class UsageOption(BaseModel):
- key: str
- label: str
- class UsageIdentityValue(BaseModel):
- cluster_name: Optional[str] = None
- model_name: Optional[str] = None
- provider_name: Optional[str] = None
- provider_type: Optional[str] = None
- user_name: Optional[str] = None
- api_key_name: Optional[str] = None
- access_key: Optional[str] = None
- api_key_is_custom: Optional[bool] = None
- class UsageIdentityCurrent(BaseModel):
- model_id: Optional[int] = None
- provider_id: Optional[int] = None
- user_id: Optional[int] = None
- api_key_id: Optional[int] = None
- class UsageIdentity(BaseModel):
- value: UsageIdentityValue
- current: Optional[UsageIdentityCurrent] = None
- class UsageFilterItem(BaseModel):
- identity: UsageIdentity
- class UsageFilterOption(UsageFilterItem):
- label: str
- deleted: bool
- class UsageFilters(BaseModel):
- models: List[UsageFilterOption] = Field(default_factory=list)
- users: List[UsageFilterOption] = Field(default_factory=list)
- api_keys: List[UsageFilterOption] = Field(default_factory=list)
- class UsageMetaResponse(BaseModel):
- metrics: List[UsageOption]
- granularities: List[UsageOption]
- group_bys: List[UsageOption]
- filters: UsageFilters
- class UsageFilterRequest(BaseModel):
- models: List[UsageFilterItem] = Field(default_factory=list)
- users: List[UsageFilterItem] = Field(default_factory=list)
- api_keys: List[UsageFilterItem] = Field(default_factory=list)
- class UsageBaseRequest(BaseModel):
- start_date: Date
- end_date: Date
- filters: UsageFilterRequest = Field(default_factory=UsageFilterRequest)
- # See USAGE_SCOPE_* constants. Defaults to "all" so that managers /
- # admins who omit the parameter get the org-wide provider view; the
- # endpoint downgrades to "self" automatically when the caller has
- # no managerial role (and rejects the request if they explicitly
- # asked for "all").
- scope: str = USAGE_SCOPE_ALL
- @field_validator("end_date")
- @classmethod
- def validate_date_range(cls, value: Date, info) -> Date:
- start_date = info.data.get("start_date")
- if start_date and value < start_date:
- raise ValueError("end_date must be on or after start_date")
- return value
- @field_validator("scope")
- @classmethod
- def validate_scope(cls, value: str) -> str:
- if value not in USAGE_SCOPES:
- raise ValueError(f"Unsupported scope: {value}")
- return value
- class UsageBreakdownRequest(UsageBaseRequest):
- group_by: List[str]
- granularity: Optional[str] = None
- sort_by: Optional[str] = f"-{USAGE_METRIC_TOTAL_TOKENS}"
- page: int = 1
- perPage: int = 20
- @field_validator("group_by")
- @classmethod
- def validate_group_by(cls, value: List[str]) -> List[str]:
- if not value:
- raise ValueError("group_by must not be empty")
- unsupported = [item for item in value if item not in USAGE_GROUP_BYS]
- if unsupported:
- raise ValueError(f"Unsupported group_by: {', '.join(unsupported)}")
- if len(value) != len(set(value)):
- raise ValueError("group_by must not contain duplicate values")
- return value
- @field_validator("granularity")
- @classmethod
- def validate_granularity(cls, value: Optional[str]) -> Optional[str]:
- if value is not None and value not in USAGE_GRANULARITIES:
- raise ValueError(f"Unsupported granularity: {value}")
- return value
- @field_validator("sort_by")
- @classmethod
- def validate_sort_by(cls, value: Optional[str]) -> Optional[str]:
- if not value:
- return value
- for field in value.split(","):
- field = field.strip()
- if not field:
- continue
- field_name = field[1:] if field.startswith("-") else field
- if field_name not in USAGE_SORTABLE_FIELDS:
- raise InvalidException(
- f"Field '{field_name}' is not sortable. "
- f"Allowed fields: {', '.join(sorted(USAGE_SORTABLE_FIELDS))}"
- )
- return value
- @field_validator("page", "perPage")
- @classmethod
- def validate_positive_int(cls, value: int) -> int:
- if value < 1:
- raise ValueError("page and perPage must be positive")
- return value
- @computed_field
- @property
- def order_by(self) -> List[tuple[str, str]]:
- if not self.sort_by:
- return []
- order_by = []
- for field in self.sort_by.split(","):
- field = field.strip()
- if not field:
- continue
- if field.startswith("-"):
- order_by.append((field[1:], USAGE_SORT_DESC))
- else:
- order_by.append((field, USAGE_SORT_ASC))
- return order_by
- class UsageSummary(BaseModel):
- input_tokens: int = 0
- output_tokens: int = 0
- input_cached_tokens: int = 0
- total_tokens: int = 0
- api_requests: int = 0
- models_called: int = 0
- class UsageBreakdownDimension(BaseModel):
- identity: Optional[UsageIdentity] = None
- label: str
- deleted: bool
- class UsageBreakdownDateDimension(BaseModel):
- value: Date
- label: str
- deleted: bool = False
- class UsageBreakdownItem(BaseModel):
- date: Optional[UsageBreakdownDateDimension] = None
- model: Optional[UsageBreakdownDimension] = None
- user: Optional[UsageBreakdownDimension] = None
- api_key: Optional[UsageBreakdownDimension] = None
- input_tokens: int = 0
- output_tokens: int = 0
- input_cached_tokens: int = 0
- total_tokens: int = 0
- api_requests: int = 0
- avg_tokens_per_request: float = 0
- models_called: Optional[int] = None
- api_keys_used: Optional[int] = None
- last_active: Optional[Date] = None
- class UsageBreakdownResponse(BaseModel):
- summary: UsageSummary
- group_by: List[str]
- granularity: Optional[str] = None
- pagination: Pagination
- items: List[UsageBreakdownItem]
|