tokenmizer 0.2.4__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.
- tokenmizer/__init__.py +21 -0
- tokenmizer/agents/__init__.py +0 -0
- tokenmizer/analytics/__init__.py +0 -0
- tokenmizer/analytics/engine.py +188 -0
- tokenmizer/api/__init__.py +0 -0
- tokenmizer/api/app.py +958 -0
- tokenmizer/api/rate_limiter.py +110 -0
- tokenmizer/checkpoints/__init__.py +0 -0
- tokenmizer/checkpoints/manager.py +383 -0
- tokenmizer/cli.py +153 -0
- tokenmizer/compression/__init__.py +0 -0
- tokenmizer/compression/engine.py +669 -0
- tokenmizer/compression/output_trimmer.py +95 -0
- tokenmizer/compression/window.py +104 -0
- tokenmizer/config/__init__.py +0 -0
- tokenmizer/config/settings.py +170 -0
- tokenmizer/core/__init__.py +0 -0
- tokenmizer/core/dto.py +196 -0
- tokenmizer/core/errors.py +35 -0
- tokenmizer/core/tokenizer.py +96 -0
- tokenmizer/dashboard/__init__.py +0 -0
- tokenmizer/dashboard/page.py +267 -0
- tokenmizer/filters/__init__.py +0 -0
- tokenmizer/filters/file_intelligence.py +960 -0
- tokenmizer/graph_memory/__init__.py +0 -0
- tokenmizer/graph_memory/decision_tracker.py +225 -0
- tokenmizer/graph_memory/graph.py +1287 -0
- tokenmizer/graph_memory/helpers.py +121 -0
- tokenmizer/graph_memory/hybrid_extractor.py +703 -0
- tokenmizer/graph_memory/types.py +134 -0
- tokenmizer/graph_memory/validator.py +304 -0
- tokenmizer/graph_memory/visualization.py +228 -0
- tokenmizer/mcp/__init__.py +0 -0
- tokenmizer/mcp/server.py +368 -0
- tokenmizer/providers/__init__.py +0 -0
- tokenmizer/providers/providers.py +456 -0
- tokenmizer/security/__init__.py +0 -0
- tokenmizer/security/auth.py +95 -0
- tokenmizer/security/middleware.py +138 -0
- tokenmizer/security/redaction.py +126 -0
- tokenmizer/semantic_cache/__init__.py +0 -0
- tokenmizer/semantic_cache/cache.py +383 -0
- tokenmizer/state/__init__.py +0 -0
- tokenmizer/state/backend.py +137 -0
- tokenmizer/storage/__init__.py +56 -0
- tokenmizer-0.2.4.dist-info/METADATA +529 -0
- tokenmizer-0.2.4.dist-info/RECORD +50 -0
- tokenmizer-0.2.4.dist-info/WHEEL +4 -0
- tokenmizer-0.2.4.dist-info/entry_points.txt +2 -0
- tokenmizer-0.2.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Accurate token counting.
|
|
3
|
+
|
|
4
|
+
- OpenAI/compatible models: tiktoken (model-specific encoding or cl100k_base fallback)
|
|
5
|
+
- Anthropic/Claude models: tiktoken is the WRONG tokenizer β Claude uses a different
|
|
6
|
+
vocabulary and previously every Claude request was counted with an OpenAI encoder,
|
|
7
|
+
which is inaccurate (typically 5-20% off, worse on code-heavy content). This module
|
|
8
|
+
now routes Claude models through the Anthropic SDK's local tokenizer when available,
|
|
9
|
+
and only falls back to the tiktoken approximation if the SDK doesn't expose one.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
|
|
15
|
+
_FALLBACK_RATIO = 4 # chars per token β only used if tiktoken unavailable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@functools.lru_cache(maxsize=16)
|
|
19
|
+
def _get_encoding(model: str):
|
|
20
|
+
try:
|
|
21
|
+
import tiktoken
|
|
22
|
+
try:
|
|
23
|
+
return tiktoken.encoding_for_model(model)
|
|
24
|
+
except KeyError:
|
|
25
|
+
return tiktoken.get_encoding("cl100k_base")
|
|
26
|
+
except ImportError:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_claude_model(model: str) -> bool:
|
|
31
|
+
"""True if this is an Anthropic/Claude model β needs the Anthropic tokenizer,
|
|
32
|
+
not tiktoken."""
|
|
33
|
+
return "claude" in model.lower() or "anthropic" in model.lower()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _count_with_anthropic_sdk(text: str) -> int | None:
|
|
37
|
+
"""
|
|
38
|
+
Try the Anthropic SDK's local tokenizer.
|
|
39
|
+
Older SDK (<0.20): anthropic.count_tokens(text)
|
|
40
|
+
Some intermediate versions: anthropic.tokenizer.count_tokens(text)
|
|
41
|
+
Newer SDK versions removed the local tokenizer entirely (requires an API call) β
|
|
42
|
+
in that case this returns None and the caller falls back to a tiktoken estimate.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
import anthropic as _anthropic
|
|
46
|
+
if hasattr(_anthropic, "count_tokens"):
|
|
47
|
+
return int(_anthropic.count_tokens(text))
|
|
48
|
+
if hasattr(_anthropic, "tokenizer") and hasattr(_anthropic.tokenizer, "count_tokens"):
|
|
49
|
+
return int(_anthropic.tokenizer.count_tokens(text))
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def count_tokens(text: str, model: str = "gpt-4o") -> int:
|
|
56
|
+
"""
|
|
57
|
+
Accurate token count for the given model.
|
|
58
|
+
|
|
59
|
+
Claude/Anthropic models: tries the Anthropic SDK's local tokenizer first, falls
|
|
60
|
+
back to a cl100k_base (tiktoken) approximation if the SDK doesn't expose one β
|
|
61
|
+
this approximation carries a documented error margin, see module docstring.
|
|
62
|
+
All other models: tiktoken with the correct model-specific encoding.
|
|
63
|
+
Falls back to char/4 if tiktoken is not installed at all.
|
|
64
|
+
"""
|
|
65
|
+
if not text:
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
if is_claude_model(model):
|
|
69
|
+
sdk_count = _count_with_anthropic_sdk(text)
|
|
70
|
+
if sdk_count is not None:
|
|
71
|
+
return sdk_count
|
|
72
|
+
enc = _get_encoding("gpt-4o") # closest available approximation
|
|
73
|
+
if enc is not None:
|
|
74
|
+
return len(enc.encode(text, disallowed_special=()))
|
|
75
|
+
return max(1, len(text) // _FALLBACK_RATIO)
|
|
76
|
+
|
|
77
|
+
enc = _get_encoding(model)
|
|
78
|
+
if enc is None:
|
|
79
|
+
return max(1, len(text) // _FALLBACK_RATIO)
|
|
80
|
+
return len(enc.encode(text, disallowed_special=()))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def count_messages_tokens(messages: list[dict], model: str = "gpt-4o") -> int:
|
|
84
|
+
"""Count tokens across all messages including OpenAI/Anthropic role overhead."""
|
|
85
|
+
total = 0
|
|
86
|
+
for msg in messages:
|
|
87
|
+
total += 4 # per-message framing tokens
|
|
88
|
+
total += count_tokens(msg.get("content", ""), model)
|
|
89
|
+
total += count_tokens(msg.get("role", ""), model)
|
|
90
|
+
total += 2 # reply priming
|
|
91
|
+
return total
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def chars_to_tokens_estimate(chars: int) -> int:
|
|
95
|
+
"""Fast estimate when we only have char count (e.g. for size checks)."""
|
|
96
|
+
return max(1, chars // _FALLBACK_RATIO)
|
|
File without changes
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Dashboard HTML β served at GET /"""
|
|
2
|
+
|
|
3
|
+
DASHBOARD_HTML = """<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
+
<title>TokenMizer β Never Lose AI Context</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0f1117;
|
|
12
|
+
--surface: #1a1d27;
|
|
13
|
+
--border: #2a2d3e;
|
|
14
|
+
--accent: #7c6af7;
|
|
15
|
+
--accent2: #5ee7c8;
|
|
16
|
+
--text: #e8eaf6;
|
|
17
|
+
--muted: #8b8fa8;
|
|
18
|
+
--green: #4ade80;
|
|
19
|
+
--yellow: #fbbf24;
|
|
20
|
+
--red: #f87171;
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23
|
+
body { background: var(--bg); color: var(--text); font-family: 'Inter', system-ui, sans-serif; min-height: 100vh; }
|
|
24
|
+
|
|
25
|
+
header {
|
|
26
|
+
border-bottom: 1px solid var(--border);
|
|
27
|
+
padding: 1rem 2rem;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 1rem;
|
|
31
|
+
}
|
|
32
|
+
header .logo { font-size: 1.4rem; font-weight: 700; }
|
|
33
|
+
header .logo span { color: var(--accent); }
|
|
34
|
+
header .tagline { color: var(--muted); font-size: 0.85rem; margin-left: auto; }
|
|
35
|
+
header .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
|
36
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
37
|
+
|
|
38
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
39
|
+
|
|
40
|
+
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
|
41
|
+
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
|
42
|
+
|
|
43
|
+
.card {
|
|
44
|
+
background: var(--surface);
|
|
45
|
+
border: 1px solid var(--border);
|
|
46
|
+
border-radius: 12px;
|
|
47
|
+
padding: 1.5rem;
|
|
48
|
+
}
|
|
49
|
+
.card h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 0.75rem; }
|
|
50
|
+
.stat-value { font-size: 2.2rem; font-weight: 700; color: var(--text); }
|
|
51
|
+
.stat-sub { font-size: 0.8rem; color: var(--muted); margin-top: 0.25rem; }
|
|
52
|
+
.stat-value.green { color: var(--green); }
|
|
53
|
+
.stat-value.accent { color: var(--accent); }
|
|
54
|
+
.stat-value.accent2 { color: var(--accent2); }
|
|
55
|
+
|
|
56
|
+
.layer-row { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
|
|
57
|
+
.layer-row:last-child { border-bottom: none; }
|
|
58
|
+
.layer-name { font-size: 0.875rem; }
|
|
59
|
+
.layer-badge { font-size: 0.75rem; padding: 2px 10px; border-radius: 20px; font-weight: 600; }
|
|
60
|
+
.badge-active { background: #1a2e1a; color: var(--green); }
|
|
61
|
+
.badge-cache { background: #1a2040; color: var(--accent); }
|
|
62
|
+
.badge-graph { background: #1e1a40; color: #a78bfa; }
|
|
63
|
+
.badge-terse { background: #2a2010; color: var(--yellow); }
|
|
64
|
+
.badge-routing { background: #1a2a2a; color: var(--accent2); }
|
|
65
|
+
|
|
66
|
+
.session-card { margin-bottom: 0.75rem; padding: 1rem; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; }
|
|
67
|
+
.session-id { font-size: 0.8rem; color: var(--muted); font-family: monospace; }
|
|
68
|
+
.session-progress { width: 100%; height: 4px; background: var(--border); border-radius: 2px; margin: 0.5rem 0; }
|
|
69
|
+
.session-bar { height: 100%; border-radius: 2px; background: linear-gradient(90deg, var(--accent), var(--accent2)); }
|
|
70
|
+
|
|
71
|
+
.graph-node { display: inline-flex; align-items: center; gap: 0.4rem; padding: 4px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 500; margin: 3px; }
|
|
72
|
+
.node-task { background: #1a2e1a; color: var(--green); }
|
|
73
|
+
.node-decision { background: #1e1a40; color: #a78bfa; }
|
|
74
|
+
.node-file { background: #1a2040; color: var(--accent); }
|
|
75
|
+
.node-error { background: #2e1a1a; color: var(--red); }
|
|
76
|
+
.node-dependency { background: #2a2010; color: var(--yellow); }
|
|
77
|
+
.node-environment { background: #1a2a2a; color: var(--accent2); }
|
|
78
|
+
.node-goal { background: #2a1a2e; color: #e879f9; }
|
|
79
|
+
.node-endpoint { background: #101a2e; color: #60a5fa; }
|
|
80
|
+
|
|
81
|
+
.endpoint-box {
|
|
82
|
+
background: var(--bg);
|
|
83
|
+
border: 1px solid var(--border);
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
padding: 1rem 1.25rem;
|
|
86
|
+
font-family: monospace;
|
|
87
|
+
font-size: 0.82rem;
|
|
88
|
+
color: var(--muted);
|
|
89
|
+
line-height: 1.8;
|
|
90
|
+
}
|
|
91
|
+
.endpoint-box .method { color: var(--green); font-weight: 700; }
|
|
92
|
+
.endpoint-box .path { color: var(--text); }
|
|
93
|
+
.endpoint-box .desc { color: var(--muted); font-size: 0.75rem; }
|
|
94
|
+
|
|
95
|
+
.resume-block {
|
|
96
|
+
background: var(--bg);
|
|
97
|
+
border: 1px solid var(--accent);
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
padding: 1rem 1.25rem;
|
|
100
|
+
font-family: monospace;
|
|
101
|
+
font-size: 0.8rem;
|
|
102
|
+
line-height: 1.7;
|
|
103
|
+
color: var(--accent2);
|
|
104
|
+
white-space: pre-wrap;
|
|
105
|
+
word-break: break-word;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
footer {
|
|
109
|
+
border-top: 1px solid var(--border);
|
|
110
|
+
padding: 1.5rem 2rem;
|
|
111
|
+
text-align: center;
|
|
112
|
+
color: var(--muted);
|
|
113
|
+
font-size: 0.8rem;
|
|
114
|
+
margin-top: 2rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@media (max-width: 768px) {
|
|
118
|
+
.grid-3, .grid-2 { grid-template-columns: 1fr; }
|
|
119
|
+
.container { padding: 1rem; }
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
|
|
125
|
+
<header>
|
|
126
|
+
<div class="status-dot"></div>
|
|
127
|
+
<div class="logo">π§ Token<span>Mizer</span></div>
|
|
128
|
+
<div class="tagline">Never lose your AI context again.</div>
|
|
129
|
+
</header>
|
|
130
|
+
|
|
131
|
+
<div class="container">
|
|
132
|
+
|
|
133
|
+
<!-- Stats row -->
|
|
134
|
+
<div class="grid-3" id="stats-row">
|
|
135
|
+
<div class="card">
|
|
136
|
+
<h3>Requests Today</h3>
|
|
137
|
+
<div class="stat-value accent" id="req-today">β</div>
|
|
138
|
+
<div class="stat-sub">All sessions combined</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="card">
|
|
141
|
+
<h3>Tokens Saved Today</h3>
|
|
142
|
+
<div class="stat-value green" id="tokens-saved">β</div>
|
|
143
|
+
<div class="stat-sub" id="savings-pct">Loadingβ¦</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="card">
|
|
146
|
+
<h3>Cache Hit Rate</h3>
|
|
147
|
+
<div class="stat-value accent2" id="cache-hit-rate">β</div>
|
|
148
|
+
<div class="stat-sub" id="cache-entries">Loadingβ¦</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="grid-2">
|
|
153
|
+
|
|
154
|
+
<!-- Pipeline layers -->
|
|
155
|
+
<div class="card">
|
|
156
|
+
<h3>Pipeline Layers</h3>
|
|
157
|
+
<div class="layer-row">
|
|
158
|
+
<span class="layer-name">Graph Memory + Checkpoint</span>
|
|
159
|
+
<span class="layer-badge badge-graph">Core</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="layer-row">
|
|
162
|
+
<span class="layer-name">Semantic Cache</span>
|
|
163
|
+
<span class="layer-badge badge-cache">Cache</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="layer-row">
|
|
166
|
+
<span class="layer-name">Prompt Compression</span>
|
|
167
|
+
<span class="layer-badge badge-active">Active</span>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="layer-row">
|
|
170
|
+
<span class="layer-name">Terse Output</span>
|
|
171
|
+
<span class="layer-badge badge-terse">Active</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="layer-row">
|
|
174
|
+
<span class="layer-name">Context Router</span>
|
|
175
|
+
<span class="layer-badge badge-routing">Beta</span>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- Live graph nodes (demo) -->
|
|
180
|
+
<div class="card">
|
|
181
|
+
<h3>Graph Node Types</h3>
|
|
182
|
+
<div>
|
|
183
|
+
<span class="graph-node node-goal">π― Goal</span>
|
|
184
|
+
<span class="graph-node node-task">β
Task</span>
|
|
185
|
+
<span class="graph-node node-decision">β‘ Decision</span>
|
|
186
|
+
<span class="graph-node node-file">π File</span>
|
|
187
|
+
<span class="graph-node node-error">π Error</span>
|
|
188
|
+
<span class="graph-node node-dependency">π¦ Dependency</span>
|
|
189
|
+
<span class="graph-node node-environment">π§ Environment</span>
|
|
190
|
+
<span class="graph-node node-endpoint">π Endpoint</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div style="margin-top:1rem;font-size:0.8rem;color:var(--muted)">
|
|
193
|
+
Session graphs are built live, survive restarts, and produce a compact resume block when context fills.
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- API Endpoints -->
|
|
199
|
+
<div class="card" style="margin-bottom:2rem">
|
|
200
|
+
<h3>API Endpoints</h3>
|
|
201
|
+
<div class="endpoint-box">
|
|
202
|
+
<span class="method">POST</span> <span class="path">/v1/chat/completions</span> <span class="desc">β drop-in OpenAI-compatible proxy</span>
|
|
203
|
+
<span class="method">GET </span> <span class="path">/api/resume/{session_id}</span> <span class="desc">β get checkpoint resume context</span>
|
|
204
|
+
<span class="method">POST</span> <span class="path">/api/checkpoint?session_id=</span> <span class="desc">β manually trigger checkpoint</span>
|
|
205
|
+
<span class="method">GET </span> <span class="path">/api/graph/{session_id}</span> <span class="desc">β session graph stats</span>
|
|
206
|
+
<span class="method">GET </span> <span class="path">/api/stats</span> <span class="desc">β analytics summary</span>
|
|
207
|
+
<span class="method">GET </span> <span class="path">/docs</span> <span class="desc">β interactive API docs (Swagger)</span>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- Resume block example -->
|
|
212
|
+
<div class="card" style="margin-bottom:2rem">
|
|
213
|
+
<h3>Example Resume Block (what gets injected on session resume)</h3>
|
|
214
|
+
<div class="resume-block" id="resume-example">Goal: Build FastAPI auth service with JWT + PostgreSQL
|
|
215
|
+
In progress: Refresh token rotation | Rate limiting
|
|
216
|
+
Done: Project structure | User model | Auth endpoints | Fix 422 error | Tests (12 passing)
|
|
217
|
+
Decided: PostgreSQL (concurrent writes) | bcrypt | Redis for refresh tokens (not DB)
|
|
218
|
+
Files: api/auth.py, api/models.py, api/main.py, config.py, tests/test_auth.py
|
|
219
|
+
Env: Python 3.12, FastAPI 0.111
|
|
220
|
+
Continue from: Add rate limiting to auth endpoints</div>
|
|
221
|
+
<div style="margin-top:0.75rem;font-size:0.8rem;color:var(--muted)" id="resume-tokens">
|
|
222
|
+
~280 tokens Β· replaces ~15,000+ tokens of conversation history
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<footer>
|
|
229
|
+
TokenMizer β open source Β· <a href="/docs" style="color:var(--accent)">API Docs</a> Β· <a href="/health" style="color:var(--accent)">Health</a>
|
|
230
|
+
</footer>
|
|
231
|
+
|
|
232
|
+
<script>
|
|
233
|
+
async function loadStats() {
|
|
234
|
+
try {
|
|
235
|
+
const [statsRes, cacheRes] = await Promise.all([
|
|
236
|
+
fetch('/api/stats').catch(() => null),
|
|
237
|
+
fetch('/api/cache/stats').catch(() => null),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
if (statsRes && statsRes.ok) {
|
|
241
|
+
const s = await statsRes.json();
|
|
242
|
+
const d = s.daily || {};
|
|
243
|
+
document.getElementById('req-today').textContent = (d.requests || 0).toLocaleString();
|
|
244
|
+
document.getElementById('tokens-saved').textContent = (d.tokens_saved || 0).toLocaleString();
|
|
245
|
+
document.getElementById('savings-pct').textContent =
|
|
246
|
+
d.savings_pct ? `${d.savings_pct.toFixed(1)}% reduction vs original` : 'No requests yet';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (cacheRes && cacheRes.ok) {
|
|
250
|
+
const c = await cacheRes.json();
|
|
251
|
+
document.getElementById('cache-hit-rate').textContent =
|
|
252
|
+
c.hit_rate != null ? `${(c.hit_rate * 100).toFixed(1)}%` : 'β';
|
|
253
|
+
document.getElementById('cache-entries').textContent =
|
|
254
|
+
`${(c.entries || 0).toLocaleString()} cached entries Β· ${c.utilization_pct || 0}% full`;
|
|
255
|
+
}
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.warn('Stats load failed:', e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
loadStats();
|
|
262
|
+
setInterval(loadStats, 15000);
|
|
263
|
+
</script>
|
|
264
|
+
|
|
265
|
+
</body>
|
|
266
|
+
</html>
|
|
267
|
+
"""
|
|
File without changes
|