@data-club/ai-hub 0.0.9 → 0.0.11
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.
- package/dist/auth/locales/cs.d.ts +1 -1
- package/dist/auth/locales/cs.d.ts.map +1 -1
- package/dist/auth/locales/cs.js +1 -12
- package/dist/auth/locales/cs.js.map +1 -1
- package/dist/auth/locales/sk.d.ts +1 -1
- package/dist/auth/locales/sk.d.ts.map +1 -1
- package/dist/auth/locales/sk.js +1 -12
- package/dist/auth/locales/sk.js.map +1 -1
- package/dist/auth/texts.d.ts +0 -17
- package/dist/auth/texts.d.ts.map +1 -1
- package/dist/auth/texts.js +0 -11
- package/dist/auth/texts.js.map +1 -1
- package/dist/client/AIHubClient.d.ts +2 -39
- package/dist/client/AIHubClient.d.ts.map +1 -1
- package/dist/client/AIHubClient.js +2 -13
- package/dist/client/AIHubClient.js.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts +7 -8
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js +8 -83
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -1
- package/dist/components/MessageList/index.d.ts.map +1 -1
- package/dist/components/MessageList/index.js +1 -2
- package/dist/components/MessageList/index.js.map +1 -1
- package/dist/context/AIHubProvider.d.ts.map +1 -1
- package/dist/context/AIHubProvider.js +1 -31
- package/dist/context/AIHubProvider.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/state/stateReducer.d.ts.map +1 -1
- package/dist/state/stateReducer.js +0 -1
- package/dist/state/stateReducer.js.map +1 -1
- package/dist/types.d.ts +1 -73
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/auth/locales/cs.ts +1 -17
- package/src/auth/locales/sk.ts +1 -17
- package/src/auth/texts.ts +0 -34
- package/src/client/AIHubClient.ts +2 -42
- package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +8 -177
- package/src/components/MessageList/index.tsx +0 -4
- package/src/context/AIHubProvider.tsx +1 -32
- package/src/index.ts +0 -8
- package/src/state/stateReducer.ts +0 -1
- package/src/styles/default.css +0 -63
- package/src/types.ts +0 -75
- package/src/components/CacheBadge/index.tsx +0 -245
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `<CacheBadge />` — small per-message badge surfacing provider token
|
|
3
|
-
* usage on an assistant bubble. Renders an inline icon + a compact
|
|
4
|
-
* token count, with the full breakdown in a `title`-attribute tooltip
|
|
5
|
-
* on hover.
|
|
6
|
-
*
|
|
7
|
-
* **What it shows.** Letta reports five things per assistant turn (via
|
|
8
|
-
* `AIHubMessageUsage`): input tokens, output tokens, cache-read tokens
|
|
9
|
-
* (prompt-cache HIT — provider charged ~0.1x), cache-write tokens
|
|
10
|
-
* (provider charged ~1.25x, Anthropic only), reasoning tokens, and a
|
|
11
|
-
* rolling context-window estimate.
|
|
12
|
-
*
|
|
13
|
-
* The headline number we show in the badge is the **cache-hit
|
|
14
|
-
* percentage of the prompt** — `cache_read / (input + cache_read +
|
|
15
|
-
* cache_write)`. That number is the single most useful at-a-glance
|
|
16
|
-
* signal:
|
|
17
|
-
* - 0% → cache is cold (cost is the worst case)
|
|
18
|
-
* - 80% → ~10x cheaper system prompt than a cold turn
|
|
19
|
-
*
|
|
20
|
-
* The full numbers live in the tooltip. We use the native `title`
|
|
21
|
-
* attribute instead of a custom popover to keep the component
|
|
22
|
-
* lightweight and dependency-free (no portals, no animation, no
|
|
23
|
-
* focus management — perfect for a peripheral info chip).
|
|
24
|
-
*
|
|
25
|
-
* **When to render.** Only when the parent passes a `usage` prop with
|
|
26
|
-
* at least one non-null number. Messages without a usage stamp (e.g.
|
|
27
|
-
* historical conversations from before this feature shipped, or
|
|
28
|
-
* tool-only assistant turns where Letta didn't report anything)
|
|
29
|
-
* simply don't render the badge.
|
|
30
|
-
*
|
|
31
|
-
* Exported under `AIHub.CacheBadge` (re-exported via the namespace).
|
|
32
|
-
*
|
|
33
|
-
* @see AIHubMessageUsage in `../../types.ts`
|
|
34
|
-
* @see SPECIFICATIONS.md §9.4 — `usage` SSE event
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import type { ReactNode } from "react";
|
|
38
|
-
|
|
39
|
-
import type { AIHubMessageUsage } from "../../types.js";
|
|
40
|
-
|
|
41
|
-
/** BEM class names emitted by `<CacheBadge />`. */
|
|
42
|
-
export interface CacheBadgeClassNames {
|
|
43
|
-
/** Outer span; `data-cache-state="hot|warm|cold|none"` for skinning. */
|
|
44
|
-
root: string;
|
|
45
|
-
/** The leading icon character (default 💾). */
|
|
46
|
-
icon: string;
|
|
47
|
-
/** The headline percentage label. */
|
|
48
|
-
label: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const DEFAULT_CLASS_NAMES: CacheBadgeClassNames = Object.freeze({
|
|
52
|
-
root: "data-club-ai-hub__cache-badge",
|
|
53
|
-
icon: "data-club-ai-hub__cache-badge__icon",
|
|
54
|
-
label: "data-club-ai-hub__cache-badge__label",
|
|
55
|
-
}) as CacheBadgeClassNames;
|
|
56
|
-
|
|
57
|
-
/** Localizable strings for the badge headline + tooltip. */
|
|
58
|
-
export interface CacheBadgeTexts {
|
|
59
|
-
/** Headline shown when there's no cache info at all (e.g. providers that don't report). */
|
|
60
|
-
noCacheLabel: string;
|
|
61
|
-
/** Headline shown when this turn was a full cache miss (no tokens served from cache). */
|
|
62
|
-
missLabel: string;
|
|
63
|
-
/** Headline shown when this turn was a cache priming write — read=0 but write>0. */
|
|
64
|
-
writePrimingLabel: string;
|
|
65
|
-
/** Headline prefix for cache hits — full text is `${hitLabel} ${N}%`. */
|
|
66
|
-
hitLabel: string;
|
|
67
|
-
/** Tooltip line prefixes. Keep short — the tooltip is `title` only. */
|
|
68
|
-
hitTooltipLabel: string;
|
|
69
|
-
writeTooltipLabel: string;
|
|
70
|
-
inputLabel: string;
|
|
71
|
-
outputLabel: string;
|
|
72
|
-
reasoningLabel: string;
|
|
73
|
-
contextLabel: string;
|
|
74
|
-
totalLabel: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const DEFAULT_TEXTS: CacheBadgeTexts = Object.freeze({
|
|
78
|
-
noCacheLabel: "no data",
|
|
79
|
-
missLabel: "MISS",
|
|
80
|
-
writePrimingLabel: "PRIMING",
|
|
81
|
-
hitLabel: "HIT",
|
|
82
|
-
hitTooltipLabel: "Cache hit",
|
|
83
|
-
writeTooltipLabel: "Cache write",
|
|
84
|
-
inputLabel: "Input",
|
|
85
|
-
outputLabel: "Output",
|
|
86
|
-
reasoningLabel: "Reasoning",
|
|
87
|
-
contextLabel: "Context",
|
|
88
|
-
totalLabel: "Total",
|
|
89
|
-
}) as CacheBadgeTexts;
|
|
90
|
-
|
|
91
|
-
/** Props for `<CacheBadge />`. */
|
|
92
|
-
export interface CacheBadgeProps {
|
|
93
|
-
/** Per-message usage, as returned by `GET /sessions/:id/messages`. */
|
|
94
|
-
usage: AIHubMessageUsage;
|
|
95
|
-
/** Optional class-name overrides. */
|
|
96
|
-
classNames?: Partial<CacheBadgeClassNames>;
|
|
97
|
-
/** Optional text overrides (l10n). */
|
|
98
|
-
texts?: Partial<CacheBadgeTexts>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Computes the cache-hit ratio (0..1) over the prompt-side tokens.
|
|
103
|
-
*
|
|
104
|
-
* Denominator: `input + cache_read + cache_write` — every prompt
|
|
105
|
-
* token the provider saw, regardless of whether it served from cache
|
|
106
|
-
* or wrote new. We exclude `output` and `reasoning` because those
|
|
107
|
-
* are completion-side and can't benefit from prompt caching.
|
|
108
|
-
*
|
|
109
|
-
* Exported so the test suite can assert numerically.
|
|
110
|
-
*/
|
|
111
|
-
export function computeCacheHitRatio(usage: AIHubMessageUsage): number | null {
|
|
112
|
-
const read = usage.cache_read ?? 0;
|
|
113
|
-
const write = usage.cache_write ?? 0;
|
|
114
|
-
const input = usage.input ?? 0;
|
|
115
|
-
const denom = read + write + input;
|
|
116
|
-
if (denom === 0) return null;
|
|
117
|
-
return read / denom;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Bucket the usage into a discrete state for skinning + label choice.
|
|
122
|
-
*
|
|
123
|
-
* - `none` — no prompt-side tokens reported at all (provider
|
|
124
|
-
* didn't expose usage, or empty turn). Badge shows
|
|
125
|
-
* the "no data" label.
|
|
126
|
-
* - `miss` — full cache miss: `cache_read = 0` AND
|
|
127
|
-
* `cache_write = 0` (or not reported). The provider
|
|
128
|
-
* had to reinitialize the whole prompt. Badge shows
|
|
129
|
-
* "MISS" in cold (grey) styling.
|
|
130
|
-
* - `priming` — `cache_read = 0` but `cache_write > 0`. This
|
|
131
|
-
* turn's prompt was a CACHE WRITE — the provider
|
|
132
|
-
* paid full price now and future turns benefit.
|
|
133
|
-
* Anthropic only (the other providers don't report
|
|
134
|
-
* writes). Badge shows "PRIMING".
|
|
135
|
-
* - `cold` — `cache_read > 0` but ratio < 10%. Hit registered
|
|
136
|
-
* but most of the prompt was new.
|
|
137
|
-
* - `warm` — 10–60% of the prompt was cached.
|
|
138
|
-
* - `hot` — 60%+ of the prompt was cached.
|
|
139
|
-
*/
|
|
140
|
-
type CacheState = "none" | "miss" | "priming" | "cold" | "warm" | "hot";
|
|
141
|
-
|
|
142
|
-
function bucketState(usage: AIHubMessageUsage, ratio: number | null): CacheState {
|
|
143
|
-
if (ratio === null) return "none";
|
|
144
|
-
if (ratio === 0) {
|
|
145
|
-
// No cache hit. Distinguish full-miss (nothing cached anywhere)
|
|
146
|
-
// from priming (we WROTE to the cache this turn — next turn will
|
|
147
|
-
// benefit). The latter is good news, the former is bad news, so
|
|
148
|
-
// the user-facing labels should differ.
|
|
149
|
-
return (usage.cache_write ?? 0) > 0 ? "priming" : "miss";
|
|
150
|
-
}
|
|
151
|
-
if (ratio >= 0.6) return "hot";
|
|
152
|
-
if (ratio >= 0.1) return "warm";
|
|
153
|
-
return "cold";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Formats a token count as a compact human-readable string: 1234 → 1.2k. */
|
|
157
|
-
function formatTokens(n: number | null | undefined): string {
|
|
158
|
-
if (n === null || n === undefined) return "—";
|
|
159
|
-
if (n < 1000) return String(n);
|
|
160
|
-
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
|
|
161
|
-
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Builds the `title`-attribute tooltip text from the usage object. */
|
|
165
|
-
function buildTooltip(usage: AIHubMessageUsage, texts: CacheBadgeTexts): string {
|
|
166
|
-
const lines: string[] = [];
|
|
167
|
-
if (usage.cache_read !== null && usage.cache_read !== undefined) {
|
|
168
|
-
lines.push(`${texts.hitTooltipLabel}: ${formatTokens(usage.cache_read)}`);
|
|
169
|
-
}
|
|
170
|
-
if (usage.cache_write !== null && usage.cache_write !== undefined) {
|
|
171
|
-
lines.push(`${texts.writeTooltipLabel}: ${formatTokens(usage.cache_write)}`);
|
|
172
|
-
}
|
|
173
|
-
if (usage.input !== null && usage.input !== undefined) {
|
|
174
|
-
lines.push(`${texts.inputLabel}: ${formatTokens(usage.input)}`);
|
|
175
|
-
}
|
|
176
|
-
if (usage.output !== null && usage.output !== undefined) {
|
|
177
|
-
lines.push(`${texts.outputLabel}: ${formatTokens(usage.output)}`);
|
|
178
|
-
}
|
|
179
|
-
if (usage.reasoning !== null && usage.reasoning !== undefined && usage.reasoning > 0) {
|
|
180
|
-
lines.push(`${texts.reasoningLabel}: ${formatTokens(usage.reasoning)}`);
|
|
181
|
-
}
|
|
182
|
-
if (usage.context !== null && usage.context !== undefined) {
|
|
183
|
-
lines.push(`${texts.contextLabel}: ${formatTokens(usage.context)}`);
|
|
184
|
-
}
|
|
185
|
-
if (usage.total !== null && usage.total !== undefined) {
|
|
186
|
-
lines.push(`${texts.totalLabel}: ${formatTokens(usage.total)}`);
|
|
187
|
-
}
|
|
188
|
-
return lines.join("\n");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Returns true when the usage object has at least one usable numeric field. */
|
|
192
|
-
function hasAnyData(usage: AIHubMessageUsage): boolean {
|
|
193
|
-
return (
|
|
194
|
-
(usage.input ?? null) !== null ||
|
|
195
|
-
(usage.output ?? null) !== null ||
|
|
196
|
-
(usage.cache_read ?? null) !== null ||
|
|
197
|
-
(usage.cache_write ?? null) !== null ||
|
|
198
|
-
(usage.reasoning ?? null) !== null ||
|
|
199
|
-
(usage.context ?? null) !== null ||
|
|
200
|
-
(usage.total ?? null) !== null
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function CacheBadge({
|
|
205
|
-
usage,
|
|
206
|
-
classNames,
|
|
207
|
-
texts,
|
|
208
|
-
}: CacheBadgeProps): ReactNode {
|
|
209
|
-
if (!hasAnyData(usage)) return null;
|
|
210
|
-
const cls: CacheBadgeClassNames = { ...DEFAULT_CLASS_NAMES, ...(classNames ?? {}) };
|
|
211
|
-
const t: CacheBadgeTexts = { ...DEFAULT_TEXTS, ...(texts ?? {}) };
|
|
212
|
-
|
|
213
|
-
const ratio = computeCacheHitRatio(usage);
|
|
214
|
-
const state = bucketState(usage, ratio);
|
|
215
|
-
// Headline label — varies by state so the user gets an immediate
|
|
216
|
-
// diagnostic signal at-a-glance.
|
|
217
|
-
let headline: string;
|
|
218
|
-
if (state === "none") {
|
|
219
|
-
headline = t.noCacheLabel;
|
|
220
|
-
} else if (state === "miss") {
|
|
221
|
-
headline = t.missLabel;
|
|
222
|
-
} else if (state === "priming") {
|
|
223
|
-
headline = t.writePrimingLabel;
|
|
224
|
-
} else {
|
|
225
|
-
// hot / warm / cold — ratio is guaranteed non-null here.
|
|
226
|
-
headline = `${t.hitLabel} ${Math.round((ratio ?? 0) * 100)}%`;
|
|
227
|
-
}
|
|
228
|
-
const tooltip = buildTooltip(usage, t);
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<span
|
|
232
|
-
className={cls.root}
|
|
233
|
-
data-testid="cache-badge"
|
|
234
|
-
data-cache-state={state}
|
|
235
|
-
title={tooltip}
|
|
236
|
-
role="status"
|
|
237
|
-
aria-label={tooltip}
|
|
238
|
-
>
|
|
239
|
-
<span className={cls.icon} aria-hidden="true">
|
|
240
|
-
💾
|
|
241
|
-
</span>
|
|
242
|
-
<span className={cls.label}>{headline}</span>
|
|
243
|
-
</span>
|
|
244
|
-
);
|
|
245
|
-
}
|