Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/app/api/cost/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { queryDatabricks } from "@/lib/databricks";
import { getQueueCost } from "@/lib/queue-costs";
import { computePricingCoverage, getQueueCost } from "@/lib/queue-costs";
import { getCached, setCache } from "@/lib/api-cache";
import { cachedJson } from "@/lib/api-response";
import { resolveCiDataSource } from "@/lib/ci-data-source";
Expand Down Expand Up @@ -138,6 +138,7 @@ export async function GET(request: NextRequest) {
total_hours: totalHours,
instance_type: pricing?.instanceType ?? null,
cost_per_hour: pricing?.costPerHour ?? null,
estimated: pricing?.estimated ?? false,
total_cost: pricing ? Math.round(totalHours * pricing.costPerHour * 100) / 100 : null,
};
});
Expand Down Expand Up @@ -207,11 +208,17 @@ export async function GET(request: NextRequest) {
}))
.sort((a, b) => b.total_cost - a.total_cost);

const coverage = computePricingCoverage(
queueWithCost.map((q) => ({ queue: q.queue as string, total_hours: q.total_hours })),
);

const result = {
byQueue: queueWithCost,
dailyCostByQueue,
byBuild,
byJob,
pricedHoursShare: coverage.pricedHoursShare,
estimatedHoursShare: coverage.estimatedHoursShare,
};
setCache(cacheKey, result, TTL);

Expand Down
52 changes: 47 additions & 5 deletions src/app/cost/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface QueueCostRow {
total_hours: number;
instance_type: string | null;
cost_per_hour: number | null;
estimated: boolean;
total_cost: number | null;
}

Expand Down Expand Up @@ -82,6 +83,8 @@ interface CostResponse {
dailyCostByQueue: DailyCostByQueueRow[];
byBuild: BuildCostRow[];
byJob: JobCostRow[];
pricedHoursShare?: number;
estimatedHoursShare?: number;
error?: string;
}

Expand Down Expand Up @@ -254,7 +257,13 @@ export default function CostPage() {
const totalCost = byQueue.reduce((s, q) => s + (q.total_cost ?? 0), 0);
const totalHours = byQueue.reduce((s, q) => s + q.total_hours, 0);
const totalJobs = byQueue.reduce((s, q) => s + parseInt(q.total_jobs, 10), 0);
const unknownCostQueues = byQueue.filter((q) => q.total_cost === null).length;
const unpricedQueues = byQueue.filter((q) => q.total_cost === null);
const pricedHours = byQueue.reduce(
(s, q) => s + (q.total_cost !== null && !q.estimated ? q.total_hours : 0), 0
);
const pricedShare =
data?.pricedHoursShare ?? (totalHours > 0 ? pricedHours / totalHours : 0);
const estShare = data?.estimatedHoursShare ?? 0;
const avgDailyCost = dayCount > 0 ? totalCost / dayCount : 0;

// Build pagination
Expand Down Expand Up @@ -297,11 +306,40 @@ export default function CostPage() {

{/* Stat cards */}
<div className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-5">
<StatCard className="col-span-2 sm:col-span-1" label="Total Cost" value={`$${totalCost.toFixed(0)}`} detail="Known queues only" />
<StatCard
className="col-span-2 sm:col-span-1"
label="Total Cost"
value={`$${totalCost.toFixed(0)}`}
detail={
`Covers ${Math.round(pricedShare * 100)}% of compute hours` +
(estShare > 0 ? ` (+${Math.round(estShare * 100)}% est.)` : "")
}
/>
<StatCard label="Avg Daily Cost" value={`$${avgDailyCost.toFixed(0)}`} />
<StatCard label="Compute Hours" value={`${totalHours.toFixed(0)}`} />
<StatCard label="Total Jobs" value={totalJobs} />
<StatCard label="Unpriced Queues" value={unknownCostQueues} detail="No cost data" />
{unpricedQueues.length > 0 ? (
<details className="rounded-lg border border-zinc-200 bg-white p-4 sm:p-5 dark:border-zinc-800 dark:bg-zinc-950">
<summary className="cursor-pointer list-none">
<p className="text-xs font-medium text-zinc-500 sm:text-sm dark:text-zinc-400">
Unpriced Queues
</p>
<p className="mt-1 text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl dark:text-zinc-100">
{unpricedQueues.length}
</p>
<p className="mt-1 text-xs leading-5 text-zinc-500 sm:text-sm dark:text-zinc-400">
No cost data · click to list
</p>
</summary>
<ul className="mt-2 space-y-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{unpricedQueues.map((q) => (
<li key={q.queue} className="font-mono">{q.queue}</li>
))}
</ul>
</details>
) : (
<StatCard label="Unpriced Queues" value={0} detail="All queues priced" />
)}
</div>

