Skip to content

Commit adf0684

Browse files
committed
fix(webapp): harden cache metric and sparkline queries
The cache hit-rate and savings queries divided by zero for models with no cached tokens, surfacing NaN or empty widgets; they now return 0 via ifNull/nullIf. Model usage sparklines bucketed on a timezone-dependent DateTime string, which could misalign bars with the charts above them; they now key on toUnixTimestamp so buckets line up regardless of the ClickHouse server timezone.
1 parent e372c8b commit adf0684

3 files changed

Lines changed: 14 additions & 13 deletions

File tree

apps/webapp/app/presenters/v3/BuiltInDashboards.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ const llmDashboard: BuiltInDashboard = {
496496
"llm-cache-hit": {
497497
title: "Cache hit rate over time",
498498
query:
499-
"SELECT timeBucket(), round(sum(cached_read_tokens) * 100.0 / (sum(input_tokens) + sum(cached_read_tokens)), 1) AS cache_hit_pct FROM llm_metrics GROUP BY timeBucket ORDER BY timeBucket",
499+
"SELECT timeBucket(), round(ifNull(sum(cached_read_tokens) * 100.0 / nullIf(sum(input_tokens) + sum(cached_read_tokens), 0), 0), 1) AS cache_hit_pct FROM llm_metrics GROUP BY timeBucket ORDER BY timeBucket",
500500
display: {
501501
type: "chart",
502502
chartType: "line",
@@ -528,7 +528,7 @@ const llmDashboard: BuiltInDashboard = {
528528
"llm-cache-savings": {
529529
title: "Cache savings over time",
530530
query:
531-
"SELECT timeBucket(), round(sum(cached_read_tokens) * (sum(input_cost) / (sum(input_tokens) + 1)) - sum(cached_read_cost), 4) AS cache_savings FROM llm_metrics WHERE cached_read_tokens > 0 GROUP BY timeBucket ORDER BY timeBucket",
531+
"SELECT timeBucket(), round(ifNull(sum(cached_read_tokens) * (sum(input_cost) / nullIf(sum(input_tokens), 0)) - sum(cached_read_cost), 0), 4) AS cache_savings FROM llm_metrics WHERE cached_read_tokens > 0 GROUP BY timeBucket ORDER BY timeBucket",
532532
display: {
533533
type: "chart",
534534
chartType: "bar",
@@ -544,7 +544,7 @@ const llmDashboard: BuiltInDashboard = {
544544
"llm-cache-by-model": {
545545
title: "Cache hit rate by model",
546546
query:
547-
"SELECT response_model, round(sum(cached_read_tokens) * 100.0 / (sum(input_tokens) + sum(cached_read_tokens)), 1) AS cache_hit_pct, sum(cached_read_tokens) AS cached_tokens FROM llm_metrics GROUP BY response_model ORDER BY cached_tokens DESC LIMIT 20",
547+
"SELECT response_model, round(ifNull(sum(cached_read_tokens) * 100.0 / nullIf(sum(input_tokens) + sum(cached_read_tokens), 0), 0), 1) AS cache_hit_pct, sum(cached_read_tokens) AS cached_tokens FROM llm_metrics GROUP BY response_model ORDER BY cached_tokens DESC LIMIT 20",
548548
display: { type: "table", prettyFormatting: true, sorting: [] },
549549
},
550550
},

apps/webapp/app/presenters/v3/ModelRegistryPresenter.server.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,17 @@ function sparklineBucketSeconds(rangeMs: number): number {
9696

9797
/**
9898
* Generate the ordered bucket-start keys for [from, to] at the given interval,
99-
* epoch-aligned in UTC to exactly match ClickHouse's
100-
* `toStartOfInterval(col, INTERVAL n SECOND)` output strings ("YYYY-MM-DD HH:MM:SS").
99+
* as epoch seconds to match ClickHouse's
100+
* `toUnixTimestamp(toStartOfInterval(col, INTERVAL n SECOND))` — timezone-independent
101+
* (a raw DateTime string would depend on the ClickHouse server timezone).
101102
*/
102-
function sparklineBucketKeys(from: Date, to: Date, intervalSeconds: number): string[] {
103+
function sparklineBucketKeys(from: Date, to: Date, intervalSeconds: number): number[] {
103104
const intervalMs = intervalSeconds * 1000;
104105
const start = Math.floor(from.getTime() / intervalMs) * intervalMs;
105106
const end = Math.floor(to.getTime() / intervalMs) * intervalMs;
106-
const keys: string[] = [];
107+
const keys: number[] = [];
107108
for (let t = start; t <= end; t += intervalMs) {
108-
keys.push(new Date(t).toISOString().slice(0, 19).replace("T", " "));
109+
keys.push(t / 1000);
109110
}
110111
return keys;
111112
}
@@ -269,7 +270,7 @@ const ProjectModelUsageRow = z.object({
269270

270271
const ModelSparklineRow = z.object({
271272
response_model: z.string(),
272-
bucket: z.string(),
273+
bucket: z.coerce.number(),
273274
val: z.coerce.number(),
274275
});
275276

@@ -754,7 +755,7 @@ export class ModelRegistryPresenter extends BasePresenter {
754755
query: `
755756
SELECT
756757
response_model,
757-
toStartOfInterval(start_time, INTERVAL ${intervalSeconds} SECOND) AS bucket,
758+
toUnixTimestamp(toStartOfInterval(start_time, INTERVAL ${intervalSeconds} SECOND)) AS bucket,
758759
${valueExpr} AS val
759760
FROM trigger_dev.llm_metrics_v1
760761
WHERE environment_id = {environmentId: String}
@@ -797,9 +798,9 @@ export class ModelRegistryPresenter extends BasePresenter {
797798
#buildSparklineMap(
798799
queryResult:
799800
| [Error, null]
800-
| [null, { response_model: string; bucket: string; val: number }[]],
801+
| [null, { response_model: string; bucket: number; val: number }[]],
801802
keys: string[],
802-
bucketKeys: string[]
803+
bucketKeys: number[]
803804
): Record<string, number[]> {
804805
const [error, rows] = queryResult;
805806
if (error || !rows) return {};

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,7 @@ function DetailYourUsageTab({
11711171
<MetricWidget
11721172
widgetKey={`${modelName}-user-cache-hit`}
11731173
title="Cache hit rate over time"
1174-
query={`SELECT timeBucket(), round(sum(cached_read_tokens) * 100.0 / (sum(input_tokens) + sum(cached_read_tokens)), 1) AS cache_hit_pct FROM llm_metrics WHERE response_model = '${escapeTSQL(
1174+
query={`SELECT timeBucket(), round(ifNull(sum(cached_read_tokens) * 100.0 / nullIf(sum(input_tokens) + sum(cached_read_tokens), 0), 0), 1) AS cache_hit_pct FROM llm_metrics WHERE response_model = '${escapeTSQL(
11751175
modelName
11761176
)}' GROUP BY timeBucket ORDER BY timeBucket`}
11771177
config={chartConfig({

0 commit comments

Comments
 (0)