fix(web): honor currency setting in benchmark costs (#113)

This commit is contained in:
laodouuu authored and GitHub committed 2026-07-29 23:29:59 +08:00
1 parent 4681135423
commit a4a434cfaf
1 file changed
+44 -13
@@ -25,6 +25,8 @@ import { apiErrorText } from "../../lib/api-error";
import { useDocumentTitle } from "../../lib/use-document-title"; import { useDocumentTitle } from "../../lib/use-document-title";
import { formatDateTime, formatMoney, formatScore, humanizeDuration } from "../../lib/format"; import { formatDateTime, formatMoney, formatScore, humanizeDuration } from "../../lib/format";
import { agentDisplayName, useProject } from "../../state/project"; import { agentDisplayName, useProject } from "../../state/project";
import { useTheme } from "../../state/theme";
import type { Currency } from "../../state/theme";
import { AgentAvatar } from "../../components/ui/agent-avatar"; import { AgentAvatar } from "../../components/ui/agent-avatar";
import { Chevron } from "../../components/ui/chevron"; import { Chevron } from "../../components/ui/chevron";
import { Segmented } from "../../components/ui/segmented"; import { Segmented } from "../../components/ui/segmented";
@@ -152,11 +154,11 @@ function metricLabel(metric: BenchmarkMetric): string {
} }
/** Display format for a metric value (shared by y-axis ticks and the tooltip): score / cost / duration each have their own formatting rule. */ /** Display format for a metric value (shared by y-axis ticks and the tooltip): score / cost / duration each have their own formatting rule. */
function formatMetric(metric: BenchmarkMetric, v: number): string { function formatMetric(metric: BenchmarkMetric, v: number, currency: Currency): string {
return metric === "score" return metric === "score"
? formatScore(v) ? formatScore(v)
: metric === "cost" : metric === "cost"
? formatMoney(v) ? formatMoney(v, currency)
: humanizeDuration(v); : humanizeDuration(v);
} }
@@ -176,10 +178,12 @@ function MetricTrendChart({
evaluations, evaluations,
series, series,
metric, metric,
currency,
}: { }: {
evaluations: BenchmarkEvaluation[]; evaluations: BenchmarkEvaluation[];
series: EvaluationSeries[]; series: EvaluationSeries[];
metric: BenchmarkMetric; metric: BenchmarkMetric;
currency: Currency;
}) { }) {
const [hover, setHover] = useState<number | null>(null); const [hover, setHover] = useState<number | null>(null);
const [ref, width] = useChartWidth(); const [ref, width] = useChartWidth();
@@ -194,7 +198,7 @@ function MetricTrendChart({
{width > 0 && ( {width > 0 && (
<ChartFrame <ChartFrame
geom={geom} geom={geom}
fmtY={(v) => formatMetric(metric, v)} fmtY={(v) => formatMetric(metric, v, currency)}
dates={dates} dates={dates}
hover={hover} hover={hover}
onHover={setHover} onHover={setHover}
@@ -205,7 +209,7 @@ function MetricTrendChart({
<> <>
<p className="text-gray-400">{formatDateTime(e.time)}</p> <p className="text-gray-400">{formatDateTime(e.time)}</p>
<p className="font-mono"> <p className="font-mono">
{v === null ? "—" : formatMetric(metric, v)} {v === null ? "—" : formatMetric(metric, v, currency)}
{e.version !== undefined && ( {e.version !== undefined && (
<span className="ml-1.5 text-gray-400">v{e.version}</span> <span className="ml-1.5 text-gray-400">v{e.version}</span>
)} )}
@@ -277,7 +281,13 @@ function MetricTrendChart({
* disambiguate. Mounted under a keyed container per Benchmark: switching Benchmarks resets back * disambiguate. Mounted under a keyed container per Benchmark: switching Benchmarks resets back
* to "score". * to "score".
*/ */
function TrendSection({ evaluations }: { evaluations: BenchmarkEvaluation[] }) { function TrendSection({
evaluations,
currency,
}: {
evaluations: BenchmarkEvaluation[];
currency: Currency;
}) {
const [metric, setMetric] = useState<BenchmarkMetric>("score"); const [metric, setMetric] = useState<BenchmarkMetric>("score");
const series = modelSeries(evaluations); const series = modelSeries(evaluations);
const ids = series.map((s) => s.modelId).filter((v): v is string => v !== undefined); const ids = series.map((s) => s.modelId).filter((v): v is string => v !== undefined);
@@ -324,7 +334,12 @@ function TrendSection({ evaluations }: { evaluations: BenchmarkEvaluation[] }) {
))} ))}
</div> </div>
)} )}
<MetricTrendChart evaluations={evaluations} series={series} metric={metric} /> <MetricTrendChart
evaluations={evaluations}
series={series}
metric={metric}
currency={currency}
/>
</div> </div>
); );
} }
@@ -335,9 +350,11 @@ const CELL = "px-3 py-2";
function EvaluationRow({ function EvaluationRow({
agentId, agentId,
evaluation, evaluation,
currency,
}: { }: {
agentId: string; agentId: string;
evaluation: BenchmarkEvaluation; evaluation: BenchmarkEvaluation;
currency: Currency;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
@@ -365,7 +382,7 @@ function EvaluationRow({
{formatScore(evaluation.score)} {formatScore(evaluation.score)}
</td> </td>
<td className={`${CELL} font-mono text-xs tabular-nums text-gray-500 dark:text-gray-400`}> <td className={`${CELL} font-mono text-xs tabular-nums text-gray-500 dark:text-gray-400`}>
{formatMoney(evaluation.cost)} {formatMoney(evaluation.cost, currency)}
</td> </td>
<td className={`${CELL} font-mono text-xs tabular-nums text-gray-500 dark:text-gray-400`}> <td className={`${CELL} font-mono text-xs tabular-nums text-gray-500 dark:text-gray-400`}>
{evaluation.durationMs !== undefined ? humanizeDuration(evaluation.durationMs) : "—"} {evaluation.durationMs !== undefined ? humanizeDuration(evaluation.durationMs) : "—"}
@@ -406,7 +423,7 @@ function EvaluationRow({
</thead> </thead>
<tbody> <tbody>
{evaluation.cases.map((c) => ( {evaluation.cases.map((c) => (
<CaseRow key={c.case} agentId={agentId} caseScore={c} /> <CaseRow key={c.case} agentId={agentId} caseScore={c} currency={currency} />
))} ))}
</tbody> </tbody>
</table> </table>
@@ -437,7 +454,15 @@ function SessionLink({ agentId, sessionId }: { agentId: string; sessionId?: stri
* of each run (#index + score / cost / duration / Session link); with the old format lacking * of each run (#index + score / cost / duration / Session link); with the old format lacking
* runs, it's not expandable and the case-level single Session link is used as before. * runs, it's not expandable and the case-level single Session link is used as before.
*/ */
function CaseRow({ agentId, caseScore: c }: { agentId: string; caseScore: BenchmarkCaseScore }) { function CaseRow({
agentId,
caseScore: c,
currency,
}: {
agentId: string;
caseScore: BenchmarkCaseScore;
currency: Currency;
}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const runs = c.runs ?? []; const runs = c.runs ?? [];
const expandable = runs.length > 0; const expandable = runs.length > 0;
@@ -455,7 +480,7 @@ function CaseRow({ agentId, caseScore: c }: { agentId: string; caseScore: Benchm
</td> </td>
<td className="px-2 py-1 font-mono tabular-nums">{formatScore(c.score)}</td> <td className="px-2 py-1 font-mono tabular-nums">{formatScore(c.score)}</td>
<td className="px-2 py-1 font-mono tabular-nums text-gray-500 dark:text-gray-400"> <td className="px-2 py-1 font-mono tabular-nums text-gray-500 dark:text-gray-400">
{formatMoney(c.cost)} {formatMoney(c.cost, currency)}
</td> </td>
<td className="px-2 py-1 font-mono tabular-nums text-gray-500 dark:text-gray-400"> <td className="px-2 py-1 font-mono tabular-nums text-gray-500 dark:text-gray-400">
{c.durationMs !== undefined ? humanizeDuration(c.durationMs) : "—"} {c.durationMs !== undefined ? humanizeDuration(c.durationMs) : "—"}
@@ -472,7 +497,7 @@ function CaseRow({ agentId, caseScore: c }: { agentId: string; caseScore: Benchm
{S.benchmark.colRun} #{i + 1} {S.benchmark.colRun} #{i + 1}
</td> </td>
<td className="px-2 py-1 font-mono tabular-nums">{formatScore(run.score)}</td> <td className="px-2 py-1 font-mono tabular-nums">{formatScore(run.score)}</td>
<td className="px-2 py-1 font-mono tabular-nums">{formatMoney(run.cost)}</td> <td className="px-2 py-1 font-mono tabular-nums">{formatMoney(run.cost, currency)}</td>
<td className="px-2 py-1 font-mono tabular-nums"> <td className="px-2 py-1 font-mono tabular-nums">
{run.durationMs !== undefined ? humanizeDuration(run.durationMs) : "—"} {run.durationMs !== undefined ? humanizeDuration(run.durationMs) : "—"}
</td> </td>
@@ -491,6 +516,7 @@ function CaseRow({ agentId, caseScore: c }: { agentId: string; caseScore: Benchm
export function BenchmarkPage() { export function BenchmarkPage() {
useDocumentTitle(S.benchmark.title); useDocumentTitle(S.benchmark.title);
const { currentProject, agents, agentsLoading } = useProject(); const { currentProject, agents, agentsLoading } = useProject();
const { currency } = useTheme();
const projectId = currentProject?.projectId ?? null; const projectId = currentProject?.projectId ?? null;
// ?agentId= deep link (entered from the "Benchmark" tab on the Agent settings page): only the target Agent is expanded by default. // ?agentId= deep link (entered from the "Benchmark" tab on the Agent settings page): only the target Agent is expanded by default.
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -555,7 +581,7 @@ export function BenchmarkPage() {
<EmptyState title={S.benchmark.noEvaluations} /> <EmptyState title={S.benchmark.noEvaluations} />
) : ( ) : (
<> <>
<TrendSection evaluations={evaluations} /> <TrendSection evaluations={evaluations} currency={currency} />
<div> <div>
<p className="mb-1 text-xs font-semibold text-gray-500"> <p className="mb-1 text-xs font-semibold text-gray-500">
@@ -575,7 +601,12 @@ export function BenchmarkPage() {
</thead> </thead>
<tbody> <tbody>
{[...evaluations].reverse().map((ev, i) => ( {[...evaluations].reverse().map((ev, i) => (
<EvaluationRow key={i} agentId={selection.agentId} evaluation={ev} /> <EvaluationRow
key={i}
agentId={selection.agentId}
evaluation={ev}
currency={currency}
/>
))} ))}
</tbody> </tbody>
</table> </table>