{/* Tab bar */}
Expand Down Expand Up @@ -449,12 +487,16 @@ export default function CostPage() {
<td className="px-5 py-2.5 font-medium">{q.queue}</td>
<td className="px-5 py-2.5 font-mono text-xs text-zinc-500">{q.instance_type ?? "\u2014"}</td>
<td className="px-5 py-2.5 text-zinc-600 dark:text-zinc-400">
{q.cost_per_hour != null ? `$${q.cost_per_hour.toFixed(2)}` : "\u2014"}
{q.cost_per_hour != null
? `$${q.cost_per_hour.toFixed(2)}${q.estimated ? " est." : ""}`
: "\u2014"}
</td>
<td className="px-5 py-2.5 tabular-nums">{q.total_hours.toFixed(1)}</td>
<td className="px-5 py-2.5 font-medium tabular-nums">
{q.total_cost != null ? (
<span className="text-purple-600 dark:text-purple-400">${q.total_cost.toFixed(2)}</span>
<span className="text-purple-600 dark:text-purple-400">
${q.total_cost.toFixed(2)}{q.estimated ? " est." : ""}
</span>
) : (
<span className="text-zinc-400">{"\u2014"}</span>
)}
Expand Down
57 changes: 57 additions & 0 deletions src/lib/queue-costs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
computePricingCoverage,
getQueueCost,
QUEUE_COSTS,
} from "./queue-costs";

test("getQueueCost returns known and estimated pricing", () => {
assert.deepEqual(getQueueCost("gpu_1_queue"), {
instanceType: "g6.4xlarge",
costPerHour: 1.3232,
});
const h200 = getQueueCost("h200_35gb");
assert.equal(h200?.estimated, true);
assert.ok(h200?.costPerHour && h200.costPerHour > 0);
assert.ok(h200?.source, "estimated rates must document their source");
assert.equal(getQueueCost("definitely-not-a-queue"), null);
});

test("every estimated rate documents a source", () => {
for (const [queue, pricing] of Object.entries(QUEUE_COSTS)) {
assert.ok(pricing.costPerHour > 0, `${queue} has a positive rate`);
if (pricing.estimated) {
assert.ok(pricing.source, `${queue} is estimated but has no source`);
}
}
});

test("the largest unpriced queues are now covered", () => {
// The queues that made the Cost page total misleading (see #cost-priced-coverage).
for (const queue of ["h200_35gb", "amd_mi300_1", "amd_mi355_1", "b200-k8s"]) {
assert.ok(getQueueCost(queue), `${queue} should be priced`);
}
});

test("computePricingCoverage splits priced, estimated, and unpriced hours", () => {
const coverage = computePricingCoverage([
{ queue: "gpu_1_queue", total_hours: 10 }, // confirmed rate
{ queue: "h200_35gb", total_hours: 30 }, // estimated rate
{ queue: "mystery_queue", total_hours: 60 }, // unpriced
]);
assert.equal(coverage.totalHours, 100);
assert.equal(coverage.pricedHours, 10);
assert.equal(coverage.estimatedHours, 30);
assert.equal(coverage.unpricedHours, 60);
assert.equal(coverage.pricedHoursShare, 0.1);
assert.equal(coverage.estimatedHoursShare, 0.3);
});

