232 lines
8.9 KiB
Python
232 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
API使用統計資料模型
|
|
|
|
Author: PANJIT IT Team
|
|
Created: 2024-01-28
|
|
Modified: 2024-01-28
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from sqlalchemy.sql import func
|
|
from app import db
|
|
|
|
|
|
class APIUsageStats(db.Model):
|
|
"""API使用統計表 (dt_api_usage_stats)"""
|
|
__tablename__ = 'dt_api_usage_stats'
|
|
|
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('dt_users.id'), nullable=False, comment='使用者ID')
|
|
job_id = db.Column(db.Integer, db.ForeignKey('dt_translation_jobs.id'), comment='任務ID')
|
|
api_endpoint = db.Column(db.String(200), nullable=False, comment='API端點')
|
|
prompt_tokens = db.Column(db.Integer, default=0, comment='Prompt token數')
|
|
completion_tokens = db.Column(db.Integer, default=0, comment='Completion token數')
|
|
total_tokens = db.Column(db.Integer, default=0, comment='總token數')
|
|
prompt_unit_price = db.Column(db.Numeric(10, 8), default=0.00000000, comment='單價')
|
|
prompt_price_unit = db.Column(db.String(20), default='USD', comment='價格單位')
|
|
cost = db.Column(db.Numeric(10, 4), default=0.0000, comment='成本')
|
|
response_time_ms = db.Column(db.Integer, default=0, comment='回應時間(毫秒)')
|
|
success = db.Column(db.Boolean, default=True, comment='是否成功')
|
|
error_message = db.Column(db.Text, comment='錯誤訊息')
|
|
created_at = db.Column(db.DateTime, default=func.now(), comment='建立時間')
|
|
|
|
def __repr__(self):
|
|
return f'<APIUsageStats {self.api_endpoint}>'
|
|
|
|
def to_dict(self):
|
|
"""轉換為字典格式"""
|
|
return {
|
|
'id': self.id,
|
|
'user_id': self.user_id,
|
|
'job_id': self.job_id,
|
|
'api_endpoint': self.api_endpoint,
|
|
'prompt_tokens': self.prompt_tokens,
|
|
'completion_tokens': self.completion_tokens,
|
|
'total_tokens': self.total_tokens,
|
|
'prompt_unit_price': float(self.prompt_unit_price) if self.prompt_unit_price else 0.0,
|
|
'prompt_price_unit': self.prompt_price_unit,
|
|
'cost': float(self.cost) if self.cost else 0.0,
|
|
'response_time_ms': self.response_time_ms,
|
|
'success': self.success,
|
|
'error_message': self.error_message,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
@classmethod
|
|
def record_api_call(cls, user_id, job_id, api_endpoint, metadata, response_time_ms, success=True, error_message=None):
|
|
"""記錄 API 呼叫統計"""
|
|
# 從 Dify API metadata 解析使用量資訊
|
|
usage_data = metadata.get('usage', {})
|
|
|
|
prompt_tokens = usage_data.get('prompt_tokens', 0)
|
|
completion_tokens = usage_data.get('completion_tokens', 0)
|
|
total_tokens = usage_data.get('total_tokens', prompt_tokens + completion_tokens)
|
|
|
|
# 計算成本 - 使用 Dify API 提供的總成本
|
|
if 'total_price' in usage_data:
|
|
# 直接使用 API 提供的總價格
|
|
cost = float(usage_data.get('total_price', 0.0))
|
|
else:
|
|
# 備用計算方式
|
|
prompt_price = float(usage_data.get('prompt_price', 0.0))
|
|
completion_price = float(usage_data.get('completion_price', 0.0))
|
|
cost = prompt_price + completion_price
|
|
|
|
# 單價資訊
|
|
prompt_unit_price = usage_data.get('prompt_unit_price', 0.0)
|
|
completion_unit_price = usage_data.get('completion_unit_price', 0.0)
|
|
prompt_price_unit = usage_data.get('currency', 'USD')
|
|
|
|
stats = cls(
|
|
user_id=user_id,
|
|
job_id=job_id,
|
|
api_endpoint=api_endpoint,
|
|
prompt_tokens=prompt_tokens,
|
|
completion_tokens=completion_tokens,
|
|
total_tokens=total_tokens,
|
|
prompt_unit_price=prompt_unit_price,
|
|
prompt_price_unit=prompt_price_unit,
|
|
cost=cost,
|
|
response_time_ms=response_time_ms,
|
|
success=success,
|
|
error_message=error_message
|
|
)
|
|
|
|
db.session.add(stats)
|
|
db.session.commit()
|
|
return stats
|
|
|
|
@classmethod
|
|
def get_user_statistics(cls, user_id, start_date=None, end_date=None):
|
|
"""取得使用者統計資料"""
|
|
query = cls.query.filter_by(user_id=user_id)
|
|
|
|
if start_date:
|
|
query = query.filter(cls.created_at >= start_date)
|
|
if end_date:
|
|
query = query.filter(cls.created_at <= end_date)
|
|
|
|
# 統計資料
|
|
total_calls = query.count()
|
|
successful_calls = query.filter_by(success=True).count()
|
|
total_tokens = query.with_entities(func.sum(cls.total_tokens)).scalar() or 0
|
|
total_cost = query.with_entities(func.sum(cls.cost)).scalar() or 0.0
|
|
avg_response_time = query.with_entities(func.avg(cls.response_time_ms)).scalar() or 0
|
|
|
|
return {
|
|
'total_calls': total_calls,
|
|
'successful_calls': successful_calls,
|
|
'failed_calls': total_calls - successful_calls,
|
|
'success_rate': (successful_calls / total_calls * 100) if total_calls > 0 else 0,
|
|
'total_tokens': total_tokens,
|
|
'total_cost': float(total_cost),
|
|
'avg_response_time': float(avg_response_time) if avg_response_time else 0
|
|
}
|
|
|
|
@classmethod
|
|
def get_daily_statistics(cls, days=30):
|
|
"""取得每日統計資料"""
|
|
end_date = datetime.utcnow()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# 按日期分組統計
|
|
daily_stats = db.session.query(
|
|
func.date(cls.created_at).label('date'),
|
|
func.count(cls.id).label('total_calls'),
|
|
func.sum(cls.total_tokens).label('total_tokens'),
|
|
func.sum(cls.cost).label('total_cost'),
|
|
func.count().filter(cls.success == True).label('successful_calls')
|
|
).filter(
|
|
cls.created_at >= start_date,
|
|
cls.created_at <= end_date
|
|
).group_by(func.date(cls.created_at)).all()
|
|
|
|
return [
|
|
{
|
|
'date': stat.date.isoformat(),
|
|
'total_calls': stat.total_calls,
|
|
'successful_calls': stat.successful_calls,
|
|
'failed_calls': stat.total_calls - stat.successful_calls,
|
|
'total_tokens': stat.total_tokens or 0,
|
|
'total_cost': float(stat.total_cost or 0)
|
|
}
|
|
for stat in daily_stats
|
|
]
|
|
|
|
@classmethod
|
|
def get_top_users(cls, limit=10, start_date=None, end_date=None):
|
|
"""取得使用量排行榜"""
|
|
query = db.session.query(
|
|
cls.user_id,
|
|
func.count(cls.id).label('total_calls'),
|
|
func.sum(cls.total_tokens).label('total_tokens'),
|
|
func.sum(cls.cost).label('total_cost')
|
|
)
|
|
|
|
if start_date:
|
|
query = query.filter(cls.created_at >= start_date)
|
|
if end_date:
|
|
query = query.filter(cls.created_at <= end_date)
|
|
|
|
top_users = query.group_by(cls.user_id).order_by(
|
|
func.sum(cls.cost).desc()
|
|
).limit(limit).all()
|
|
|
|
return [
|
|
{
|
|
'user_id': user.user_id,
|
|
'total_calls': user.total_calls,
|
|
'total_tokens': user.total_tokens or 0,
|
|
'total_cost': float(user.total_cost or 0)
|
|
}
|
|
for user in top_users
|
|
]
|
|
|
|
@classmethod
|
|
def get_cost_trend(cls, days=30):
|
|
"""取得成本趨勢"""
|
|
end_date = datetime.utcnow()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# 按日期統計成本
|
|
cost_trend = db.session.query(
|
|
func.date(cls.created_at).label('date'),
|
|
func.sum(cls.cost).label('daily_cost')
|
|
).filter(
|
|
cls.created_at >= start_date,
|
|
cls.created_at <= end_date
|
|
).group_by(func.date(cls.created_at)).order_by(
|
|
func.date(cls.created_at)
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
'date': trend.date.isoformat(),
|
|
'cost': float(trend.daily_cost or 0)
|
|
}
|
|
for trend in cost_trend
|
|
]
|
|
|
|
@classmethod
|
|
def get_endpoint_statistics(cls):
|
|
"""取得 API 端點統計"""
|
|
endpoint_stats = db.session.query(
|
|
cls.api_endpoint,
|
|
func.count(cls.id).label('total_calls'),
|
|
func.sum(cls.cost).label('total_cost'),
|
|
func.avg(cls.response_time_ms).label('avg_response_time')
|
|
).group_by(cls.api_endpoint).order_by(
|
|
func.count(cls.id).desc()
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
'endpoint': stat.api_endpoint,
|
|
'total_calls': stat.total_calls,
|
|
'total_cost': float(stat.total_cost or 0),
|
|
'avg_response_time': float(stat.avg_response_time or 0)
|
|
}
|
|
for stat in endpoint_stats
|
|
] |