ctxoptimizer 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ctxoptimizer/__init__.py +5 -0
- ctxoptimizer/__main__.py +14 -0
- ctxoptimizer/admin_security.py +130 -0
- ctxoptimizer/analysis/__init__.py +1 -0
- ctxoptimizer/analysis/baseline.py +69 -0
- ctxoptimizer/analysis/break_even.py +49 -0
- ctxoptimizer/analysis/drift.py +151 -0
- ctxoptimizer/analysis/edits.py +148 -0
- ctxoptimizer/analysis/markers.py +34 -0
- ctxoptimizer/analysis/recommend.py +114 -0
- ctxoptimizer/analysis/segments.py +57 -0
- ctxoptimizer/analysis/session.py +62 -0
- ctxoptimizer/analysis/state.py +239 -0
- ctxoptimizer/analysis/tokens.py +62 -0
- ctxoptimizer/analysis/waste.py +322 -0
- ctxoptimizer/cli.py +873 -0
- ctxoptimizer/config.py +512 -0
- ctxoptimizer/dashboard/__init__.py +1 -0
- ctxoptimizer/dashboard/api.py +537 -0
- ctxoptimizer/dashboard/service.py +157 -0
- ctxoptimizer/dashboard/static/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/doctor.html +46 -0
- ctxoptimizer/dashboard/templates/live.html +91 -0
- ctxoptimizer/dashboard/templates/overview.html +172 -0
- ctxoptimizer/dashboard/templates/pricing.html +115 -0
- ctxoptimizer/dashboard/templates/request.html +111 -0
- ctxoptimizer/dashboard/templates/session.html +67 -0
- ctxoptimizer/dashboard/templates/settings.html +79 -0
- ctxoptimizer/dashboard/templates/table.html +56 -0
- ctxoptimizer/dashboard/templates/transforms.html +118 -0
- ctxoptimizer/dashboard/templates/waste.html +120 -0
- ctxoptimizer/dashboard/views.py +429 -0
- ctxoptimizer/data/cache_limits.json +23 -0
- ctxoptimizer/data/default_config.toml +185 -0
- ctxoptimizer/data/pricing.json +421 -0
- ctxoptimizer/doctor.py +129 -0
- ctxoptimizer/guard.py +139 -0
- ctxoptimizer/ids.py +73 -0
- ctxoptimizer/log.py +73 -0
- ctxoptimizer/mcp.py +124 -0
- ctxoptimizer/models.py +145 -0
- ctxoptimizer/pipeline.py +126 -0
- ctxoptimizer/profiles.py +152 -0
- ctxoptimizer/providers/__init__.py +37 -0
- ctxoptimizer/providers/anthropic.py +105 -0
- ctxoptimizer/providers/base.py +191 -0
- ctxoptimizer/providers/gemini.py +143 -0
- ctxoptimizer/providers/ollama.py +133 -0
- ctxoptimizer/providers/openai_chat.py +135 -0
- ctxoptimizer/providers/openai_responses.py +121 -0
- ctxoptimizer/proxy.py +665 -0
- ctxoptimizer/routing.py +259 -0
- ctxoptimizer/server.py +319 -0
- ctxoptimizer/service.py +150 -0
- ctxoptimizer/telemetry/__init__.py +1 -0
- ctxoptimizer/telemetry/blobs.py +57 -0
- ctxoptimizer/telemetry/expand.py +62 -0
- ctxoptimizer/telemetry/pricing.py +168 -0
- ctxoptimizer/telemetry/redact.py +53 -0
- ctxoptimizer/telemetry/sse.py +145 -0
- ctxoptimizer/telemetry/store.py +829 -0
- ctxoptimizer/telemetry/usage.py +435 -0
- ctxoptimizer/transforms/__init__.py +87 -0
- ctxoptimizer/transforms/ansi.py +21 -0
- ctxoptimizer/transforms/anthropic_cache.py +100 -0
- ctxoptimizer/transforms/backref.py +74 -0
- ctxoptimizer/transforms/base.py +93 -0
- ctxoptimizer/transforms/cache_serve.py +146 -0
- ctxoptimizer/transforms/dedupe_lines.py +69 -0
- ctxoptimizer/transforms/eligibility.py +24 -0
- ctxoptimizer/transforms/openai_hints.py +84 -0
- ctxoptimizer/transforms/progress.py +52 -0
- ctxoptimizer/transforms/stale_result_elision.py +109 -0
- ctxoptimizer/transforms/truncate.py +100 -0
- ctxoptimizer/transforms/volatile.py +177 -0
- ctxoptimizer/transforms/whitespace.py +25 -0
- ctxoptimizer-0.4.1.dist-info/METADATA +196 -0
- ctxoptimizer-0.4.1.dist-info/RECORD +83 -0
- ctxoptimizer-0.4.1.dist-info/WHEEL +4 -0
- ctxoptimizer-0.4.1.dist-info/entry_points.txt +3 -0
- ctxoptimizer-0.4.1.dist-info/licenses/LICENSE +21 -0
- ctxoptimizer-0.4.1.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ctxoptimizer — {{ title }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body { margin: 0; padding: 28px; background: #0d1117; color: #e6edf3;
|
|
10
|
+
font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
11
|
+
nav { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 22px; font-size: .88rem; }
|
|
12
|
+
nav a { color: #60a5fa; text-decoration: none; }
|
|
13
|
+
.muted { color: #9aa7b4; font-size: .9rem; }
|
|
14
|
+
table { width: 100%; border-collapse: collapse; font-size: .88rem;
|
|
15
|
+
background: #161b22; border: 1px solid #2d3644; border-radius: 10px; }
|
|
16
|
+
th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #2d3644; }
|
|
17
|
+
th { color: #9aa7b4; font-size: .75rem; text-transform: uppercase; }
|
|
18
|
+
tr:last-child td { border-bottom: none; }
|
|
19
|
+
.ok { color: #4ade80; } .fail { color: #f87171; } .warn { color: #fcd34d; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<nav>
|
|
24
|
+
<a href="/_ctxoptimizer/">Overview</a><a href="/_ctxoptimizer/live">Live</a>
|
|
25
|
+
<a href="/_ctxoptimizer/sessions">Sessions</a><a href="/_ctxoptimizer/requests">Requests</a>
|
|
26
|
+
<a href="/_ctxoptimizer/waste">Waste</a><a href="/_ctxoptimizer/transforms">Transforms</a>
|
|
27
|
+
<a href="/_ctxoptimizer/pricing">Pricing</a><a href="/_ctxoptimizer/settings">Settings</a>
|
|
28
|
+
<a href="/_ctxoptimizer/doctor">Doctor</a>
|
|
29
|
+
</nav>
|
|
30
|
+
<h1>{{ title }}</h1>
|
|
31
|
+
<p class="muted">Same checks as <code>ctxoptimizer doctor</code> (offline mode).</p>
|
|
32
|
+
<table>
|
|
33
|
+
<thead><tr><th>Status</th><th>Check</th><th>Detail</th></tr></thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
{% for check in checks %}
|
|
36
|
+
<tr>
|
|
37
|
+
<td class="{{ 'ok' if check.ok and not check.warning else ('warn' if check.warning else 'fail') }}">
|
|
38
|
+
{{ "ok" if check.ok and not check.warning else ("warn" if check.warning else "fail") }}
|
|
39
|
+
</td>
|
|
40
|
+
<td>{{ check.name }}</td><td>{{ check.detail }}</td>
|
|
41
|
+
</tr>
|
|
42
|
+
{% endfor %}
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ctxoptimizer — Live</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body { margin: 0; padding: 28px; background: #0d1117; color: #e6edf3;
|
|
10
|
+
font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
11
|
+
nav { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 22px; font-size: .88rem; }
|
|
12
|
+
nav a, a { color: #60a5fa; text-decoration: none; }
|
|
13
|
+
h2 { font-size: 1rem; color: #93c5fd; margin-top: 26px; }
|
|
14
|
+
.muted { color: #9aa7b4; font-size: .9rem; }
|
|
15
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
16
|
+
gap: 12px; margin-top: 16px; }
|
|
17
|
+
.card { background: #161b22; border: 1px solid #2d3644; border-radius: 10px;
|
|
18
|
+
padding: 14px 16px; }
|
|
19
|
+
.label { color: #9aa7b4; font-size: .75rem; text-transform: uppercase; }
|
|
20
|
+
.value { font-size: 1.3rem; margin-top: 4px; font-family: Consolas, monospace; }
|
|
21
|
+
pre { background: #161b22; border: 1px solid #2d3644; border-radius: 10px;
|
|
22
|
+
padding: 16px; overflow: auto; font-size: .82rem; color: #a5d6ff; }
|
|
23
|
+
table { width: 100%; border-collapse: collapse; font-size: .84rem;
|
|
24
|
+
background: #161b22; border: 1px solid #2d3644; border-radius: 10px; }
|
|
25
|
+
th, td { padding: 7px 9px; text-align: left; border-bottom: 1px solid #2d3644; }
|
|
26
|
+
th { color: #9aa7b4; font-size: .72rem; text-transform: uppercase; }
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<nav>
|
|
31
|
+
<a href="/_ctxoptimizer/">Overview</a><a href="/_ctxoptimizer/live">Live</a>
|
|
32
|
+
<a href="/_ctxoptimizer/sessions">Sessions</a><a href="/_ctxoptimizer/requests">Requests</a>
|
|
33
|
+
<a href="/_ctxoptimizer/waste">Waste</a><a href="/_ctxoptimizer/transforms">Transforms</a>
|
|
34
|
+
<a href="/_ctxoptimizer/pricing">Pricing</a><a href="/_ctxoptimizer/settings">Settings</a>
|
|
35
|
+
<a href="/_ctxoptimizer/doctor">Doctor</a>
|
|
36
|
+
</nav>
|
|
37
|
+
<h1>Live</h1>
|
|
38
|
+
<p class="muted">v{{ version }} · mode {{ mode }} · SSE updates every 2s</p>
|
|
39
|
+
<div class="cards">
|
|
40
|
+
<div class="card"><div class="label">Requests</div><div class="value" id="requests">{{ counters["requests"] }}</div></div>
|
|
41
|
+
<div class="card"><div class="label">Active sessions</div><div class="value" id="active">—</div></div>
|
|
42
|
+
<div class="card"><div class="label">Burn $/min</div><div class="value" id="burn">—</div></div>
|
|
43
|
+
<div class="card"><div class="label">Upstream errors</div><div class="value" id="errors">{{ counters["upstream_errors"] }}</div></div>
|
|
44
|
+
<div class="card"><div class="label">Dropped telemetry</div><div class="value" id="dropped">{{ counters["dropped_telemetry"] }}</div></div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<h2>Last request</h2>
|
|
48
|
+
<pre id="last">{{ last | tojson(indent=2) }}</pre>
|
|
49
|
+
|
|
50
|
+
<h2>Recent requests</h2>
|
|
51
|
+
<table>
|
|
52
|
+
<thead><tr><th>Time</th><th>Model</th><th>Status</th><th>Latency</th><th>Cache read</th><th>Cost</th><th>Fallback</th></tr></thead>
|
|
53
|
+
<tbody id="recent">
|
|
54
|
+
<tr><td colspan="7" class="muted">Waiting for data…</td></tr>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
|
|
58
|
+
<h2>Quarantined</h2>
|
|
59
|
+
<pre>{{ quarantined | tojson(indent=2) }}</pre>
|
|
60
|
+
|
|
61
|
+
<script>
|
|
62
|
+
const source = new EventSource("/_ctxoptimizer/api/live");
|
|
63
|
+
function esc(value) {
|
|
64
|
+
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
65
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
66
|
+
}[char]));
|
|
67
|
+
}
|
|
68
|
+
source.onmessage = (event) => {
|
|
69
|
+
const data = JSON.parse(event.data);
|
|
70
|
+
document.getElementById("requests").textContent = data.counters.requests;
|
|
71
|
+
document.getElementById("errors").textContent = data.counters.upstream_errors;
|
|
72
|
+
document.getElementById("dropped").textContent = data.counters.dropped_telemetry;
|
|
73
|
+
document.getElementById("active").textContent = data.active_sessions;
|
|
74
|
+
document.getElementById("burn").textContent = "$" + data.burn_per_min_usd.toFixed(6);
|
|
75
|
+
document.getElementById("last").textContent = JSON.stringify(data.last_request, null, 2);
|
|
76
|
+
const recent = data.recent.map((row) => {
|
|
77
|
+
const total = row.fresh_input || 0;
|
|
78
|
+
const read = row.cache_read || 0;
|
|
79
|
+
const ratio = total + read ? (100 * read / (total + read)).toFixed(1) + "%" : "—";
|
|
80
|
+
return `<tr><td>${new Date((row.ts || 0) * 1000).toLocaleTimeString()}</td>
|
|
81
|
+
<td>${esc(row.model || "—")}</td><td>${esc(row.status_code || "—")}</td>
|
|
82
|
+
<td>${row.total_ms ? row.total_ms.toFixed(1) + " ms" : "—"}</td>
|
|
83
|
+
<td>${ratio}</td><td>$${(row.cost_actual_usd || 0).toFixed(6)}</td>
|
|
84
|
+
<td>${row.fallback_used ? "yes" : "no"}</td></tr>`;
|
|
85
|
+
}).join("");
|
|
86
|
+
document.getElementById("recent").innerHTML =
|
|
87
|
+
recent || '<tr><td colspan="7" class="muted">No requests yet.</td></tr>';
|
|
88
|
+
};
|
|
89
|
+
</script>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ctxoptimizer</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; padding: 28px; background: #0d1117; color: #e6edf3;
|
|
11
|
+
font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
12
|
+
h1 { margin: 0 0 4px; font-size: 1.6rem; }
|
|
13
|
+
h2 { margin: 28px 0 10px; font-size: 1.1rem; color: #93c5fd; }
|
|
14
|
+
.muted { color: #9aa7b4; font-size: .9rem; }
|
|
15
|
+
nav { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 22px; font-size: .88rem; }
|
|
16
|
+
nav a, a { color: #60a5fa; text-decoration: none; }
|
|
17
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
18
|
+
gap: 12px; margin-top: 18px; }
|
|
19
|
+
.card { background: #161b22; border: 1px solid #2d3644; border-radius: 10px;
|
|
20
|
+
padding: 14px 16px; }
|
|
21
|
+
.label { color: #9aa7b4; font-size: .78rem; text-transform: uppercase;
|
|
22
|
+
letter-spacing: .5px; }
|
|
23
|
+
.value { font-size: 1.4rem; margin-top: 4px; font-family: Consolas, monospace; }
|
|
24
|
+
table { width: 100%; border-collapse: collapse; font-size: .88rem;
|
|
25
|
+
background: #161b22; border: 1px solid #2d3644; border-radius: 10px; }
|
|
26
|
+
th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #2d3644; }
|
|
27
|
+
th { color: #9aa7b4; font-size: .75rem; text-transform: uppercase; }
|
|
28
|
+
tr:last-child td { border-bottom: none; }
|
|
29
|
+
.est { color: #fcd34d; font-weight: 600; }
|
|
30
|
+
code { color: #a5d6ff; }
|
|
31
|
+
.bar { height: 10px; background: #1f6feb; border-radius: 4px; min-width: 2px; }
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<nav>
|
|
36
|
+
<a href="/_ctxoptimizer/">Overview</a>
|
|
37
|
+
<a href="/_ctxoptimizer/live">Live</a>
|
|
38
|
+
<a href="/_ctxoptimizer/sessions">Sessions</a>
|
|
39
|
+
<a href="/_ctxoptimizer/requests">Requests</a>
|
|
40
|
+
<a href="/_ctxoptimizer/waste">Waste</a>
|
|
41
|
+
<a href="/_ctxoptimizer/transforms">Transforms</a>
|
|
42
|
+
<a href="/_ctxoptimizer/pricing">Pricing</a>
|
|
43
|
+
<a href="/_ctxoptimizer/settings">Settings</a>
|
|
44
|
+
<a href="/_ctxoptimizer/doctor">Doctor</a>
|
|
45
|
+
</nav>
|
|
46
|
+
<h1>ctxoptimizer</h1>
|
|
47
|
+
<p class="muted">v{{ version }} · mode {{ mode }} · local only</p>
|
|
48
|
+
{% if privacy %}
|
|
49
|
+
<p class="muted">Privacy: payload capture is enabled ({{ capture }}); captured text stays local.
|
|
50
|
+
Use Settings or <code>ctxoptimizer purge --all</code> to manage it.</p>
|
|
51
|
+
{% endif %}
|
|
52
|
+
<p class="muted">
|
|
53
|
+
Period:
|
|
54
|
+
<a href="?period=today">today</a> ·
|
|
55
|
+
<a href="?period=7d">7d</a> ·
|
|
56
|
+
<a href="?period=30d">30d</a> ·
|
|
57
|
+
<a href="?period=all">all</a>
|
|
58
|
+
(current: {{ period }})
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
<div class="cards">
|
|
62
|
+
<div class="card"><div class="label">Requests</div><div class="value">{{ summary["requests"] }}</div></div>
|
|
63
|
+
<div class="card"><div class="label">Actual spend</div><div class="value">${{ "%.6f"|format(summary["cost_usd"]) }}</div></div>
|
|
64
|
+
<div class="card"><div class="label">Baseline <span class="est">sim.</span></div><div class="value">${{ "%.6f"|format(summary["baseline_usd"]) }}</div></div>
|
|
65
|
+
<div class="card"><div class="label">Savings <span class="est">sim.</span></div><div class="value">${{ "%.6f"|format([summary["baseline_usd"] - summary["cost_usd"], 0]|max) }}</div></div>
|
|
66
|
+
<div class="card"><div class="label">Waste <span class="est">est.</span></div><div class="value">${{ "%.6f"|format(waste["cost_usd"]) }}</div></div>
|
|
67
|
+
<div class="card"><div class="label">Fresh input</div><div class="value">{{ summary["fresh_input"] }}</div></div>
|
|
68
|
+
<div class="card"><div class="label">Cache read</div><div class="value">{{ summary["cache_read"] }}</div></div>
|
|
69
|
+
<div class="card"><div class="label">Cache hit ratio</div><div class="value">{% set total = summary["fresh_input"] + summary["cache_read"] %}{% if total %}{{ "%.1f%%"|format(100 * summary["cache_read"] / total) }}{% else %}—{% endif %}</div></div>
|
|
70
|
+
<div class="card"><div class="label">Tokens removed <span class="est">est.</span></div><div class="value">{{ summary["tokens_removed"] }}</div></div>
|
|
71
|
+
<div class="card"><div class="label">Output</div><div class="value">{{ summary["output_tokens"] }}</div></div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{% if points %}
|
|
75
|
+
<h2>Spend over time <span class="est">sim.</span></h2>
|
|
76
|
+
{% if spend_line %}
|
|
77
|
+
<svg viewBox="0 0 600 80" width="100%" height="100" preserveAspectRatio="none"
|
|
78
|
+
style="background:#161b22;border:1px solid #2d3644;border-radius:10px;">
|
|
79
|
+
<polyline fill="none" stroke="#60a5fa" stroke-width="2" points="{{ spend_line }}"/>
|
|
80
|
+
</svg>
|
|
81
|
+
{% endif %}
|
|
82
|
+
<table>
|
|
83
|
+
<thead><tr><th>Bucket</th><th>Cost (USD)</th><th></th></tr></thead>
|
|
84
|
+
<tbody>
|
|
85
|
+
{% for point in points %}
|
|
86
|
+
<tr><td>{{ point["bucket"] }}</td><td>${{ "%.6f"|format(point["value"] or 0) }}</td>
|
|
87
|
+
<td><div class="bar" style="width: {{ ((point["value"] or 0) * 400)|round(0) }}px"></div></td></tr>
|
|
88
|
+
{% endfor %}
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
{% endif %}
|
|
92
|
+
|
|
93
|
+
{% if waste_points %}
|
|
94
|
+
<h2>Waste over time <span class="est">est.</span></h2>
|
|
95
|
+
{% if waste_line %}
|
|
96
|
+
<svg viewBox="0 0 600 80" width="100%" height="100" preserveAspectRatio="none"
|
|
97
|
+
style="background:#161b22;border:1px solid #2d3644;border-radius:10px;">
|
|
98
|
+
<polyline fill="none" stroke="#fcd34d" stroke-width="2" points="{{ waste_line }}"/>
|
|
99
|
+
</svg>
|
|
100
|
+
{% endif %}
|
|
101
|
+
<table>
|
|
102
|
+
<thead><tr><th>Bucket</th><th>Findings</th><th>Est. tokens</th><th>Est. USD</th></tr></thead>
|
|
103
|
+
<tbody>
|
|
104
|
+
{% for point in waste_points %}
|
|
105
|
+
<tr><td>{{ point["bucket"] }}</td><td>{{ point["findings"] }}</td>
|
|
106
|
+
<td>{{ point["tokens"] }}</td><td>${{ "%.6f"|format(point["cost_usd"] or 0) }}</td></tr>
|
|
107
|
+
{% endfor %}
|
|
108
|
+
</tbody>
|
|
109
|
+
</table>
|
|
110
|
+
{% endif %}
|
|
111
|
+
|
|
112
|
+
<h2>Recent requests</h2>
|
|
113
|
+
<table>
|
|
114
|
+
<thead>
|
|
115
|
+
<tr><th>Time</th><th>Client</th><th>Provider</th><th>Model</th>
|
|
116
|
+
<th>Fresh</th><th>Cache read</th><th>Output</th><th>Cost</th><th>Outcome</th></tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
{% for row in rows %}
|
|
120
|
+
<tr>
|
|
121
|
+
<td><a href="/_ctxoptimizer/requests/{{ row["request_id"] }}">{{ row["ts"]|round(0) }}</a></td>
|
|
122
|
+
<td>{{ row["client"] or "—" }}</td>
|
|
123
|
+
<td>{{ row["provider"] }}</td>
|
|
124
|
+
<td>{{ row["model"] or "—" }}</td>
|
|
125
|
+
<td>{{ row["fresh_input"] if row["fresh_input"] is not none else "—" }}</td>
|
|
126
|
+
<td>{{ row["cache_read"] if row["cache_read"] is not none else "—" }}</td>
|
|
127
|
+
<td>{{ row["output_tokens"] if row["output_tokens"] is not none else "—" }}</td>
|
|
128
|
+
<td>${{ "%.6f"|format(row["cost_actual_usd"] or 0) }}</td>
|
|
129
|
+
<td>{{ row["outcome"] or "—" }}</td>
|
|
130
|
+
</tr>
|
|
131
|
+
{% else %}
|
|
132
|
+
<tr><td colspan="9" class="muted">No requests recorded yet.</td></tr>
|
|
133
|
+
{% endfor %}
|
|
134
|
+
</tbody>
|
|
135
|
+
</table>
|
|
136
|
+
|
|
137
|
+
<h2>Per project</h2>
|
|
138
|
+
<table>
|
|
139
|
+
<thead><tr><th>Project</th><th>Requests</th><th>Cost</th><th>Fresh</th><th>Cache read</th><th>Output</th></tr></thead>
|
|
140
|
+
<tbody>
|
|
141
|
+
{% for row in projects %}
|
|
142
|
+
<tr><td>{{ row["name"] or "—" }}</td><td>{{ row["requests"] }}</td>
|
|
143
|
+
<td>${{ "%.6f"|format(row["cost_usd"]) }}</td><td>{{ row["fresh_input"] }}</td>
|
|
144
|
+
<td>{{ row["cache_read"] }}</td><td>{{ row["output_tokens"] }}</td></tr>
|
|
145
|
+
{% else %}
|
|
146
|
+
<tr><td colspan="6" class="muted">No project data yet.</td></tr>
|
|
147
|
+
{% endfor %}
|
|
148
|
+
</tbody>
|
|
149
|
+
</table>
|
|
150
|
+
|
|
151
|
+
<h2>Per model</h2>
|
|
152
|
+
<table>
|
|
153
|
+
<thead><tr><th>Model</th><th>Requests</th><th>Cost</th><th>Fresh</th><th>Cache read</th><th>Output</th></tr></thead>
|
|
154
|
+
<tbody>
|
|
155
|
+
{% for row in models %}
|
|
156
|
+
<tr><td>{{ row["name"] or "—" }}</td><td>{{ row["requests"] }}</td>
|
|
157
|
+
<td>${{ "%.6f"|format(row["cost_usd"]) }}</td><td>{{ row["fresh_input"] }}</td>
|
|
158
|
+
<td>{{ row["cache_read"] }}</td><td>{{ row["output_tokens"] }}</td></tr>
|
|
159
|
+
{% else %}
|
|
160
|
+
<tr><td colspan="6" class="muted">No model data yet.</td></tr>
|
|
161
|
+
{% endfor %}
|
|
162
|
+
</tbody>
|
|
163
|
+
</table>
|
|
164
|
+
|
|
165
|
+
<p class="muted" style="margin-top:18px;">
|
|
166
|
+
Metadata only; prompts are not shown. API:
|
|
167
|
+
<a href="/_ctxoptimizer/api/summary">summary</a> ·
|
|
168
|
+
<a href="/_ctxoptimizer/api/requests">requests</a> ·
|
|
169
|
+
<a href="/_ctxoptimizer/api/timeseries">timeseries</a>.
|
|
170
|
+
</p>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ctxoptimizer — {{ title }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body { margin: 0; padding: 28px; background: #0d1117; color: #e6edf3;
|
|
10
|
+
font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
11
|
+
nav { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 22px; font-size: .88rem; }
|
|
12
|
+
nav a, a { color: #60a5fa; text-decoration: none; }
|
|
13
|
+
h2 { margin: 26px 0 10px; font-size: 1.05rem; color: #93c5fd; }
|
|
14
|
+
.muted { color: #9aa7b4; font-size: .9rem; }
|
|
15
|
+
table { width: 100%; border-collapse: collapse; font-size: .84rem;
|
|
16
|
+
background: #161b22; border: 1px solid #2d3644; border-radius: 10px; }
|
|
17
|
+
th, td { padding: 7px 9px; text-align: left; border-bottom: 1px solid #2d3644; }
|
|
18
|
+
th { color: #9aa7b4; font-size: .72rem; text-transform: uppercase; }
|
|
19
|
+
tr:last-child td { border-bottom: none; }
|
|
20
|
+
textarea { width: 100%; height: 280px; background: #0d1117; color: #a5d6ff;
|
|
21
|
+
border: 1px solid #2d3644; border-radius: 8px; padding: 10px;
|
|
22
|
+
font-family: Consolas, monospace; font-size: .8rem; }
|
|
23
|
+
button { background: #1f6feb; color: white; border: 0; border-radius: 6px;
|
|
24
|
+
padding: 7px 12px; cursor: pointer; }
|
|
25
|
+
#status { color: #a5d6ff; margin-top: 8px; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<nav>
|
|
30
|
+
<a href="/_ctxoptimizer/">Overview</a><a href="/_ctxoptimizer/live">Live</a>
|
|
31
|
+
<a href="/_ctxoptimizer/sessions">Sessions</a><a href="/_ctxoptimizer/requests">Requests</a>
|
|
32
|
+
<a href="/_ctxoptimizer/waste">Waste</a><a href="/_ctxoptimizer/transforms">Transforms</a>
|
|
33
|
+
<a href="/_ctxoptimizer/pricing">Pricing</a><a href="/_ctxoptimizer/settings">Settings</a>
|
|
34
|
+
<a href="/_ctxoptimizer/doctor">Doctor</a>
|
|
35
|
+
</nav>
|
|
36
|
+
<h1>{{ title }}</h1>
|
|
37
|
+
<p class="muted">Rates are USD per million tokens. Unknown models stay unpriced.</p>
|
|
38
|
+
|
|
39
|
+
<h2>Rates</h2>
|
|
40
|
+
<table id="rate-table">
|
|
41
|
+
<thead><tr><th>Provider</th><th>Model glob</th><th>Input</th><th>Output</th>
|
|
42
|
+
<th>Cache read</th><th>Verified</th><th>Source</th></tr></thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
{% for model in models %}
|
|
45
|
+
<tr class="model-row" data-extra='{{ model | tojson }}'>
|
|
46
|
+
<td><input data-field="provider" value="{{ model['provider'] }}"></td>
|
|
47
|
+
<td><input data-field="match" value="{{ model['match'] }}"></td>
|
|
48
|
+
<td><input data-field="input" type="number" step="0.001" value="{{ model['input'] }}"></td>
|
|
49
|
+
<td><input data-field="output" type="number" step="0.001" value="{{ model['output'] }}"></td>
|
|
50
|
+
<td><input data-field="cache_read" type="number" step="0.001" value="{{ model['cache_read'] }}"></td>
|
|
51
|
+
<td><input data-field="verified" type="checkbox" {% if model["verified"] %}checked{% endif %}></td>
|
|
52
|
+
<td>{{ model["source"] }}</td>
|
|
53
|
+
</tr>
|
|
54
|
+
{% endfor %}
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
<button id="save-rows">Save table</button>
|
|
58
|
+
|
|
59
|
+
<h2>Unpriced models <span class="muted">est.</span></h2>
|
|
60
|
+
<table>
|
|
61
|
+
<thead><tr><th>Model</th><th>Provider</th><th>Requests</th></tr></thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
{% for row in unpriced %}
|
|
64
|
+
<tr><td>{{ row["model"] }}</td><td>{{ row["provider"] }}</td><td>{{ row["requests"] }}</td></tr>
|
|
65
|
+
{% else %}
|
|
66
|
+
<tr><td colspan="3" class="muted">No unpriced models recorded.</td></tr>
|
|
67
|
+
{% endfor %}
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
|
|
71
|
+
<h2>Advanced JSON</h2>
|
|
72
|
+
<p class="muted">Edit the full override JSON and save. The file is validated before use.</p>
|
|
73
|
+
<textarea id="pricing">{{ payload_json }}</textarea>
|
|
74
|
+
<button id="save">Save JSON</button>
|
|
75
|
+
<div id="status"></div>
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
function csrf() {
|
|
79
|
+
const match = document.cookie.match(/(?:^|; )ctxoptimizer_csrf=([^;]+)/);
|
|
80
|
+
return match ? decodeURIComponent(match[1]) : "";
|
|
81
|
+
}
|
|
82
|
+
async function savePricing(payload) {
|
|
83
|
+
const response = await fetch("/_ctxoptimizer/api/pricing", {
|
|
84
|
+
method: "PUT",
|
|
85
|
+
headers: {"content-type": "application/json", "x-ctxoptimizer-csrf": csrf()},
|
|
86
|
+
body: JSON.stringify(payload),
|
|
87
|
+
});
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
document.getElementById("status").textContent =
|
|
90
|
+
response.ok ? "Saved." : ("Error: " + (body.error || response.status));
|
|
91
|
+
}
|
|
92
|
+
document.getElementById("save").addEventListener("click", () => {
|
|
93
|
+
let payload;
|
|
94
|
+
try { payload = JSON.parse(document.getElementById("pricing").value); }
|
|
95
|
+
catch (error) {
|
|
96
|
+
document.getElementById("status").textContent = "Invalid JSON: " + error;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
savePricing(payload);
|
|
100
|
+
});
|
|
101
|
+
document.getElementById("save-rows").addEventListener("click", () => {
|
|
102
|
+
const models = [...document.querySelectorAll("tr.model-row")].map((row) => {
|
|
103
|
+
const extra = JSON.parse(row.dataset.extra || "{}");
|
|
104
|
+
for (const input of row.querySelectorAll("input[data-field]")) {
|
|
105
|
+
if (input.type === "checkbox") extra[input.dataset.field] = input.checked;
|
|
106
|
+
else if (input.type === "number") extra[input.dataset.field] = Number(input.value);
|
|
107
|
+
else extra[input.dataset.field] = input.value;
|
|
108
|
+
}
|
|
109
|
+
return extra;
|
|
110
|
+
});
|
|
111
|
+
savePricing({models});
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ctxoptimizer — {{ title }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body { margin: 0; padding: 28px; background: #0d1117; color: #e6edf3;
|
|
10
|
+
font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
11
|
+
nav { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 22px; font-size: .88rem; }
|
|
12
|
+
nav a, a { color: #60a5fa; text-decoration: none; }
|
|
13
|
+
.muted { color: #9aa7b4; font-size: .9rem; }
|
|
14
|
+
h2 { margin: 26px 0 10px; font-size: 1.05rem; color: #93c5fd; }
|
|
15
|
+
table { width: 100%; border-collapse: collapse; font-size: .86rem;
|
|
16
|
+
background: #161b22; border: 1px solid #2d3644; border-radius: 10px; }
|
|
17
|
+
th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #2d3644; }
|
|
18
|
+
th { color: #9aa7b4; font-size: .72rem; text-transform: uppercase; }
|
|
19
|
+
tr:last-child td { border-bottom: none; }
|
|
20
|
+
pre { background: #161b22; border: 1px solid #2d3644; border-radius: 10px;
|
|
21
|
+
padding: 14px; overflow: auto; font-size: .8rem; color: #a5d6ff;
|
|
22
|
+
white-space: pre-wrap; }
|
|
23
|
+
.diff { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
24
|
+
.label { color: #9aa7b4; font-size: .72rem; text-transform: uppercase; }
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<nav>
|
|
29
|
+
<a href="/_ctxoptimizer/">Overview</a><a href="/_ctxoptimizer/live">Live</a>
|
|
30
|
+
<a href="/_ctxoptimizer/sessions">Sessions</a><a href="/_ctxoptimizer/requests">Requests</a>
|
|
31
|
+
<a href="/_ctxoptimizer/waste">Waste</a><a href="/_ctxoptimizer/transforms">Transforms</a>
|
|
32
|
+
<a href="/_ctxoptimizer/pricing">Pricing</a><a href="/_ctxoptimizer/settings">Settings</a>
|
|
33
|
+
<a href="/_ctxoptimizer/doctor">Doctor</a>
|
|
34
|
+
</nav>
|
|
35
|
+
<h1>{{ title }}</h1>
|
|
36
|
+
{% if found %}
|
|
37
|
+
<p class="muted">Metadata only; prompts are not shown unless capture is enabled.</p>
|
|
38
|
+
|
|
39
|
+
<h2>Summary</h2>
|
|
40
|
+
<table>
|
|
41
|
+
<tbody>
|
|
42
|
+
{% for key in ("provider", "endpoint", "upstream", "model", "client", "project", "mode",
|
|
43
|
+
"status_code", "outcome", "mutated", "fallback_used", "usage_source",
|
|
44
|
+
"price_source", "cost_actual_usd", "error") %}
|
|
45
|
+
<tr><th>{{ key }}</th><td>{{ row[key] if row[key] is not none else "—" }}</td></tr>
|
|
46
|
+
{% endfor %}
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
|
|
50
|
+
<h2>Timing</h2>
|
|
51
|
+
<table>
|
|
52
|
+
<thead><tr><th>Overhead</th><th>TTFB</th><th>Total</th><th>Original bytes</th><th>Final bytes</th>
|
|
53
|
+
<th>Original tokens</th><th>Final tokens</th></tr></thead>
|
|
54
|
+
<tbody><tr>
|
|
55
|
+
<td>{{ timing["proxy_overhead_ms"] or "—" }} ms</td>
|
|
56
|
+
<td>{{ timing["ttfb_ms"] or "—" }} ms</td>
|
|
57
|
+
<td>{{ timing["total_ms"] or "—" }} ms</td>
|
|
58
|
+
<td>{{ row["orig_bytes"] if row["orig_bytes"] is not none else "—" }}</td>
|
|
59
|
+
<td>{{ row["final_bytes"] if row["final_bytes"] is not none else "—" }}</td>
|
|
60
|
+
<td>{{ row["orig_est_tokens"] if row["orig_est_tokens"] is not none else "—" }}</td>
|
|
61
|
+
<td>{{ row["final_est_tokens"] if row["final_est_tokens"] is not none else "—" }}</td>
|
|
62
|
+
</tr></tbody>
|
|
63
|
+
</table>
|
|
64
|
+
|
|
65
|
+
<h2>Request headers (redacted)</h2>
|
|
66
|
+
<table>
|
|
67
|
+
<tbody>
|
|
68
|
+
{% for name, value in headers.items() %}
|
|
69
|
+
<tr><th>{{ name }}</th><td>{{ value }}</td></tr>
|
|
70
|
+
{% else %}
|
|
71
|
+
<tr><td class="muted">No headers captured.</td></tr>
|
|
72
|
+
{% endfor %}
|
|
73
|
+
</tbody>
|
|
74
|
+
</table>
|
|
75
|
+
|
|
76
|
+
<h2>Transform reports</h2>
|
|
77
|
+
<table>
|
|
78
|
+
<thead><tr><th>Transform</th><th>Changed</th><th>Items</th><th>Tokens saved</th>
|
|
79
|
+
<th>Duration</th><th>Discarded</th></tr></thead>
|
|
80
|
+
<tbody>
|
|
81
|
+
{% for report in reports %}
|
|
82
|
+
<tr><td>{{ report["transform_id"] }}</td><td>{{ report["changed"] }}</td>
|
|
83
|
+
<td>{{ report["items_touched"] }}</td><td>{{ report["est_tokens_saved"] }}</td>
|
|
84
|
+
<td>{{ "%.3f"|format(report["duration_ms"] or 0) }} ms</td>
|
|
85
|
+
<td>{{ report["discarded_reason"] or "—" }}</td></tr>
|
|
86
|
+
{% else %}
|
|
87
|
+
<tr><td colspan="6" class="muted">No reports persisted for this request.</td></tr>
|
|
88
|
+
{% endfor %}
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
|
|
92
|
+
<h2>Diff viewer (captured changed blocks)</h2>
|
|
93
|
+
{% if diff %}
|
|
94
|
+
{% for block in diff %}
|
|
95
|
+
<p class="label">{{ block["location"] }}</p>
|
|
96
|
+
<div class="diff">
|
|
97
|
+
<div><div class="label">before</div><pre>{{ block["before"] }}</pre></div>
|
|
98
|
+
<div><div class="label">after</div><pre>{{ block["after"] }}</pre></div>
|
|
99
|
+
</div>
|
|
100
|
+
{% endfor %}
|
|
101
|
+
{% else %}
|
|
102
|
+
<p class="muted">No captured diff. Set <code>capture_payloads = "changed_blocks"</code> or <code>"full"</code>.</p>
|
|
103
|
+
{% endif %}
|
|
104
|
+
|
|
105
|
+
<h2>Usage</h2>
|
|
106
|
+
<pre>{{ usage | tojson(indent=2) }}</pre>
|
|
107
|
+
{% else %}
|
|
108
|
+
<p class="muted">Request not found.</p>
|
|
109
|
+
{% endif %}
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ctxoptimizer — {{ title }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body { margin: 0; padding: 28px; background: #0d1117; color: #e6edf3;
|
|
10
|
+
font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
11
|
+
nav { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 22px; font-size: .88rem; }
|
|
12
|
+
nav a, a { color: #60a5fa; text-decoration: none; }
|
|
13
|
+
.muted { color: #9aa7b4; font-size: .9rem; }
|
|
14
|
+
table { width: 100%; border-collapse: collapse; font-size: .84rem;
|
|
15
|
+
background: #161b22; border: 1px solid #2d3644; border-radius: 10px; }
|
|
16
|
+
th, td { padding: 7px 9px; text-align: left; border-bottom: 1px solid #2d3644; }
|
|
17
|
+
th { color: #9aa7b4; font-size: .72rem; text-transform: uppercase; }
|
|
18
|
+
tr:last-child td { border-bottom: none; }
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<nav>
|
|
23
|
+
<a href="/_ctxoptimizer/">Overview</a><a href="/_ctxoptimizer/live">Live</a>
|
|
24
|
+
<a href="/_ctxoptimizer/sessions">Sessions</a><a href="/_ctxoptimizer/requests">Requests</a>
|
|
25
|
+
<a href="/_ctxoptimizer/waste">Waste</a><a href="/_ctxoptimizer/transforms">Transforms</a>
|
|
26
|
+
<a href="/_ctxoptimizer/pricing">Pricing</a><a href="/_ctxoptimizer/settings">Settings</a>
|
|
27
|
+
<a href="/_ctxoptimizer/doctor">Doctor</a>
|
|
28
|
+
</nav>
|
|
29
|
+
<h1>{{ title }}</h1>
|
|
30
|
+
<p class="muted">Turn-by-turn metadata. Cache-read ratio = cache read / (fresh + cache read).</p>
|
|
31
|
+
{% if ratio_line %}
|
|
32
|
+
<svg viewBox="0 0 600 80" width="100%" height="100" preserveAspectRatio="none"
|
|
33
|
+
style="background:#161b22;border:1px solid #2d3644;border-radius:10px;">
|
|
34
|
+
<polyline fill="none" stroke="#4ade80" stroke-width="2" points="{{ ratio_line }}"/>
|
|
35
|
+
</svg>
|
|
36
|
+
{% endif %}
|
|
37
|
+
<table>
|
|
38
|
+
<thead>
|
|
39
|
+
<tr><th>#</th><th>Time</th><th>Model</th><th>Orig tokens</th><th>Final tokens</th>
|
|
40
|
+
<th>Fresh</th><th>Read</th><th>Out</th><th>Cost</th><th>Cache ratio</th>
|
|
41
|
+
<th>Drift</th><th>Outcome</th></tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
{% for row in rows %}
|
|
45
|
+
{% set read = row["cache_read"] or 0 %}
|
|
46
|
+
{% set fresh = row["fresh_input"] or 0 %}
|
|
47
|
+
<tr>
|
|
48
|
+
<td>{{ loop.index }}</td>
|
|
49
|
+
<td><a href="/_ctxoptimizer/requests/{{ row["request_id"] }}">{{ row["ts"]|round(0) }}</a></td>
|
|
50
|
+
<td>{{ row["model"] or "—" }}</td>
|
|
51
|
+
<td>{{ row["orig_est_tokens"] if row["orig_est_tokens"] is not none else "—" }}</td>
|
|
52
|
+
<td>{{ row["final_est_tokens"] if row["final_est_tokens"] is not none else "—" }}</td>
|
|
53
|
+
<td>{{ fresh if row["fresh_input"] is not none else "—" }}</td>
|
|
54
|
+
<td>{{ read if row["cache_read"] is not none else "—" }}</td>
|
|
55
|
+
<td>{{ row["output_tokens"] if row["output_tokens"] is not none else "—" }}</td>
|
|
56
|
+
<td>${{ "%.6f"|format(row["cost_actual_usd"] or 0) }}</td>
|
|
57
|
+
<td>{% if read + fresh %}{{ "%.1f%%"|format(100 * read / (read + fresh)) }}{% else %}—{% endif %}</td>
|
|
58
|
+
<td>{{ row["drift"] or "—" }}</td>
|
|
59
|
+
<td>{{ row["outcome"] or "—" }}</td>
|
|
60
|
+
</tr>
|
|
61
|
+
{% else %}
|
|
62
|
+
<tr><td colspan="12" class="muted">No requests in this session.</td></tr>
|
|
63
|
+
{% endfor %}
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|