From 00d5889dfff071c979b40a1261ee3e1e8d7b9c52 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 04:16:47 +0000 Subject: [PATCH] Fix node extension price calculation showing ~10x actual cost Two bugs caused the multi-node extend confirmation to display ~10x the correct price: 1. The quote API was called with quantity=8 (GPUs per node) instead of quantity=1 (one node per quote). The API's quantity parameter means 'number of nodes', not GPUs. This inflated each quote's total price by 8x. 2. The multi-node total was computed by summing raw quote prices, which include the full quoted duration. Because the quote request adds flexibility (up to +1 hour), a 4-hour request could yield 5-hour quotes, adding another ~1.25x. Combined: 8 * 1.25 = 10x. Fix: - Change quantity from 8 to 1 in the per-node getQuote call - Normalize multi-node total using per-node-hour rates (via getPricePerGpuHourFromQuote) multiplied by the actual requested duration, matching the single-node code path Co-authored-by: Daniel Tao --- src/helpers/test/quote.test.ts | 113 +++++++++++++++++++++++++++++++++ src/lib/nodes/extend.ts | 14 ++-- 2 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 src/helpers/test/quote.test.ts diff --git a/src/helpers/test/quote.test.ts b/src/helpers/test/quote.test.ts new file mode 100644 index 0000000..28aa8dd --- /dev/null +++ b/src/helpers/test/quote.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest"; +import { getPricePerGpuHourFromQuote } from "../quote.ts"; +import { GPUS_PER_NODE } from "../../lib/constants.ts"; + +function makeQuote(opts: { + priceCents: number; + quantity: number; + durationHours: number; +}) { + const start = new Date("2025-06-01T00:00:00Z"); + const end = new Date( + start.getTime() + opts.durationHours * 3600 * 1000, + ); + return { + price: opts.priceCents, + quantity: opts.quantity, + start_at: start.toISOString(), + end_at: end.toISOString(), + }; +} + +function pricePerNodeHourFromQuote( + quote: ReturnType, +): number { + const pricePerGpuHour = getPricePerGpuHourFromQuote(quote); + return (pricePerGpuHour * GPUS_PER_NODE) / 100; +} + +describe("getPricePerGpuHourFromQuote", () => { + it("returns correct per-GPU-hour price for a single node", () => { + // 1 node, 4 hours, $12/node/hr = $48 total = 4800 cents + const quote = makeQuote({ priceCents: 4800, quantity: 1, durationHours: 4 }); + const pricePerNodeHour = pricePerNodeHourFromQuote(quote); + expect(pricePerNodeHour).toBeCloseTo(12.0); + }); + + it("normalizes correctly regardless of quantity in quote", () => { + // 8 nodes, 4 hours, $12/node/hr = $384 total = 38400 cents + const quote = makeQuote({ priceCents: 38400, quantity: 8, durationHours: 4 }); + const pricePerNodeHour = pricePerNodeHourFromQuote(quote); + expect(pricePerNodeHour).toBeCloseTo(12.0); + }); +}); + +describe("multi-node total price calculation (extend confirmation)", () => { + it("computes correct total using per-node-hour rates", () => { + // Simulates extending 16 nodes for 4 hours at $12/node/hr + // Each quote is for 1 node (quantity: 1) + const requestedDurationHours = 4; + const nodeCount = 16; + + const quotes = Array.from({ length: nodeCount }, () => + makeQuote({ priceCents: 4800, quantity: 1, durationHours: 4 }), + ); + + const totalPricePerHour = quotes.reduce((acc, quote) => { + const pricePerGpuHour = getPricePerGpuHourFromQuote(quote); + const pricePerNodeHour = (pricePerGpuHour * GPUS_PER_NODE) / 100; + return acc + pricePerNodeHour; + }, 0); + const totalEstimate = totalPricePerHour * requestedDurationHours; + + // 16 nodes * $12/hr * 4 hours = $768 + expect(totalEstimate).toBeCloseTo(768); + }); + + it("handles quotes with longer duration than requested without overestimating", () => { + // Quote returned for 5 hours (due to flexibility), but we only want 4 hours + const requestedDurationHours = 4; + const nodeCount = 16; + + // 1 node, 5 hours, $12/node/hr = $60 total = 6000 cents + const quotes = Array.from({ length: nodeCount }, () => + makeQuote({ priceCents: 6000, quantity: 1, durationHours: 5 }), + ); + + const totalPricePerHour = quotes.reduce((acc, quote) => { + const pricePerGpuHour = getPricePerGpuHourFromQuote(quote); + const pricePerNodeHour = (pricePerGpuHour * GPUS_PER_NODE) / 100; + return acc + pricePerNodeHour; + }, 0); + const totalEstimate = totalPricePerHour * requestedDurationHours; + + // Rate is $12/hr, so 16 * 4 * 12 = $768 (not $960) + expect(totalEstimate).toBeCloseTo(768); + }); + + it("OLD BUG: raw price sum with quantity=8 would have been 8x too high", () => { + // This test demonstrates the old bug: + // Each quote was requested with quantity=8 (nodes) instead of 1, + // and the total was computed as raw sum of prices / 100 + const nodeCount = 16; + + // 8 nodes, 4 hours, $12/node/hr = $384 total = 38400 cents per quote + const quotes = Array.from({ length: nodeCount }, () => + makeQuote({ priceCents: 38400, quantity: 8, durationHours: 4 }), + ); + + // Old calculation: sum raw prices / 100 + const oldTotal = quotes.reduce((acc, q) => acc + q.price, 0) / 100; + // This gave $6,144 (8x the correct $768) + expect(oldTotal).toBeCloseTo(6144); + + // With 5-hour quotes (duration flexibility), it would have been ~10x + const quotesWithFlexDuration = Array.from({ length: nodeCount }, () => + makeQuote({ priceCents: 48000, quantity: 8, durationHours: 5 }), + ); + const oldTotalFlex = + quotesWithFlexDuration.reduce((acc, q) => acc + q.price, 0) / 100; + // $7,680 - exactly matching the user's reported bug + expect(oldTotalFlex).toBeCloseTo(7680); + }); +}); diff --git a/src/lib/nodes/extend.ts b/src/lib/nodes/extend.ts index 780d8a3..882e667 100644 --- a/src/lib/nodes/extend.ts +++ b/src/lib/nodes/extend.ts @@ -195,7 +195,7 @@ async function extendNodeAction( extendableNodes.map(async ({ node }) => { return await getQuote({ instanceType: `${node.gpu_type.toLowerCase()}v` as const, - quantity: 8, + quantity: 1, minStartTime: node.end_at ? new Date(node.end_at * 1000) : "NOW", maxStartTime: node.end_at ? new Date(node.end_at * 1000) : "NOW", minDurationSeconds: minDurationSeconds, @@ -223,11 +223,15 @@ async function extendNodeAction( const pricePerNodeHour = (pricePerGpuHour * GPUS_PER_NODE) / 100; confirmationMessage += ` for ~$${pricePerNodeHour.toFixed(2)}/node/hr`; } else if (filteredQuotes.length > 1) { - const totalPrice = filteredQuotes.reduce((acc, quote) => { - return acc + (quote.value?.price ?? 0); + const durationHours = options.duration! / 3600; + const totalPricePerHour = filteredQuotes.reduce((acc, quote) => { + if (!quote.value) return acc; + const pricePerGpuHour = getPricePerGpuHourFromQuote(quote.value); + const pricePerNodeHour = (pricePerGpuHour * GPUS_PER_NODE) / 100; + return acc + pricePerNodeHour; }, 0); - // If there's multiple nodes, show the total price, as nodes could be on different zones or have different hardware - confirmationMessage += ` for ~$${totalPrice / 100}`; + const totalEstimate = totalPricePerHour * durationHours; + confirmationMessage += ` for ~$${totalEstimate.toFixed(0)}`; } else { confirmationMessage = chalk.red( "No nodes available matching your requirements. This is likely due to insufficient capacity. Attempt to extend anyway",