Metrics
All checks were successful
Build and Deploy MoneyMaker / build-and-deploy (push) Successful in 12m22s
All checks were successful
Build and Deploy MoneyMaker / build-and-deploy (push) Successful in 12m22s
This commit is contained in:
46
analyze.py
46
analyze.py
@@ -29,6 +29,7 @@ import math
|
||||
import sqlite3
|
||||
import logging
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Silence transformer noise before importing
|
||||
@@ -78,6 +79,37 @@ MIN_COVERAGE_SPREAD = 0.0 # disabled: signal_score naturally zeros out single-
|
||||
FINBERT_MIN_CONF = 0.70 # drop neutral articles below this FinBERT confidence
|
||||
ALERT_THRESHOLD = 0.35 # signal_score > this → alert
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Claude metrics
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
METRICS_FILE = Path(__file__).parent / "metrics.json"
|
||||
|
||||
# Pricing: Claude Haiku 4.5 — https://www.anthropic.com/pricing
|
||||
_PRICE_INPUT_PER_TOKEN = 0.80 / 1_000_000 # $0.80 per MTok
|
||||
_PRICE_OUTPUT_PER_TOKEN = 4.00 / 1_000_000 # $4.00 per MTok
|
||||
|
||||
|
||||
def calc_cost(input_tokens: int, output_tokens: int) -> float:
|
||||
return round(input_tokens * _PRICE_INPUT_PER_TOKEN + output_tokens * _PRICE_OUTPUT_PER_TOKEN, 6)
|
||||
|
||||
|
||||
def update_metrics(input_tokens: int, output_tokens: int) -> None:
|
||||
"""Accumulate Claude token usage into metrics.json."""
|
||||
cost = calc_cost(input_tokens, output_tokens)
|
||||
data: dict = {}
|
||||
if METRICS_FILE.exists():
|
||||
try:
|
||||
data = json.loads(METRICS_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
data["total_calls"] = data.get("total_calls", 0) + 1
|
||||
data["total_input_tokens"] = data.get("total_input_tokens", 0) + input_tokens
|
||||
data["total_output_tokens"] = data.get("total_output_tokens", 0) + output_tokens
|
||||
data["total_cost_usd"] = round(data.get("total_cost_usd", 0.0) + cost, 6)
|
||||
data["last_updated"] = datetime.now().isoformat(timespec="seconds")
|
||||
METRICS_FILE.write_text(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model loading
|
||||
@@ -195,16 +227,16 @@ def coverage_spread_score(row) -> float:
|
||||
# Claude structured extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def claude_extract(title: str, text: str, tickers: list[str]) -> dict:
|
||||
def claude_extract(title: str, text: str, tickers: list[str]) -> tuple[dict, int, int]:
|
||||
"""
|
||||
Use Claude Haiku to extract structured financial signal.
|
||||
Returns {"confirmed_tickers", "magnitude", "timeframe", "reasoning"}.
|
||||
Returns ({"confirmed_tickers", "magnitude", "timeframe", "reasoning"}, input_tokens, output_tokens).
|
||||
"""
|
||||
import anthropic
|
||||
|
||||
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
if not api_key:
|
||||
return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": "(no API key)"}
|
||||
return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": "(no API key)"}, 0, 0
|
||||
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
ticker_ctx = "\n".join(
|
||||
@@ -245,10 +277,10 @@ Fields:
|
||||
raw = msg.content[0].text.strip()
|
||||
raw = re.sub(r"^```(?:json)?\n?", "", raw)
|
||||
raw = re.sub(r"\n?```$", "", raw)
|
||||
return json.loads(raw)
|
||||
return json.loads(raw), msg.usage.input_tokens, msg.usage.output_tokens
|
||||
except Exception as e:
|
||||
print(f" [warn] Claude failed: {e}")
|
||||
return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": str(e)[:120]}
|
||||
return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": str(e)[:120]}, 0, 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -476,7 +508,9 @@ def analyze_articles(
|
||||
claude_data: dict = {}
|
||||
if use_claude and not dry_run and os.environ.get("ANTHROPIC_API_KEY"):
|
||||
print(f" [claude] {slug[:50]}")
|
||||
claude_data = claude_extract(title, full_text, list(matches.keys()))
|
||||
claude_data, _in_tok, _out_tok = claude_extract(title, full_text, list(matches.keys()))
|
||||
if _in_tok:
|
||||
update_metrics(_in_tok, _out_tok)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 6 — yfinance momentum + scoring
|
||||
|
||||
334
dashboard.py
334
dashboard.py
@@ -28,10 +28,281 @@ from report import _c25_day_return, _unrealised_pnl, _realised_pnl, _total_fees,
|
||||
|
||||
CAPITAL = 10_000
|
||||
LOG_DIR = Path(os.getenv("LOG_DIR", str(Path(__file__).parent / "logs")))
|
||||
METRICS_FILE = Path(__file__).parent / "metrics.json"
|
||||
REFRESH = 60 # seconds
|
||||
|
||||
CAPITAL = 10_000
|
||||
LOG_DIR = Path(os.getenv("LOG_DIR", str(Path(__file__).parent / "logs")))
|
||||
METRICS_FILE = Path(__file__).parent / "metrics.json"
|
||||
REFRESH = 60 # seconds
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# ── Metrics dashboard template ────────────────────────────────────────────────
|
||||
|
||||
METRICS_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MoneyMaker · AI Costs</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eef2f7;
|
||||
--surface: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--accent: #2563eb;
|
||||
--green: #16a34a;
|
||||
--red: #dc2626;
|
||||
--yellow: #d97706;
|
||||
--purple: #7c3aed;
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
|
||||
--shadow: 0 1px 3px rgba(0,0,0,.07), 0 1px 2px rgba(0,0,0,.04);
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 24px 32px 52px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 22px; padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.header-left { display: flex; align-items: center; gap: 12px; }
|
||||
.header h1 { font-size: 1.25rem; font-weight: 800; letter-spacing: -.03em; }
|
||||
.header h1 span { color: var(--accent); }
|
||||
.header-pill {
|
||||
font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
|
||||
color: var(--purple); background: rgba(124,58,237,.09);
|
||||
padding: 3px 9px; border-radius: 20px; border: 1px solid rgba(124,58,237,.18);
|
||||
}
|
||||
.header-meta { font-size: 0.7rem; color: var(--muted); text-align: right; line-height: 1.9; }
|
||||
.back-link {
|
||||
font-size: 0.72rem; color: var(--accent); text-decoration: none; font-weight: 600;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
|
||||
/* KPI strip */
|
||||
.kpis { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 18px; }
|
||||
@media (max-width: 1000px) { .kpis { grid-template-columns: repeat(3, 1fr); } }
|
||||
@media (max-width: 600px) { .kpis { grid-template-columns: repeat(2, 1fr); } }
|
||||
.kpi {
|
||||
background: var(--surface);
|
||||
border-radius: 10px; padding: 15px 16px;
|
||||
box-shadow: var(--shadow);
|
||||
border-top: 3px solid transparent;
|
||||
}
|
||||
.kpi.blue { border-top-color: var(--accent); }
|
||||
.kpi.green { border-top-color: var(--green); }
|
||||
.kpi.red { border-top-color: var(--red); }
|
||||
.kpi.yellow { border-top-color: var(--yellow); }
|
||||
.kpi.purple { border-top-color: var(--purple); }
|
||||
.kpi-label { font-size: 0.64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .09em; color: var(--muted); margin-bottom: 9px; }
|
||||
.kpi-value { font-size: 1.6rem; font-weight: 800; line-height: 1; letter-spacing: -.04em; font-family: 'JetBrains Mono', monospace; }
|
||||
.kpi-value.sm { font-size: 1.15rem; }
|
||||
.kpi-sub { font-size: 0.71rem; color: var(--muted); margin-top: 6px; }
|
||||
|
||||
/* Charts row */
|
||||
.charts { display: grid; grid-template-columns: 1fr 320px; gap: 10px; margin-bottom: 18px; }
|
||||
@media (max-width: 900px) { .charts { grid-template-columns: 1fr; } }
|
||||
.chart-card {
|
||||
background: var(--surface); border-radius: 10px; box-shadow: var(--shadow);
|
||||
padding: 14px 16px 8px;
|
||||
}
|
||||
.chart-card-title {
|
||||
font-size: 0.64rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: .09em; color: var(--muted); margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Burn rate bar */
|
||||
.burn-section { background: var(--surface); border-radius: 10px; box-shadow: var(--shadow); padding: 16px 20px; margin-bottom: 18px; }
|
||||
.burn-title { font-size: 0.64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .09em; color: var(--muted); margin-bottom: 14px; }
|
||||
.burn-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
|
||||
.burn-label { font-size: 0.75rem; color: var(--muted); width: 130px; flex-shrink: 0; }
|
||||
.burn-bar-bg { flex: 1; height: 8px; background: #f1f5f9; border-radius: 4px; overflow: hidden; }
|
||||
.burn-bar-fg { height: 100%; border-radius: 4px; transition: width .6s ease; }
|
||||
.burn-val { font-size: 0.72rem; font-family: 'JetBrains Mono', monospace; color: var(--text); width: 80px; text-align: right; flex-shrink: 0; }
|
||||
|
||||
/* Info table */
|
||||
.section { background: var(--surface); border-radius: 10px; box-shadow: var(--shadow); overflow: hidden; }
|
||||
.section-header {
|
||||
padding: 11px 16px; font-size: 0.64rem; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: .09em;
|
||||
color: var(--muted); background: #f8fafc;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { padding: 8px 14px; font-size: 0.63rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); text-align: left; border-bottom: 1px solid var(--border); background: #f8fafc; }
|
||||
td { padding: 10px 14px; font-size: 0.81rem; border-bottom: 1px solid #f1f5f9; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: rgba(37,99,235,.025); }
|
||||
td.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.77rem; }
|
||||
.empty { padding: 24px 16px; font-size: 0.8rem; color: var(--muted); font-style: italic; text-align: center; }
|
||||
|
||||
.footer { font-size: 0.64rem; color: #cbd5e1; text-align: center; margin-top: 32px; letter-spacing: .05em; }
|
||||
.no-data { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 60px 20px; }
|
||||
.no-data-icon { font-size: 2.5rem; }
|
||||
.no-data-text { font-size: 0.88rem; color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<h1>Money<span>Maker</span></h1>
|
||||
<span class="header-pill">AI Costs</span>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
{{ now }}<br>
|
||||
<a class="back-link" href="/">← Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if no_data %}
|
||||
<div class="section">
|
||||
<div class="no-data">
|
||||
<div class="no-data-icon">📊</div>
|
||||
<div class="no-data-text">Ingen data endnu — kør analyze.py med Claude for at begynde at tracke forbrug.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="kpis">
|
||||
<div class="kpi red">
|
||||
<div class="kpi-label">Total Cost</div>
|
||||
<div class="kpi-value{% if total_cost_usd >= 1 %} sm{% endif %}">${{ "%.4f"|format(total_cost_usd) }}</div>
|
||||
<div class="kpi-sub">USD spent</div>
|
||||
</div>
|
||||
<div class="kpi blue">
|
||||
<div class="kpi-label">Claude Calls</div>
|
||||
<div class="kpi-value">{{ total_calls }}</div>
|
||||
<div class="kpi-sub">articles analyzed</div>
|
||||
</div>
|
||||
<div class="kpi yellow">
|
||||
<div class="kpi-label">Input Tokens</div>
|
||||
<div class="kpi-value sm">{{ "{:,}".format(total_input_tokens) }}</div>
|
||||
<div class="kpi-sub">${{ "%.4f"|format(cost_input) }} · {{ input_pct }}%</div>
|
||||
</div>
|
||||
<div class="kpi purple">
|
||||
<div class="kpi-label">Output Tokens</div>
|
||||
<div class="kpi-value sm">{{ "{:,}".format(total_output_tokens) }}</div>
|
||||
<div class="kpi-sub">${{ "%.4f"|format(cost_output) }} · {{ output_pct }}%</div>
|
||||
</div>
|
||||
<div class="kpi green">
|
||||
<div class="kpi-label">Cost / Article</div>
|
||||
<div class="kpi-value{% if cost_per_call >= 0.01 %} sm{% endif %}">${{ "%.5f"|format(cost_per_call) }}</div>
|
||||
<div class="kpi-sub">avg per Claude call</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts">
|
||||
<div class="chart-card">
|
||||
<div class="chart-card-title">Token Distribution — Input vs Output</div>
|
||||
<div id="bar-chart" style="height:220px;"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-card-title">Cost Split</div>
|
||||
<div id="donut-chart" style="height:220px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="burn-section">
|
||||
<div class="burn-title">Token Breakdown</div>
|
||||
<div class="burn-row">
|
||||
<div class="burn-label">Input tokens</div>
|
||||
<div class="burn-bar-bg"><div class="burn-bar-fg" style="width:{{ input_pct }}%; background: var(--yellow);"></div></div>
|
||||
<div class="burn-val">{{ "{:,}".format(total_input_tokens) }}</div>
|
||||
</div>
|
||||
<div class="burn-row">
|
||||
<div class="burn-label">Output tokens</div>
|
||||
<div class="burn-bar-bg"><div class="burn-bar-fg" style="width:{{ output_pct }}%; background: var(--purple);"></div></div>
|
||||
<div class="burn-val">{{ "{:,}".format(total_output_tokens) }}</div>
|
||||
</div>
|
||||
<div class="burn-row">
|
||||
<div class="burn-label">Total tokens</div>
|
||||
<div class="burn-bar-bg"><div class="burn-bar-fg" style="width:100%; background: var(--accent);"></div></div>
|
||||
<div class="burn-val">{{ "{:,}".format(total_input_tokens + total_output_tokens) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">Model Details</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Value</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Model</td><td class="mono">claude-haiku-4-5</td><td>Anthropic</td></tr>
|
||||
<tr><td>Input price</td><td class="mono">$0.80 / MTok</td><td>${{ "%.6f"|format(0.80/1_000_000) }} per token</td></tr>
|
||||
<tr><td>Output price</td><td class="mono">$4.00 / MTok</td><td>${{ "%.6f"|format(4.00/1_000_000) }} per token</td></tr>
|
||||
<tr><td>Total calls</td><td class="mono">{{ total_calls }}</td><td>articles sent to Claude</td></tr>
|
||||
<tr><td>Total input tokens</td><td class="mono">{{ "{:,}".format(total_input_tokens) }}</td><td>cost ${{ "%.5f"|format(cost_input) }}</td></tr>
|
||||
<tr><td>Total output tokens</td><td class="mono">{{ "{:,}".format(total_output_tokens) }}</td><td>cost ${{ "%.5f"|format(cost_output) }}</td></tr>
|
||||
<tr><td>Total cost</td><td class="mono">${{ "%.6f"|format(total_cost_usd) }}</td><td>cumulative</td></tr>
|
||||
<tr><td>Last updated</td><td class="mono">{{ last_updated }}</td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Bar chart: tokens per type ──────────────────────────────────────────────
|
||||
const barChart = echarts.init(document.getElementById('bar-chart'));
|
||||
barChart.setOption({
|
||||
grid: { top: 24, right: 20, bottom: 28, left: 70 },
|
||||
tooltip: { trigger: 'axis', formatter: p => p[0].name + '<br/>' + p[0].marker + ' ' + p[0].seriesName + ': <b>' + p[0].value.toLocaleString() + '</b>' },
|
||||
xAxis: { type: 'category', data: ['Input', 'Output'], axisLabel: { fontSize: 11 } },
|
||||
yAxis: { type: 'value', axisLabel: { fontSize: 10, formatter: v => v >= 1000 ? (v/1000).toFixed(0)+'k' : v } },
|
||||
series: [{
|
||||
name: 'Tokens',
|
||||
type: 'bar', barMaxWidth: 60,
|
||||
data: [
|
||||
{ value: {{ total_input_tokens }}, itemStyle: { color: '#d97706', borderRadius: [4,4,0,0] } },
|
||||
{ value: {{ total_output_tokens }}, itemStyle: { color: '#7c3aed', borderRadius: [4,4,0,0] } },
|
||||
],
|
||||
label: { show: true, position: 'top', formatter: p => p.value.toLocaleString(), fontSize: 10, color: '#64748b' }
|
||||
}]
|
||||
});
|
||||
|
||||
// ── Donut: cost split ───────────────────────────────────────────────────────
|
||||
const donut = echarts.init(document.getElementById('donut-chart'));
|
||||
donut.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: ${c} ({d}%)' },
|
||||
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 10, color: '#64748b' } },
|
||||
series: [{
|
||||
type: 'pie', radius: ['42%', '68%'],
|
||||
center: ['50%', '44%'],
|
||||
label: { show: false },
|
||||
emphasis: { label: { show: true, fontSize: 12, fontWeight: 'bold' } },
|
||||
data: [
|
||||
{ value: {{ "%.6f"|format(cost_input) }}, name: 'Input', itemStyle: { color: '#d97706' } },
|
||||
{ value: {{ "%.6f"|format(cost_output) }}, name: 'Output', itemStyle: { color: '#7c3aed' } },
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => { barChart.resize(); donut.resize(); });
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">MoneyMaker · claude-haiku-4-5 · metrics updated on each analyze run</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# ── HTML template ─────────────────────────────────────────────────────────────
|
||||
|
||||
TEMPLATE = """<!DOCTYPE html>
|
||||
@@ -762,6 +1033,69 @@ def health():
|
||||
return {"status": "ok", "db": DB_TYPE, "ts": time.time()}
|
||||
|
||||
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
data: dict = {}
|
||||
if METRICS_FILE.exists():
|
||||
try:
|
||||
data = json.loads(METRICS_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Total articles with Claude signal from DB
|
||||
try:
|
||||
db = get_conn()
|
||||
row = db.execute(
|
||||
"SELECT COUNT(DISTINCT article_slug) AS cnt FROM article_signals WHERE claude_reasoning != '' AND claude_reasoning IS NOT NULL"
|
||||
).fetchone()
|
||||
data["total_articles_with_claude"] = row[0] if row else 0
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@app.route("/metrics-dash")
|
||||
def metrics_dash():
|
||||
data: dict = {}
|
||||
if METRICS_FILE.exists():
|
||||
try:
|
||||
data = json.loads(METRICS_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
total_calls = data.get("total_calls", 0)
|
||||
total_input_tokens = data.get("total_input_tokens", 0)
|
||||
total_output_tokens = data.get("total_output_tokens", 0)
|
||||
total_cost_usd = data.get("total_cost_usd", 0.0)
|
||||
last_updated = data.get("last_updated", "—")
|
||||
|
||||
total_tokens = total_input_tokens + total_output_tokens
|
||||
input_pct = round(total_input_tokens / total_tokens * 100) if total_tokens else 0
|
||||
output_pct = round(total_output_tokens / total_tokens * 100) if total_tokens else 0
|
||||
|
||||
cost_input = round(total_input_tokens * 0.80 / 1_000_000, 6)
|
||||
cost_output = round(total_output_tokens * 4.00 / 1_000_000, 6)
|
||||
cost_per_call = round(total_cost_usd / total_calls, 6) if total_calls else 0.0
|
||||
|
||||
return render_template_string(
|
||||
METRICS_TEMPLATE,
|
||||
no_data=total_calls == 0,
|
||||
now=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
|
||||
total_calls=total_calls,
|
||||
total_input_tokens=total_input_tokens,
|
||||
total_output_tokens=total_output_tokens,
|
||||
total_cost_usd=total_cost_usd,
|
||||
last_updated=last_updated,
|
||||
input_pct=input_pct,
|
||||
output_pct=output_pct,
|
||||
cost_input=cost_input,
|
||||
cost_output=cost_output,
|
||||
cost_per_call=cost_per_call,
|
||||
)
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
|
||||
Reference in New Issue
Block a user