feat: unified dashboard — all dashboard_for_sage metrics from data mart, 6 stat cards, perf+ops
This commit is contained in:
parent
92572bcf6b
commit
054b0dd8e2
136
sage_datamark/dashboards.py
Normal file
136
sage_datamark/dashboards.py
Normal file
@ -0,0 +1,136 @@
|
||||
"""
|
||||
统一 Dashboard API — 从集市表查询所有运营指标 + 性能指标
|
||||
替代 dashboard_for_sage 中对 llmusage 的直接查询
|
||||
"""
|
||||
from sqlor.dbpools import get_sor_context
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
# ── 统计卡片 ──
|
||||
|
||||
async def get_today_usage(env, sor):
|
||||
"""当天调用总数"""
|
||||
sql = "SELECT COUNT(*) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$"
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString()})
|
||||
return int(recs[0].cnt) if recs else 0
|
||||
|
||||
|
||||
async def get_today_amount(env, sor):
|
||||
"""当天总金额"""
|
||||
sql = "SELECT COALESCE(SUM(amount), 0) as total FROM dm_model_call_fact WHERE call_date = ${today}$"
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString()})
|
||||
return round(float(recs[0].total), 2) if recs else 0
|
||||
|
||||
|
||||
async def get_success_rate(env, sor):
|
||||
"""当天成功率"""
|
||||
sql = """SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status='SUCCEEDED' THEN 1 ELSE 0 END) as success
|
||||
FROM dm_model_call_fact WHERE call_date = ${today}$"""
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString()})
|
||||
if recs and recs[0].total:
|
||||
return round(recs[0].success * 100.0 / recs[0].total, 1)
|
||||
return 0
|
||||
|
||||
|
||||
async def get_fail_count(env, sor):
|
||||
"""当天失败数"""
|
||||
sql = "SELECT COUNT(*) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$ AND status = 'FAILED'"
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString()})
|
||||
return int(recs[0].cnt) if recs else 0
|
||||
|
||||
|
||||
async def get_active_users(env, sor):
|
||||
"""当天活跃用户数"""
|
||||
sql = "SELECT COUNT(DISTINCT userid) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$"
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString()})
|
||||
return int(recs[0].cnt) if recs else 0
|
||||
|
||||
|
||||
# ── 排行榜 ──
|
||||
|
||||
async def get_top_models(env, sor, limit=5):
|
||||
"""当天 Top N 模型(按调用次数)"""
|
||||
sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM dm_model_call_fact WHERE call_date = ${today}$
|
||||
GROUP BY model ORDER BY cnt DESC LIMIT ${limit}$"""
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'limit': str(limit)})
|
||||
return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
||||
|
||||
|
||||
async def get_top_providers(env, sor, limit=5):
|
||||
"""当天 Top N 供应商(按金额)"""
|
||||
sql = """SELECT providerid, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM dm_model_call_fact WHERE call_date = ${today}$ AND providerid IS NOT NULL
|
||||
GROUP BY providerid ORDER BY total_amount DESC LIMIT ${limit}$"""
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'limit': str(limit)})
|
||||
return [{'provider_name': r.providerid or 'Unknown', 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
||||
|
||||
|
||||
async def get_top_users(env, sor, limit=5):
|
||||
"""全量 Top N 用户(按金额)"""
|
||||
sql = """SELECT userid, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM dm_model_call_fact GROUP BY userid ORDER BY total_amount DESC LIMIT ${limit}$"""
|
||||
recs = await sor.sqlExe(sql, {'limit': str(limit)})
|
||||
return [{'user_name': r.userid, 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
||||
|
||||
|
||||
# ── 客户维度 ──
|
||||
|
||||
async def get_customer_daily_models(env, sor, userorgid):
|
||||
"""客户当天各模型用量"""
|
||||
sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM dm_model_call_fact WHERE call_date = ${today}$ AND userorgid = ${orgid}$
|
||||
GROUP BY model ORDER BY cnt DESC"""
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'orgid': userorgid})
|
||||
return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 4)} for r in recs]
|
||||
|
||||
|
||||
async def get_customer_monthly_models(env, sor, userorgid):
|
||||
"""客户当月各模型用量"""
|
||||
now = datetime.now()
|
||||
month_start = now.strftime('%Y-%m-01')
|
||||
sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM dm_model_call_fact WHERE call_date >= ${start}$ AND userorgid = ${orgid}$
|
||||
GROUP BY model ORDER BY cnt DESC"""
|
||||
recs = await sor.sqlExe(sql, {'start': month_start, 'orgid': userorgid})
|
||||
return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 4)} for r in recs]
|
||||
|
||||
|
||||
async def get_customer_user_today(env, sor, userorgid):
|
||||
"""客户当天各用户用量"""
|
||||
sql = """SELECT userid, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM dm_model_call_fact WHERE call_date = ${today}$ AND userorgid = ${orgid}$
|
||||
GROUP BY userid ORDER BY cnt DESC"""
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'orgid': userorgid})
|
||||
return [{'user_name': r.userid, 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 4)} for r in recs]
|
||||
|
||||
|
||||
# ── 趋势 ──
|
||||
|
||||
async def get_daily_trend(env, sor, days=7):
|
||||
"""最近 N 天调用量和金额趋势"""
|
||||
sql = """SELECT call_date, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM dm_model_call_fact
|
||||
WHERE call_date >= ${start}$
|
||||
GROUP BY call_date ORDER BY call_date"""
|
||||
start = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
recs = await sor.sqlExe(sql, {'start': start})
|
||||
return [{'date': r.call_date, 'cnt': int(r.cnt),
|
||||
'amount': round(float(r.total_amount), 2)} for r in recs]
|
||||
|
||||
|
||||
async def get_hourly_concurrency(env, sor):
|
||||
"""当天每小时并发(活跃用户)"""
|
||||
sql = """SELECT call_hour, COUNT(DISTINCT userid) as users, COUNT(*) as calls
|
||||
FROM dm_model_call_fact WHERE call_date = ${today}$
|
||||
GROUP BY call_hour ORDER BY call_hour"""
|
||||
recs = await sor.sqlExe(sql, {'today': env.curDateString()})
|
||||
return [{'hour': r.call_hour, 'users': int(r.users), 'calls': int(r.calls)} for r in recs]
|
||||
@ -1,23 +1,24 @@
|
||||
"""sage_datamark 模块初始化"""
|
||||
"""sage_datamark 模块初始化 — 统一 Dashboard API"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.log import debug, exception
|
||||
from sqlor.dbpools import DBPools
|
||||
from sqlor.dbpools import DBPools, get_sor_context
|
||||
from .etl import sync_call_fact, aggregate_daily_perf, aggregate_provider_cost, ensure_tables
|
||||
from .dashboards import (
|
||||
get_today_usage, get_today_amount, get_success_rate, get_fail_count,
|
||||
get_active_users, get_top_models, get_top_providers, get_top_users,
|
||||
get_customer_daily_models, get_customer_monthly_models,
|
||||
get_customer_user_today, get_daily_trend, get_hourly_concurrency,
|
||||
)
|
||||
|
||||
MODULE_NAME = "sage_datamark"
|
||||
|
||||
|
||||
def get_dbname():
|
||||
env = ServerEnv()
|
||||
return env.get_module_dbname(MODULE_NAME)
|
||||
|
||||
# ── 性能指标 API ──
|
||||
|
||||
async def api_model_perf(sor, params_kw=None):
|
||||
"""模型性能统计 API — POST /sage_datamark/api/model_perf.dspy"""
|
||||
model = params_kw.get('model') if params_kw else None
|
||||
stat_date = params_kw.get('date') if params_kw else None
|
||||
|
||||
sql = "SELECT * FROM dm_model_perf_daily WHERE 1=1"
|
||||
ns = {}
|
||||
if stat_date:
|
||||
@ -27,16 +28,13 @@ async def api_model_perf(sor, params_kw=None):
|
||||
sql += " AND model=${model}$"
|
||||
ns['model'] = model
|
||||
sql += " ORDER BY total_calls DESC LIMIT 50"
|
||||
|
||||
rows = await sor.sqlExe(sql, ns)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def api_provider_roi(sor, params_kw=None):
|
||||
"""供应商性价比 API — POST /sage_datamark/api/provider_roi.dspy"""
|
||||
model = params_kw.get('model') if params_kw else None
|
||||
stat_date = params_kw.get('date') if params_kw else None
|
||||
|
||||
sql = "SELECT * FROM dm_provider_cost_daily WHERE 1=1"
|
||||
ns = {}
|
||||
if stat_date:
|
||||
@ -46,33 +44,96 @@ async def api_provider_roi(sor, params_kw=None):
|
||||
sql += " AND model=${model}$"
|
||||
ns['model'] = model
|
||||
sql += " ORDER BY total_amount DESC LIMIT 50"
|
||||
|
||||
rows = await sor.sqlExe(sql, ns)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def api_call_detail(sor, params_kw=None):
|
||||
"""调用明细 API — POST /sage_datamark/api/call_detail.dspy"""
|
||||
stat_date = (params_kw or {}).get('date')
|
||||
userorgid = (params_kw or {}).get('userorgid')
|
||||
# ── 运营指标 API(替代 dashboard_for_sage)──
|
||||
|
||||
sql = "SELECT * FROM dm_model_call_fact WHERE 1=1"
|
||||
ns = {}
|
||||
if stat_date:
|
||||
sql += " AND call_date=${stat_date}$"
|
||||
ns['stat_date'] = stat_date
|
||||
if userorgid:
|
||||
sql += " AND userorgid=${userorgid}$"
|
||||
ns['userorgid'] = userorgid
|
||||
sql += " ORDER BY call_time DESC LIMIT 200"
|
||||
async def api_stats(request):
|
||||
"""统计卡片数据 — /sage_datamark/api/stats.dspy"""
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return {
|
||||
'today_usage': await get_today_usage(env, sor),
|
||||
'today_amount': await get_today_amount(env, sor),
|
||||
'success_rate': await get_success_rate(env, sor),
|
||||
'fail_count': await get_fail_count(env, sor),
|
||||
'active_users': await get_active_users(env, sor),
|
||||
}
|
||||
|
||||
rows = await sor.sqlExe(sql, ns)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
async def api_top_models(request):
|
||||
"""Top 模型 — /sage_datamark/api/top_models.dspy"""
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_top_models(env, sor)
|
||||
|
||||
|
||||
async def api_top_providers(request):
|
||||
"""Top 供应商 — /sage_datamark/api/top_providers.dspy"""
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_top_providers(env, sor)
|
||||
|
||||
|
||||
async def api_top_users(request):
|
||||
"""Top 用户 — /sage_datamark/api/top_users.dspy"""
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_top_users(env, sor)
|
||||
|
||||
|
||||
async def api_customer_models(request):
|
||||
"""客户用量 — /sage_datamark/api/customer_models.dspy"""
|
||||
env = request._run_ns
|
||||
userorgid = await env.get_userorgid()
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return {
|
||||
'daily': await get_customer_daily_models(env, sor, userorgid),
|
||||
'monthly': await get_customer_monthly_models(env, sor, userorgid),
|
||||
}
|
||||
|
||||
|
||||
async def api_daily_trend(request):
|
||||
"""7天趋势 — /sage_datamark/api/daily_trend.dspy"""
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_daily_trend(env, sor)
|
||||
|
||||
|
||||
async def api_hourly_concurrency(request):
|
||||
"""每小时并发 — /sage_datamark/api/hourly_concurrency.dspy"""
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_hourly_concurrency(env, sor)
|
||||
|
||||
|
||||
# ── Jinja2 模板函数(给 stat_*.ui 调用)──
|
||||
|
||||
async def j2_today_usage(request):
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_today_usage(env, sor)
|
||||
|
||||
|
||||
async def j2_today_amount(request):
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_today_amount(env, sor)
|
||||
|
||||
|
||||
def load_sage_datamark():
|
||||
env = ServerEnv()
|
||||
env.api_model_perf = api_model_perf
|
||||
env.api_provider_roi = api_provider_roi
|
||||
env.api_call_detail = api_call_detail
|
||||
debug("[sage_datamark] registered API endpoints")
|
||||
env.api_stats = api_stats
|
||||
env.api_top_models = api_top_models
|
||||
env.api_top_providers = api_top_providers
|
||||
env.api_top_users = api_top_users
|
||||
env.api_customer_models = api_customer_models
|
||||
env.api_daily_trend = api_daily_trend
|
||||
env.api_hourly_concurrency = api_hourly_concurrency
|
||||
env.j2_today_usage = j2_today_usage
|
||||
env.j2_today_amount = j2_today_amount
|
||||
debug("[sage_datamark] registered all API endpoints")
|
||||
|
||||
@ -11,10 +11,13 @@ PATHS_LOGINED = [
|
||||
f"/{MOD}",
|
||||
f"/{MOD}/index.ui",
|
||||
f"/{MOD}/provider_roi.ui",
|
||||
f"/{MOD}/menu.ui",
|
||||
f"/{MOD}/stat_avg_ttft.ui",
|
||||
f"/{MOD}/stat_success_rate.ui",
|
||||
f"/{MOD}/stat_total_calls.ui",
|
||||
f"/{MOD}/stat_fail_calls.ui",
|
||||
f"/{MOD}/stat_today_usage.ui",
|
||||
f"/{MOD}/stat_today_amount.ui",
|
||||
f"/{MOD}/api/%",
|
||||
]
|
||||
|
||||
|
||||
4
wwwroot/api/customer_models.dspy
Normal file
4
wwwroot/api/customer_models.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# 客户用量 API
|
||||
import json
|
||||
data = await api_customer_models(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
4
wwwroot/api/daily_trend.dspy
Normal file
4
wwwroot/api/daily_trend.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# 7天趋势 API
|
||||
import json
|
||||
data = await api_daily_trend(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
4
wwwroot/api/hourly_concurrency.dspy
Normal file
4
wwwroot/api/hourly_concurrency.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# 每小时并发 API
|
||||
import json
|
||||
data = await api_hourly_concurrency(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
4
wwwroot/api/stats.dspy
Normal file
4
wwwroot/api/stats.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# 统计卡片 JSON API
|
||||
import json
|
||||
data = await api_stats(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
4
wwwroot/api/top_models.dspy
Normal file
4
wwwroot/api/top_models.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# Top 模型 API
|
||||
import json
|
||||
data = await api_top_models(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
4
wwwroot/api/top_providers.dspy
Normal file
4
wwwroot/api/top_providers.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# Top 供应商 API
|
||||
import json
|
||||
data = await api_top_providers(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
4
wwwroot/api/top_users.dspy
Normal file
4
wwwroot/api/top_users.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# Top 用户 API
|
||||
import json
|
||||
data = await api_top_users(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
168
wwwroot/index.ui
168
wwwroot/index.ui
@ -1,3 +1,4 @@
|
||||
{% set roles = get_user_roles(get_user()) %}
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "datamart_root",
|
||||
@ -11,46 +12,157 @@
|
||||
"widgettype": "HBox",
|
||||
"options": {"width": "100%", "alignItems": "center", "marginBottom": "12px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "模型性能监控", "i18n": true}},
|
||||
{"widgettype": "Filler"},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {"label": "刷新", "fontSize": "12px"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "-@Cols", "method": "render_urldata", "params": {}}]
|
||||
}
|
||||
{"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "数据概览", "i18n": true}},
|
||||
{"widgettype": "Filler"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {"width": "100%", "gap": "16px", "marginBottom": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "RefreshWidget", "id": "stat_avg_ttft", "options": {"period_seconds": 60, "url": "{{entire_url('stat_avg_ttft.ui')}}", "width": "25%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_success_rate", "options": {"period_seconds": 60, "url": "{{entire_url('stat_success_rate.ui')}}", "width": "25%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_total_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_total_calls.ui')}}", "width": "25%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_fail_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_fail_calls.ui')}}", "width": "25%"}}
|
||||
{"widgettype": "RefreshWidget", "id": "stat_today_usage", "options": {"period_seconds": 30, "url": "{{entire_url('stat_today_usage.ui')}}", "width": "16%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_today_amount", "options": {"period_seconds": 30, "url": "{{entire_url('stat_today_amount.ui')}}", "width": "16%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_success_rate", "options": {"period_seconds": 60, "url": "{{entire_url('stat_success_rate.ui')}}", "width": "16%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_fail_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_fail_calls.ui')}}", "width": "16%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_avg_ttft", "options": {"period_seconds": 60, "url": "{{entire_url('stat_avg_ttft.ui')}}", "width": "16%"}},
|
||||
{"widgettype": "RefreshWidget", "id": "stat_total_users", "options": {"period_seconds": 60, "url": "{{entire_url('stat_total_users.ui')}}", "width": "16%"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "Cols",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"col_cwidth": 28,
|
||||
"title": "模型性能排行",
|
||||
"data_url": "{{entire_url('api/model_perf.dspy')}}",
|
||||
"data_params": {},
|
||||
"record_view": {
|
||||
"widgettype": "HBox",
|
||||
"options": {"cheight": 4, "width": "100%"},
|
||||
"widgettype": "HBox",
|
||||
"options": {"width": "100%", "gap": "16px", "marginBottom": "16px"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "width": "50%", "padding": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "${model}", "width": "30%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${avg_ttot_ms}ms", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${success_rate}%", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${total_calls}", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "10%"}}
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "模型性能排行", "i18n": true, "marginBottom": "8px"}},
|
||||
{
|
||||
"widgettype": "Cols",
|
||||
"options": {
|
||||
"col_cwidth": 24,
|
||||
"data_url": "{{entire_url('api/model_perf.dspy')}}",
|
||||
"data_params": {},
|
||||
"record_view": {
|
||||
"widgettype": "HBox",
|
||||
"options": {"cheight": 3, "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "${model}", "width": "35%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${success_rate}%", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${total_calls}", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "20%"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "width": "50%", "padding": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "供应商性价比", "i18n": true, "marginBottom": "8px"}},
|
||||
{
|
||||
"widgettype": "Cols",
|
||||
"options": {
|
||||
"col_cwidth": 24,
|
||||
"data_url": "{{entire_url('api/provider_roi.dspy')}}",
|
||||
"data_params": {},
|
||||
"record_view": {
|
||||
"widgettype": "HBox",
|
||||
"options": {"cheight": 3, "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "${model}", "width": "35%"}},
|
||||
{"widgettype": "Text", "options": {"text": "¥${unit_price}/t", "width": "20%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${total_calls}次", "width": "15%"}},
|
||||
{"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "15%"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {"width": "100%", "gap": "16px"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "width": "33%", "padding": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "热门模型", "i18n": true, "marginBottom": "8px"}},
|
||||
{
|
||||
"widgettype": "Cols",
|
||||
"options": {
|
||||
"col_cwidth": 20,
|
||||
"data_url": "{{entire_url('api/top_models.dspy')}}",
|
||||
"data_params": {},
|
||||
"record_view": {
|
||||
"widgettype": "HBox",
|
||||
"options": {"cheight": 3, "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "${model_name}", "width": "50%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${cnt}次", "width": "25%"}},
|
||||
{"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "25%"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "width": "33%", "padding": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "用户排行", "i18n": true, "marginBottom": "8px"}},
|
||||
{
|
||||
"widgettype": "Cols",
|
||||
"options": {
|
||||
"col_cwidth": 20,
|
||||
"data_url": "{{entire_url('api/top_users.dspy')}}",
|
||||
"data_params": {},
|
||||
"record_view": {
|
||||
"widgettype": "HBox",
|
||||
"options": {"cheight": 3, "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "${user_name}", "width": "50%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${cnt}次", "width": "25%"}},
|
||||
{"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "25%"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "width": "33%", "padding": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "供应商排行", "i18n": true, "marginBottom": "8px"}},
|
||||
{
|
||||
"widgettype": "Cols",
|
||||
"options": {
|
||||
"col_cwidth": 20,
|
||||
"data_url": "{{entire_url('api/top_providers.dspy')}}",
|
||||
"data_params": {},
|
||||
"record_view": {
|
||||
"widgettype": "HBox",
|
||||
"options": {"cheight": 3, "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "${provider_name}", "width": "50%"}},
|
||||
{"widgettype": "Text", "options": {"text": "${cnt}次", "width": "25%"}},
|
||||
{"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "25%"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
{% set rows = api_model_perf(request) %}
|
||||
{% set rows = api_model_perf(None) %}
|
||||
{% set total = 0 %}{% set succ = 0 %}
|
||||
{% for r in rows %}{% set total = total + r.total_calls %}{% set succ = succ + r.success_calls %}{% endfor %}
|
||||
{% set rate = (succ * 100 / total)|round(1) if total > 0 else 0 %}
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "成功率", "fontSize": "12px", "color": "var(--sage-text-secondary)"}},
|
||||
{"widgettype": "Title2", "options": {"text": "{{rate}}%", "fontWeight": "700", "marginTop": "4px"}}
|
||||
]
|
||||
}
|
||||
{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"成功率","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{rate}}%","fontWeight":"700","marginTop":"4px"}}]}
|
||||
|
||||
9
wwwroot/stat_today_amount.ui
Normal file
9
wwwroot/stat_today_amount.ui
Normal file
@ -0,0 +1,9 @@
|
||||
{% set amt = j2_today_amount(request) %}
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "今日金额", "fontSize": "12px", "color": "var(--sage-text-secondary)"}},
|
||||
{"widgettype": "Title2", "options": {"text": "¥{{'%.2f' % amt}}", "fontWeight": "700", "marginTop": "4px"}}
|
||||
]
|
||||
}
|
||||
9
wwwroot/stat_today_usage.ui
Normal file
9
wwwroot/stat_today_usage.ui
Normal file
@ -0,0 +1,9 @@
|
||||
{% set cnt = j2_today_usage(request) %}
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "今日调用", "fontSize": "12px", "color": "var(--sage-text-secondary)"}},
|
||||
{"widgettype": "Title2", "options": {"text": "{{cnt}}", "fontWeight": "700", "marginTop": "4px"}}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user