usage_snapshots.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from typing import Optional
  2. from datetime import date
  3. from gpustack.schemas.api_keys import ApiKey
  4. from gpustack.schemas.model_provider import ModelProvider
  5. from gpustack.schemas.models import Model
  6. from gpustack.schemas.usage import USAGE_GRANULARITY_MONTH
  7. from gpustack.schemas.users import User
  8. def format_model_snapshot_label(
  9. model_name: str,
  10. cluster_name: Optional[str] = None,
  11. ) -> str:
  12. """Return a human-readable display label for a model usage snapshot.
  13. Format: ``"<cluster_name> / <model_name>"`` when a cluster is given,
  14. otherwise just ``"<model_name>"``.
  15. """
  16. if cluster_name:
  17. return f"{cluster_name} / {model_name}"
  18. return model_name
  19. def format_usage_model_label(
  20. model_name: Optional[str],
  21. cluster_name: Optional[str] = None,
  22. provider_name: Optional[str] = None,
  23. ) -> str:
  24. """Return the display label for usage records grouped by model.
  25. Provider-routed usage is identified by provider + model name. Directly
  26. deployed usage is identified by cluster + model name.
  27. """
  28. if provider_name and model_name:
  29. parts = [provider_name, model_name]
  30. if cluster_name:
  31. parts.insert(0, cluster_name)
  32. return " / ".join(parts)
  33. if model_name:
  34. return format_model_snapshot_label(model_name, cluster_name)
  35. return "Unknown Model"
  36. def format_usage_user_label(user_name: Optional[str]) -> str:
  37. return user_name or "Unknown User"
  38. def format_usage_api_key_label(
  39. user_name: Optional[str] = None,
  40. api_key_name: Optional[str] = None,
  41. ) -> str:
  42. parts = [p for p in [user_name, api_key_name] if p]
  43. return " / ".join(parts) or "-"
  44. def format_usage_date_label(value: date, granularity: str) -> str:
  45. if granularity == USAGE_GRANULARITY_MONTH:
  46. return value.strftime("%Y-%m")
  47. return value.isoformat()
  48. def build_model_usage_snapshot(
  49. model: Model,
  50. cluster_name: Optional[str] = None,
  51. user: Optional[User] = None,
  52. api_key: Optional[ApiKey] = None,
  53. provider: Optional[ModelProvider] = None,
  54. ) -> dict:
  55. """Build a usage snapshot dict capturing the model identity at request time.
  56. Records model ID, name, and cluster; optionally includes user and API key
  57. fields when the caller supplies them. The snapshot is stored alongside usage
  58. records so usage can be attributed even after models or keys are deleted.
  59. ``cluster_name`` is resolved from ``model.cluster`` when not passed
  60. explicitly.
  61. Shared-snapshot contract: the returned dict is splatted (``**snapshot``)
  62. into BOTH ``ModelUsage`` and ``ModelUsageDetails`` constructors. Every
  63. key emitted here MUST therefore be a valid column on both tables,
  64. otherwise the rollup write or the details write will fail at runtime.
  65. Fields specific to one table only (e.g. ``cluster_id`` /
  66. ``model_route_id`` / ``started_at`` / ``completed_at`` on details)
  67. must be passed via dedicated kwargs at the call site, NOT added here.
  68. """
  69. if cluster_name is None:
  70. cluster = getattr(model, "cluster", None)
  71. cluster_name = None if cluster is None else cluster.name
  72. snapshot = {
  73. "model_id": model.id,
  74. "model_name": model.name,
  75. "cluster_name": cluster_name,
  76. # Usage rows inherit the model's tenant scope so dashboard/filtering
  77. # by Org doesn't need to re-join models.
  78. "owner_principal_id": getattr(model, "owner_principal_id", None),
  79. }
  80. if user is not None:
  81. snapshot.update(
  82. {
  83. "user_id": user.id,
  84. "user_name": user.username,
  85. }
  86. )
  87. if provider is not None:
  88. provider_type = getattr(getattr(provider, "config", None), "type", None)
  89. if provider_type is not None and hasattr(provider_type, "value"):
  90. provider_type = provider_type.value
  91. snapshot.update(
  92. {
  93. "provider_id": provider.id,
  94. "provider_name": provider.name,
  95. "provider_type": provider_type,
  96. }
  97. )
  98. if api_key is not None:
  99. snapshot.update(
  100. {
  101. "api_key_id": api_key.id,
  102. "api_key_name": api_key.name,
  103. "access_key": api_key.access_key,
  104. "api_key_is_custom": api_key.is_custom,
  105. }
  106. )
  107. return snapshot