SPB Git

spb/llmindex Public

The discriminative, contamination-resistant, fully transparent LLM ranking — updated live.

TypeScript 77.9% TeX 15.2% Python 3.7% SQL 1.4% JavaScript 1.1% Shell 0.5%

feat(web): dedicated full-page interactive live Pareto frontier chart (/frontier)

Hand-rolled SVG scatter: log-cost x-axis, Global Index y-axis with 95% CI
whiskers on every point, Pareto step line, direct labels on frontier models,
per-mark hover tooltips (score/CI, cost, latency, status) with enlarged hit
targets, click-through to model pages, 15s live polling with entry animation
for newly landed models. Two-class palette #047857/#7c3aed validated (CVD
deutan dE 23.9, contrast >=3:1). Nav link + home cross-link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 6 days ago (Aug 5, 2026) parent 45b821d

Showing 6 changed files with +423 and −11

modified apps/web/app/api/v1/efficiency/route.ts +4 −0
@@ -19,7 +19,11 @@ export async function GET(req: NextRequest) {
19 19 points: points.map((p) => ({
20 20 model: p.slug,
21 21 name: p.name,
22 + provider: p.provider,
22 23 score: p.score,
24 + score_low: p.scoreLow,
25 + score_high: p.scoreHigh,
26 + latency_p50: p.latencyP50,
23 27 cost_per_1k_items: Number(p.costPer1kItems.toFixed(4)),
24 28 on_frontier: p.onFrontier,
25 29 })),
added apps/web/app/frontier/page.tsx +69 −0
@@ -0,0 +1,69 @@
1 +/**
2 + * llmindex.io — dedicated full-page live efficiency frontier
3 + * Author: Simon-Pierre Boucher
4 + * Contact: contact@spboucher.ai
5 + * License: Proprietary — © Simon-Pierre Boucher, all rights reserved
6 + */
7 +import type { Metadata } from 'next';
8 +import Link from 'next/link';
9 +import { FrontierChart, type FrontierPoint } from '@/components/FrontierChart';
10 +import { getEfficiencyData } from '@/lib/data';
11 +
12 +export const metadata: Metadata = {
13 + title: 'Efficiency Frontier',
14 + description:
15 + 'Live Pareto frontier of LLM quality vs. measured cost — every point with 95% confidence intervals, updated automatically as models finish the benchmark.',
16 +};
17 +export const revalidate = 300;
18 +
19 +export default async function FrontierPage() {
20 + const data = await getEfficiencyData();
21 + const initial: FrontierPoint[] = data.map((p) => ({
22 + model: p.slug,
23 + name: p.name,
24 + provider: p.provider,
25 + score: p.score,
26 + score_low: p.scoreLow,
27 + score_high: p.scoreHigh,
28 + latency_p50: p.latencyP50,
29 + cost_per_1k_items: Number(p.costPer1kItems.toFixed(4)),
30 + on_frontier: p.onFrontier,
31 + }));
32 +
33 + return (
34 + <div className="space-y-6">
35 + <div className="space-y-2">
36 + <h1 className="text-2xl font-bold tracking-tight text-zinc-900 sm:text-3xl">
37 + Efficiency frontier
38 + </h1>
39 + <p className="max-w-3xl text-sm leading-relaxed text-zinc-600">
40 + Global Index versus <em>measured</em> cost per 1,000 evaluation items (metered tokens ×
41 + live pricing), on a log scale. Models on the{' '}
42 + <span className="font-semibold text-emerald-700">frontier</span> are not dominated on
43 + both axes; every other model is strictly worse on quality <em>and</em> cost than some
44 + frontier point. By design this is published as a set — never collapsed into a single
45 + blended “value” number. The chart updates itself as the live benchmark lands each model.
46 + </p>
47 + </div>
48 +
49 + {/* full-bleed chart: break out of the content column on large screens */}
50 + <div className="lg:relative lg:left-1/2 lg:w-[min(96vw,1500px)] lg:-translate-x-1/2">
51 + <FrontierChart initial={initial} />
52 + </div>
53 +
54 + <p className="text-sm text-zinc-500">
55 + Prefer numbers? The same data lives in the{' '}
56 + <Link href="/" className="underline hover:text-emerald-600">
57 + leaderboard table
58 + </Link>{' '}
59 + and the{' '}
60 + <a href="/api/v1/efficiency" className="underline hover:text-emerald-600">
61 + public API
62 + </a>
63 + . Methodology: cost is the mean measured cost per item across a model&apos;s scored
64 + domains × 1,000; whiskers are the 95% CI of the Global Index; the frontier is computed on
65 + (score ↑, cost ↓) dominance.
66 + </p>
67 + </div>
68 + );
69 +}
modified apps/web/app/layout.tsx +3 −0
@@ -38,6 +38,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
38 38 <Link href="/" className="text-zinc-600 transition-colors hover:text-zinc-900">
39 39 Leaderboard
40 40 </Link>
41 + <Link href="/frontier" className="text-zinc-600 transition-colors hover:text-zinc-900">
42 + Frontier
43 + </Link>
41 44 <Link href="/methodology" className="text-zinc-600 transition-colors hover:text-zinc-900">
42 45 Methodology
43 46 </Link>
modified apps/web/components/EfficiencyLive.tsx +9 −1
@@ -48,7 +48,15 @@ export function EfficiencyLive({ initial }: { initial: EffPoint[] }) {
48 48
49 49 return (
50 50 <section className="space-y-3">
51 <h2 className="text-lg font-semibold text-zinc-900 sm:text-xl">Efficiency frontier</h2>
51 + <div className="flex flex-wrap items-baseline justify-between gap-2">
52 + <h2 className="text-lg font-semibold text-zinc-900 sm:text-xl">Efficiency frontier</h2>
53 + <Link
54 + href="/frontier"
55 + className="rounded-full border border-emerald-300 bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700 transition-colors hover:border-emerald-400"
56 + >
57 + Open the full interactive chart →
58 + </Link>
59 + </div>
52 60 <p className="text-sm text-zinc-500">
53 61 Score vs. measured cost per 1k items — refreshed live as models land. Frontier models are
54 62 not dominated on both axes; never collapsed into a single blended number.
added apps/web/components/FrontierChart.tsx +293 −0
@@ -0,0 +1,293 @@
1 +/**
2 + * llmindex.io — interactive live Pareto frontier chart (full-page, SVG)
3 + * Author: Simon-Pierre Boucher
4 + * Contact: contact@spboucher.ai
5 + * License: Proprietary — © Simon-Pierre Boucher, all rights reserved
6 + *
7 + * Form: scatter + Pareto step line (quality vs log-cost). Color encodes the
8 + * two-class identity frontier/dominated — palette #047857/#7c3aed validated
9 + * (lightness band, chroma, CVD ΔE 23.9 deutan, normal-vision ΔE 32, contrast).
10 + * Provider identity rides on labels/tooltips/logos, never on hue cycling.
11 + * CI whiskers on every point; frontier points carry direct labels; per-mark
12 + * hover tooltips with enlarged hit targets; polls fresh data every 15 s and
13 + * animates newly landed models in.
14 + */
15 +'use client';
16 +
17 +import { useEffect, useMemo, useRef, useState } from 'react';
18 +import { useRouter } from 'next/navigation';
19 +import { ProviderLogo } from './ProviderLogo';
20 +
21 +export interface FrontierPoint {
22 + model: string;
23 + name: string;
24 + provider: string;
25 + score: number;
26 + score_low: number;
27 + score_high: number;
28 + latency_p50: number | null;
29 + cost_per_1k_items: number;
30 + on_frontier: boolean;
31 +}
32 +
33 +const FRONTIER = '#047857';
34 +const DOMINATED = '#7c3aed';
35 +const INK = '#3f3f46';
36 +const INK_MUTED = '#a1a1aa';
37 +const GRID = '#e4e4e7';
38 +const POLL_MS = 15000;
39 +
40 +const W = 1280;
41 +const H = 720;
42 +const M = { top: 28, right: 48, bottom: 64, left: 72 };
43 +const IW = W - M.left - M.right;
44 +const IH = H - M.top - M.bottom;
45 +
46 +function fmtCost(v: number): string {
47 + if (v >= 100) return `$${Math.round(v)}`;
48 + if (v >= 1) return `$${v.toFixed(v >= 10 ? 0 : 1)}`;
49 + if (v >= 0.01) return `$${v.toFixed(2)}`;
50 + return `$${v.toFixed(3)}`;
51 +}
52 +
53 +export function FrontierChart({ initial }: { initial: FrontierPoint[] }) {
54 + const router = useRouter();
55 + const [points, setPoints] = useState<FrontierPoint[]>(initial);
56 + const [hover, setHover] = useState<FrontierPoint | null>(null);
57 + const [fresh, setFresh] = useState<Set<string>>(new Set());
58 + const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
59 + const knownRef = useRef<Set<string>>(new Set(initial.map((p) => p.model)));
60 +
61 + useEffect(() => {
62 + let cancelled = false;
63 + async function tick() {
64 + try {
65 + const res = await fetch('/api/v1/efficiency', { cache: 'no-store' });
66 + if (!res.ok || cancelled) return;
67 + const body = (await res.json()) as { points: FrontierPoint[] };
68 + if (!body.points) return;
69 + const newcomers = body.points.map((p) => p.model).filter((m) => !knownRef.current.has(m));
70 + if (newcomers.length > 0) {
71 + setFresh(new Set(newcomers));
72 + newcomers.forEach((m) => knownRef.current.add(m));
73 + setTimeout(() => setFresh(new Set()), 5000);
74 + }
75 + setPoints(body.points);
76 + setUpdatedAt(new Date());
77 + } catch {
78 + /* keep last state */
79 + }
80 + }
81 + const id = setInterval(tick, POLL_MS);
82 + tick();
83 + return () => {
84 + cancelled = true;
85 + clearInterval(id);
86 + };
87 + }, []);
88 +
89 + const view = useMemo(() => {
90 + if (points.length === 0) return null;
91 + const costs = points.map((p) => p.cost_per_1k_items).filter((c) => c > 0);
92 + const minC = Math.min(...costs);
93 + const maxC = Math.max(...costs);
94 + const lo = Math.floor(Math.log10(minC) - 0.15);
95 + const hi = Math.ceil(Math.log10(maxC) + 0.15);
96 + const yMin = Math.max(0, Math.floor((Math.min(...points.map((p) => p.score_low)) - 50) / 100) * 100);
97 + const yMax = Math.min(1000, Math.ceil((Math.max(...points.map((p) => p.score_high)) + 50) / 100) * 100);
98 +
99 + const x = (c: number): number => M.left + ((Math.log10(c) - lo) / (hi - lo)) * IW;
100 + const y = (s: number): number => M.top + (1 - (s - yMin) / (yMax - yMin)) * IH;
101 +
102 + const xTicks: Array<{ v: number; major: boolean }> = [];
103 + for (let d = lo; d <= hi; d++) {
104 + xTicks.push({ v: 10 ** d, major: true });
105 + for (const m of [2, 5]) {
106 + const v = m * 10 ** d;
107 + if (Math.log10(v) < hi) xTicks.push({ v, major: false });
108 + }
109 + }
110 + const yTicks: number[] = [];
111 + for (let s = yMin; s <= yMax; s += 100) yTicks.push(s);
112 +
113 + // Pareto step polyline (const-right steps in cost order).
114 + const front = points.filter((p) => p.on_frontier).sort((a, b) => a.cost_per_1k_items - b.cost_per_1k_items);
115 + let path = '';
116 + front.forEach((p, i) => {
117 + const px = x(p.cost_per_1k_items);
118 + const py = y(p.score);
119 + if (i === 0) path += `M ${px} ${py}`;
120 + else path += ` L ${px} ${y(front[i - 1]!.score)} L ${px} ${py}`;
121 + });
122 + if (front.length > 0) {
123 + path += ` L ${M.left + IW} ${y(front[front.length - 1]!.score)}`;
124 + }
125 +
126 + // Direct labels on frontier points, nudged apart vertically when close.
127 + const labels = front.map((p) => ({ p, lx: x(p.cost_per_1k_items) + 12, ly: y(p.score) - 10 }));
128 + for (let i = 1; i < labels.length; i++) {
129 + const prev = labels[i - 1]!;
130 + const cur = labels[i]!;
131 + if (Math.abs(cur.ly - prev.ly) < 16 && cur.lx - prev.lx < 130) cur.ly = prev.ly - 16;
132 + }
133 +
134 + return { x, y, xTicks, yTicks, path, labels, yMin, yMax };
135 + }, [points]);
136 +
137 + if (!view || points.length === 0) {
138 + return (
139 + <div className="flex h-[50vh] items-center justify-center rounded-2xl border border-zinc-200 bg-white text-sm text-zinc-500">
140 + Waiting for the first scored models — the chart populates automatically as the live
141 + benchmark lands them.
142 + </div>
143 + );
144 + }
145 +
146 + const shortName = (slug: string): string => slug.split('/')[1] ?? slug;
147 +
148 + return (
149 + <div className="space-y-3">
150 + {/* legend + live status — one row above the chart */}
151 + <div className="flex flex-wrap items-center gap-x-5 gap-y-2 text-sm">
152 + <span className="flex items-center gap-2">
153 + <span className="inline-block h-3 w-3 rounded-full" style={{ background: FRONTIER }} />
154 + <span className="text-zinc-700">Pareto frontier ({points.filter((p) => p.on_frontier).length})</span>
155 + </span>
156 + <span className="flex items-center gap-2">
157 + <span className="inline-block h-3 w-3 rounded-full" style={{ background: DOMINATED, opacity: 0.75 }} />
158 + <span className="text-zinc-700">dominated ({points.filter((p) => !p.on_frontier).length})</span>
159 + </span>
160 + <span className="flex items-center gap-2 text-zinc-500">
161 + <span className="inline-block h-3 w-[2px] bg-zinc-400" /> 95% CI
162 + </span>
163 + <span className="ml-auto flex items-center gap-2 text-xs text-zinc-500">
164 + <span className="relative flex h-2 w-2">
165 + <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-60" />
166 + <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-400" />
167 + </span>
168 + live · {points.length} models{updatedAt ? ` · updated ${updatedAt.toLocaleTimeString()}` : ''}
169 + </span>
170 + </div>
171 +
172 + <div className="relative overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm">
173 + <svg viewBox={`0 0 ${W} ${H}`} className="block w-full" role="img"
174 + aria-label="Pareto efficiency frontier: Global Index versus cost per thousand items">
175 + {/* grid */}
176 + {view.yTicks.map((s) => (
177 + <line key={`gy${s}`} x1={M.left} x2={M.left + IW} y1={view.y(s)} y2={view.y(s)}
178 + stroke={GRID} strokeWidth={1} />
179 + ))}
180 + {view.xTicks.filter((t) => t.major).map((t) => (
181 + <line key={`gx${t.v}`} x1={view.x(t.v)} x2={view.x(t.v)} y1={M.top} y2={M.top + IH}
182 + stroke={GRID} strokeWidth={1} />
183 + ))}
184 +
185 + {/* axes labels */}
186 + {view.yTicks.map((s) => (
187 + <text key={`ty${s}`} x={M.left - 10} y={view.y(s) + 4} textAnchor="end"
188 + fontSize={13} fill={INK_MUTED}>{s}</text>
189 + ))}
190 + {view.xTicks.map((t) => (
191 + <text key={`tx${t.v}`} x={view.x(t.v)} y={M.top + IH + (t.major ? 24 : 20)} textAnchor="middle"
192 + fontSize={t.major ? 13 : 10.5} fill={t.major ? INK_MUTED : '#c4c4cc'}>
193 + {fmtCost(t.v)}
194 + </text>
195 + ))}
196 + <text x={M.left + IW / 2} y={H - 14} textAnchor="middle" fontSize={14} fill={INK}>
197 + cost per 1,000 items (USD, log scale) →
198 + </text>
199 + <text x={20} y={M.top + IH / 2} textAnchor="middle" fontSize={14} fill={INK}
200 + transform={`rotate(-90 20 ${M.top + IH / 2})`}>
201 + Global Index →
202 + </text>
203 +
204 + {/* frontier step line */}
205 + <path d={view.path} fill="none" stroke={FRONTIER} strokeWidth={2} strokeLinejoin="round" opacity={0.85} />
206 +
207 + {/* CI whiskers */}
208 + {points.map((p) => {
209 + const px = view.x(p.cost_per_1k_items);
210 + const color = p.on_frontier ? FRONTIER : DOMINATED;
211 + return (
212 + <g key={`w${p.model}`} opacity={0.45}>
213 + <line x1={px} x2={px} y1={view.y(p.score_low)} y2={view.y(p.score_high)}
214 + stroke={color} strokeWidth={1.5} />
215 + <line x1={px - 4} x2={px + 4} y1={view.y(p.score_low)} y2={view.y(p.score_low)} stroke={color} strokeWidth={1.5} />
216 + <line x1={px - 4} x2={px + 4} y1={view.y(p.score_high)} y2={view.y(p.score_high)} stroke={color} strokeWidth={1.5} />
217 + </g>
218 + );
219 + })}
220 +
221 + {/* dots (2px surface ring separates overlapping marks) */}
222 + {points.map((p) => (
223 + <circle key={`d${p.model}`} cx={view.x(p.cost_per_1k_items)} cy={view.y(p.score)}
224 + r={p.on_frontier ? 7 : 5.5}
225 + fill={p.on_frontier ? FRONTIER : DOMINATED}
226 + fillOpacity={p.on_frontier ? 1 : 0.75}
227 + stroke="#ffffff" strokeWidth={2}
228 + className={fresh.has(p.model) ? 'animate-row-in' : undefined} />
229 + ))}
230 +
231 + {/* selective direct labels: frontier only */}
232 + {view.labels.map(({ p, lx, ly }) => (
233 + <text key={`l${p.model}`} x={lx} y={ly} fontSize={12.5} fontWeight={600} fill={INK}>
234 + {shortName(p.model)}
235 + </text>
236 + ))}
237 +
238 + {/* enlarged invisible hit targets */}
239 + {points.map((p) => (
240 + <circle key={`h${p.model}`} cx={view.x(p.cost_per_1k_items)} cy={view.y(p.score)} r={16}
241 + fill="transparent" style={{ cursor: 'pointer' }}
242 + onMouseEnter={() => setHover(p)} onMouseLeave={() => setHover(null)}
243 + onClick={() => router.push(`/models/${p.model}`)} />
244 + ))}
245 +
246 + {/* hover ring */}
247 + {hover && (
248 + <circle cx={view.x(hover.cost_per_1k_items)} cy={view.y(hover.score)} r={11}
249 + fill="none" stroke={hover.on_frontier ? FRONTIER : DOMINATED} strokeWidth={2} opacity={0.6} />
250 + )}
251 + </svg>
252 +
253 + {/* tooltip */}
254 + {hover && (
255 + <div
256 + className="pointer-events-none absolute z-10 w-64 rounded-xl border border-zinc-200 bg-white p-3 shadow-lg"
257 + style={{
258 + left: `min(max(${((view.x(hover.cost_per_1k_items) / W) * 100).toFixed(2)}% - 8rem, 0.5rem), calc(100% - 16.5rem))`,
259 + top: `calc(${((view.y(hover.score) / H) * 100).toFixed(2)}% - 0.5rem)`,
260 + transform: 'translateY(-100%)',
261 + }}
262 + >
263 + <div className="flex items-center gap-2">
264 + <ProviderLogo provider={hover.provider} size={18} />
265 + <span className="truncate text-sm font-semibold text-zinc-900">{hover.name}</span>
266 + </div>
267 + <div className="mt-2 grid grid-cols-2 gap-x-3 gap-y-1 text-xs text-zinc-600">
268 + <span>Global Index</span>
269 + <span className="text-right font-semibold tabular-nums text-zinc-900">
270 + {hover.score} <span className="font-normal text-zinc-500">[{hover.score_low}–{hover.score_high}]</span>
271 + </span>
272 + <span>Cost / 1k items</span>
273 + <span className="text-right tabular-nums">{fmtCost(hover.cost_per_1k_items)}</span>
274 + <span>Latency p50</span>
275 + <span className="text-right tabular-nums">
276 + {hover.latency_p50 != null ? `${(hover.latency_p50 / 1000).toFixed(1)}s` : '—'}
277 + </span>
278 + <span>Status</span>
279 + <span className="text-right">
280 + {hover.on_frontier ? (
281 + <span className="rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700">frontier</span>
282 + ) : (
283 + <span className="rounded-full bg-violet-100 px-2 py-0.5 text-violet-700">dominated</span>
284 + )}
285 + </span>
286 + </div>
287 + <p className="mt-2 text-[10px] text-zinc-400">click to open the model page</p>
288 + </div>
289 + )}
290 + </div>
291 + </div>
292 + );
293 +}
modified apps/web/lib/data.ts +45 −10
@@ -173,6 +173,10 @@ export async function getModelProfile(slug: string): Promise<ModelProfile | null
173 173
174 174 export interface EfficiencyPoint extends ParetoPoint {
175 175 name: string;
176 + provider: string;
177 + scoreLow: number;
178 + scoreHigh: number;
179 + latencyP50: number | null;
176 180 onFrontier: boolean;
177 181 }
178 182
@@ -288,29 +292,60 @@ export async function getEfficiencyData(): Promise<EfficiencyPoint[]> {
288 292 if (!run) return [];
289 293 const scores = await prisma.score.findMany({
290 294 where: { runId: run.id },
291 include: { model: { select: { slug: true, name: true } } },
295 + include: { model: { select: { slug: true, name: true, provider: true } } },
292 296 });
293 const byModel = new Map<string, { name: string; global: number | null; costs: number[] }>();
297 + interface Acc {
298 + name: string;
299 + provider: string;
300 + global: { score: number; low: number; high: number } | null;
301 + costs: number[];
302 + latencies: number[];
303 + }
304 + const byModel = new Map<string, Acc>();
294 305 for (const s of scores) {
295 const entry = byModel.get(s.model.slug) ?? { name: s.model.name, global: null, costs: [] };
296 if (s.domain === GLOBAL_DOMAIN) entry.global = s.score;
297 const cost = (s.subMetrics as Record<string, number> | null)?.cost_per_1k_items;
298 if (typeof cost === 'number') entry.costs.push(cost);
306 + const entry: Acc = byModel.get(s.model.slug) ?? {
307 + name: s.model.name,
308 + provider: s.model.provider,
309 + global: null,
310 + costs: [],
311 + latencies: [],
312 + };
313 + if (s.domain === GLOBAL_DOMAIN)
314 + entry.global = { score: s.score, low: s.scoreLow, high: s.scoreHigh };
315 + const sub = s.subMetrics as Record<string, number> | null;
316 + if (typeof sub?.cost_per_1k_items === 'number') entry.costs.push(sub.cost_per_1k_items);
317 + if (typeof sub?.latency_p50 === 'number') entry.latencies.push(sub.latency_p50);
299 318 byModel.set(s.model.slug, entry);
300 319 }
301 320 const points: ParetoPoint[] = [];
302 const names = new Map<string, string>();
321 + const meta = new Map<string, Acc>();
303 322 for (const [slug, e] of byModel) {
304 323 if (e.global === null || e.costs.length === 0) continue;
305 names.set(slug, e.name);
324 + meta.set(slug, e);
306 325 points.push({
307 326 slug,
308 score: e.global,
327 + score: e.global.score,
309 328 costPer1kItems: e.costs.reduce((a, b) => a + b, 0) / e.costs.length,
310 329 });
311 330 }
312 331 const frontier = new Set(paretoFrontier(points).map((p) => p.slug));
332 + const median = (arr: number[]): number | null => {
333 + if (!arr.length) return null;
334 + const s = [...arr].sort((a, b) => a - b);
335 + return s[Math.floor(s.length / 2)]!;
336 + };
313 337 return points
314 .map((p) => ({ ...p, name: names.get(p.slug) ?? p.slug, onFrontier: frontier.has(p.slug) }))
338 + .map((p) => {
339 + const e = meta.get(p.slug)!;
340 + return {
341 + ...p,
342 + name: e.name,
343 + provider: e.provider,
344 + scoreLow: e.global!.low,
345 + scoreHigh: e.global!.high,
346 + latencyP50: median(e.latencies),
347 + onFrontier: frontier.has(p.slug),
348 + };
349 + })
315 350 .sort((a, b) => a.costPer1kItems - b.costPer1kItems);
316 351 }
317 352