test("computePricingCoverage handles empty input", () => {
const coverage = computePricingCoverage([]);
assert.equal(coverage.totalHours, 0);
assert.equal(coverage.pricedHoursShare, 0);
assert.equal(coverage.estimatedHoursShare, 0);
});
174 changes: 171 additions & 3 deletions src/lib/queue-costs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
// AWS on-demand pricing per hour (us-west-2) by queue name.
// Only queues with known instance types are listed.
// Others will show compute hours but no dollar cost.
// Queues with confirmed instance types carry exact rates.
// Self-hosted / partner hardware (AMD, H200 slices, TPU, Intel, ...) has no
// AWS price; where a defensible market rate exists we add it with
// `estimated: true` and a source comment. Queues with neither stay unpriced
// and are surfaced by the "Unpriced Queues" card on the Cost page.

export interface QueuePricing {
instanceType: string;
costPerHour: number;
/** True when the rate is an estimate rather than a confirmed instance price. */
estimated?: boolean;
/** Human-readable basis for an estimated rate (shown in the UI/docs). */
source?: string;
}

export const QUEUE_COSTS: Record<string, QueuePricing> = {
Expand All @@ -21,15 +28,176 @@ export const QUEUE_COSTS: Record<string, QueuePricing> = {
small_cpu_queue_premerge: { instanceType: "r6in.large", costPerHour: 0.1743 },
small_cpu_queue_postmerge: { instanceType: "r6in.large", costPerHour: 0.1743 },
small_cpu_queue_release: { instanceType: "r6in.large", costPerHour: 0.1743 },
// Assumed midpoint between small_cpu (r6in.large) and cpu (r6in.16xlarge).
medium_cpu_queue_premerge: {
instanceType: "r6in.4xlarge",
costPerHour: 1.3948,
estimated: true,
source: "AWS on-demand r6in.4xlarge, us-west-2 (assumed instance size)",
},

// H200 queues
// H200 queues. h200_18gb is a known fractional-GPU slice at $0.30/hr.
h200_18gb: { instanceType: "h200_18gb", costPerHour: 0.30 },
// Scaled from the known h200_18gb slice price ($0.30/hr * 35/18 GB).
h200_35gb: {
instanceType: "h200_35gb",
costPerHour: 0.5833,
estimated: true,
source: "Scaled from h200_18gb slice ($0.30/hr * 35/18)",
},
// Full H200 GPU: 8 x the $0.30/hr 18GB slice (141GB HBM3e / 18GB slice).
H200: {
instanceType: "H200 SXM",
costPerHour: 2.4,
estimated: true,
source: "8x h200_18gb slice price ($0.30/hr)",
},

// ARM64 queues — r7g Graviton instances
arm64_cpu_queue_postmerge: { instanceType: "r7g.16xlarge", costPerHour: 4.3546 },
arm64_cpu_queue_release: { instanceType: "r7g.16xlarge", costPerHour: 4.3546 },
// Same AWS ARM64 pool as the postmerge/release queues.
arm64_cpu_queue_premerge: {
instanceType: "r7g.16xlarge",
costPerHour: 4.3546,
estimated: true,
source: "Assumed same instance as arm64_cpu_queue_postmerge",
},

// AMD queues — partner-hosted AMD GPUs. Queue suffix is GPUs per agent.
// Market rental rates per GPU/hr (TensorWave / Lambda / RunPod list, 2026):
// MI300X ~$2.50, MI355X ~$3.00, MI250 ~$1.80.
amd_mi300_1: {
instanceType: "MI300X x1",
costPerHour: 2.5,
estimated: true,
source: "MI300X market rental ~$2.50/GPU/hr",
},
amd_mi300_2: {
instanceType: "MI300X x2",
costPerHour: 5.0,
estimated: true,
source: "MI300X market rental ~$2.50/GPU/hr",
},
amd_mi300_4: {
instanceType: "MI300X x4",
costPerHour: 10.0,
estimated: true,
source: "MI300X market rental ~$2.50/GPU/hr",
},
amd_mi300_8: {
instanceType: "MI300X x8",
costPerHour: 20.0,
estimated: true,
source: "MI300X market rental ~$2.50/GPU/hr",
},
amd_mi355_1: {
instanceType: "MI355X x1",
costPerHour: 3.0,
estimated: true,
source: "MI355X market rental ~$3.00/GPU/hr",
},
amd_mi355_2: {
instanceType: "MI355X x2",
costPerHour: 6.0,
estimated: true,
source: "MI355X market rental ~$3.00/GPU/hr",
},
amd_mi355_4: {
instanceType: "MI355X x4",
costPerHour: 12.0,
estimated: true,
source: "MI355X market rental ~$3.00/GPU/hr",
},
amd_mi250_1: {
instanceType: "MI250 x1",
costPerHour: 1.8,
estimated: true,
source: "MI250 market rental ~$1.80/GPU/hr",
},
amd_mi250_2: {
instanceType: "MI250 x2",
costPerHour: 3.6,
estimated: true,
source: "MI250 market rental ~$1.80/GPU/hr",
},
amd_mi250_4: {
instanceType: "MI250 x4",
costPerHour: 7.2,
estimated: true,
source: "MI250 market rental ~$1.80/GPU/hr",
},

// Other accelerator pools — per-GPU/hr market rental estimates (2026).
"mithril-h100-pool": {
instanceType: "H100",
costPerHour: 2.5,
estimated: true,
source: "H100 market rental ~$2.50/GPU/hr (Mithril cloud)",
},
"b200-k8s": {
instanceType: "B200",
costPerHour: 5.0,
estimated: true,
source: "B200 market rental ~$5.00/GPU/hr",
},
"l4-k8s": {
instanceType: "L4",
costPerHour: 0.8,
estimated: true,
source: "L4 market rental ~$0.80/GPU/hr",
},
a100_queue: {
instanceType: "A100 80GB",
costPerHour: 2.25,
estimated: true,
source: "A100 80GB market rental ~$2.25/GPU/hr",
},
gh200_queue: {
instanceType: "GH200",
costPerHour: 3.75,
estimated: true,
source: "GH200 market rental ~$3.75/GPU/hr",
},
};

export function getQueueCost(queue: string): QueuePricing | null {
return QUEUE_COSTS[queue] ?? null;
}

export interface PricingCoverage {
totalHours: number;
/** Hours on queues with confirmed (non-estimated) rates. */
pricedHours: number;
/** Hours on queues with estimated rates. */
estimatedHours: number;
/** Hours on queues with no rate at all. */
unpricedHours: number;
/** pricedHours / totalHours, 0-1 (0 when there are no hours). */
pricedHoursShare: number;
/** estimatedHours / totalHours, 0-1 (0 when there are no hours). */
estimatedHoursShare: number;
}

export function computePricingCoverage(
rows: { queue: string; total_hours: number }[],
): PricingCoverage {
let totalHours = 0;
let pricedHours = 0;
let estimatedHours = 0;
for (const row of rows) {
const pricing = getQueueCost(row.queue);
totalHours += row.total_hours;
if (!pricing) continue;
if (pricing.estimated) estimatedHours += row.total_hours;
else pricedHours += row.total_hours;
}
return {
totalHours,
pricedHours,
estimatedHours,
unpricedHours: totalHours - pricedHours - estimatedHours,
pricedHoursShare: totalHours > 0 ? pricedHours / totalHours : 0,
estimatedHoursShare: totalHours > 0 ? estimatedHours / totalHours : 0,
};
}