Skip to content
Merged
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
10 changes: 10 additions & 0 deletions tools/cost_calculator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ The GPT Live meter is hourly and is divided by 60 for USD-per-minute pricing.
meter references**, not
direct OpenAI model API prices. GPT Realtime 2.1 uses the Pro text-output-2
meter; `gpt-realtime` uses the original output meter.
- **Azure Realtime** uses the documented Voice Live **Pro** tier.
**Azure Realtime Fast** uses a **user-specified Lite-tier reference**,
not a verified published Fast tariff. Both use the existing Central India
tier text and native-audio meters, with no separate STT, Azure HD, or GPT Live
session charge. They appear in managed native speech-to-speech selection,
comparison, and CSV export only; no BYOM or alternate-pipeline prices are
inferred from GPT models. Fast's assumption is labeled in the UI and CSV.
The editable audio tokens/second remain planning assumptions, not verified
Azure Realtime-family tokenization. No price-snapshot refresh is required
for these existing tier meters.
- When no managed tier is published, use the exact model's **AOAI Global
inference prices + Central India BYO speech** as an economic reference.
This fills GPT-5.4 mini/nano, GPT OSS 120B, and GPT-6 Sol reference scenarios.
Expand Down
22 changes: 16 additions & 6 deletions tools/cost_calculator/app.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DEFAULTS, PROMPTS, MODELS, GPT_LIVE_METER, getRates, calculate } from "./calculator.mjs";
import { DEFAULTS, PROMPTS, MODELS, GPT_LIVE_METER, supportsScenario, getRates, calculate } from "./calculator.mjs";

const $ = id => document.getElementById(id);
const form = $("assumptions");
Expand Down Expand Up @@ -51,7 +51,7 @@ function modelRates(model, pipeline) {

function populateModels(preferred) {
const pipeline = $("pipeline").value;
const available = MODELS.filter(m => ["native", "hybrid"].includes(pipeline) ? m.realtime : !m.realtime);
const available = MODELS.filter(m => supportsScenario(m, effectiveHosting(pipeline), pipeline));
$("model").replaceChildren(...available.map(m => {
const option = element("option", m.name);
option.value = m.id;
Expand Down Expand Up @@ -111,6 +111,8 @@ function rateEditor(model, pipeline, book) {
}
if (pipeline === "live") $("price-note").textContent += " GPT Live uses Azure OpenAI's South India Global voice rate: $3/hour = $0.05/minute in this snapshot. Backend model usage is charged separately. GPT Live generates its own audio, so no Azure HD charge is added.";
if (model.oss && (hosting === "byom" || book.usesGlobalReference)) $("price-note").textContent += " No OSS cached-input meter was found; reused text is conservatively charged at full input price.";
if (model.managedNativeOnly) $("price-note").textContent += " Azure Realtime models use native audio only in this calculator; no separate Azure STT, HD voice, or GPT Live session charge is added. Audio token rates remain editable assumptions, not verified Azure Realtime tokenization.";
if (model.tierAssumption) $("price-note").textContent += " Azure Realtime Fast uses the user-specified Voice Live Lite tier reference, not a verified published Fast tariff.";
}

function renderOverview(a, model, pipeline, result, book) {
Expand All @@ -121,6 +123,7 @@ function renderOverview(a, model, pipeline, result, book) {
$("volume-note").textContent = `${a.calls.toLocaleString("en-US")} calls / ${(a.calls * a.duration / 60).toLocaleString("en-US")} minutes`;
const badge = $("estimate-badge");
badge.textContent = result.list === null ? "Missing rates" :
model.tierAssumption ? "Lite-tier assumption" :
book.usesGlobalReference ? "AOAI Global reference" : "List-price estimate";
badge.className = `badge${result.list === null ? "" : " good"}`;
$("summary").textContent = result.missing.length ?
Expand All @@ -131,6 +134,9 @@ function renderOverview(a, model, pipeline, result, book) {
book.usesGlobalReference ?
"Managed tariff unavailable: showing an AOAI Global model-price + Central India BYO speech reference instead. This does not establish managed model support." :
`Economic scenario only. ${$("hosting").value === "managed" ? `Central India managed-tier reference: ${model.tier || "unverified"}.` : "Model rates: South India Global. STT: Central India BYO reference."} Azure HD: Central India. Confirm model and transport availability.`;
if (model.managedNativeOnly) {
$("model-note").textContent = `${model.tierAssumption ? "User-specified Lite-tier reference, not a verified published Azure Realtime Fast tariff." : "Central India Voice Live Pro tier reference."} Native audio only; no separate STT/HD charge. Confirm regional availability and actual audio token rates.`;
}
}

function renderChart(a, pipeline, rates) {
Expand Down Expand Up @@ -163,7 +169,7 @@ function scenarios() {
if (!defs.some(s => s.model.id === currentModel.id && s.pipeline === currentPipeline)) {
defs.unshift({ model: currentModel, pipeline: currentPipeline });
}
return defs;
return defs.filter(({ model, pipeline }) => supportsScenario(model, effectiveHosting(pipeline), pipeline));
}

function renderComparison(a) {
Expand All @@ -177,7 +183,7 @@ function renderComparison(a) {
const liveMeter = pipeline === "live" ? snapshot.meters.find(m => m.meterName === GPT_LIVE_METER) : null;
const row = element("tr");
const title = element("td", model.name);
title.append(element("small", `${pipelines[pipeline]} / ${usesGlobalReference ? "AOAI GLOBAL REFERENCE (managed tariff unavailable)" : effectiveHosting(pipeline).toUpperCase()}${modelMeter?.armRegionName === "southindia" ? " / model: South India Global reference" : ""}${liveMeter ? " / voice: South India Global reference" : ""}`));
title.append(element("small", `${pipelines[pipeline]} / ${model.tierAssumption ? "USER-SPECIFIED LITE TIER REFERENCE" : usesGlobalReference ? "AOAI GLOBAL REFERENCE (managed tariff unavailable)" : effectiveHosting(pipeline).toUpperCase()}${modelMeter?.armRegionName === "southindia" ? " / model: South India Global reference" : ""}${liveMeter ? " / voice: South India Global reference" : ""}`));
row.append(title);
for (const prompt of PROMPTS) {
const result = calculate({ ...a, prompt }, pipeline, rates);
Expand All @@ -187,7 +193,8 @@ function renderComparison(a) {
row.append(cell);
exportedRows.push({
model: model.id, pipeline, hosting: effectiveHosting(pipeline), region: snapshot.region,
pricing_basis: usesGlobalReference ? "aoai-global-model-plus-byo-speech-reference" :
pricing_basis: model.tierAssumption ? "user-specified-voice-live-lite-reference" :
usesGlobalReference ? "aoai-global-model-plus-byo-speech-reference" :
effectiveHosting(pipeline) === "managed" ? "voice-live-tier-reference" : "byom",
prompt_tokens: prompt, silence_percent: a.silencePercent, list_per_call: result.list,
list_per_minute: result.listPerMinute, missing_list_rates: result.missing.join("; "),
Expand Down Expand Up @@ -251,7 +258,10 @@ form.addEventListener("input", event => {
if (event.target.tagName === "SELECT") return;
update();
});
$("hosting").addEventListener("change", () => update(true));
$("hosting").addEventListener("change", () => {
populateModels($("model").value);
update(true);
});
$("pipeline").addEventListener("change", () => {
populateModels($("model").value);
update(true);
Expand Down
11 changes: 11 additions & 0 deletions tools/cost_calculator/calculator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const MODELS = [
{ id: "gpt-realtime-2.1-mini", name: "GPT Realtime 2.1 mini", tier: "Std", realtime: true },
{ id: "gpt-realtime", name: "GPT Realtime 1.0 (gpt-realtime)", tier: "Pro", realtime: true },
{ id: "gpt-realtime-1.5", name: "GPT Realtime 1.5", tier: "Pro", realtime: true },
{ id: "azure-realtime", name: "Azure Realtime", tier: "Pro", realtime: true, managedNativeOnly: true },
{ id: "azure-realtime-fast", name: "Azure Realtime Fast", tier: "Lite", realtime: true,
managedNativeOnly: true, tierAssumption: true },
{ id: "gpt-4.1", name: "GPT-4.1", tier: "Pro" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 mini", tier: "Std" },
{ id: "gpt-4.1-nano", name: "GPT-4.1 nano", tier: "Lite" },
Expand All @@ -27,6 +30,11 @@ export const MODELS = [
{ id: "gpt-6-sol", name: "GPT-6 Sol" },
];

export function supportsScenario(model, hosting, pipeline) {
if (model.managedNativeOnly) return hosting === "managed" && pipeline === "native";
return ["native", "hybrid"].includes(pipeline) ? Boolean(model.realtime) : !model.realtime;
}

export function validateAssumptions(a) {
for (const key of Object.keys(DEFAULTS)) {
if (typeof a[key] !== "number" || !Number.isFinite(a[key]) || a[key] < 0) {
Expand Down Expand Up @@ -56,6 +64,9 @@ export function meterRate(snapshot, name, unit = "tokens") {
}

export function getRates(snapshot, model, hosting, pipeline = "cascade") {
if (model.managedNativeOnly && !supportsScenario(model, hosting, pipeline)) {
throw new Error(`${model.name} uses a managed native-audio tier reference; no BYOM or alternative-pipeline rates are verified.`);
}
const rates = { textInput: null, textCached: null, textOutput: null,
audioInput: null, audioCached: null, audioOutput: null, speechInput: null, speechOutput: null,
liveMinute: null };
Expand Down
57 changes: 53 additions & 4 deletions tools/cost_calculator/calculator.test.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { DEFAULTS, MODELS, PROMPTS, GPT_LIVE_METER, getRates, calculate, meterRate } from "./calculator.mjs";
import { DEFAULTS, MODELS, PROMPTS, GPT_LIVE_METER, supportsScenario, getRates, calculate, meterRate } from "./calculator.mjs";

const snapshot = JSON.parse((await readFile(new URL("./prices.json", import.meta.url), "utf8")).replace(/^\uFEFF/, ""));
const model = id => MODELS.find(m => m.id === id);
const book = (id, hosting = "managed", pipeline = "cascade") => getRates(snapshot, model(id), hosting, pipeline).rates;
const book = (id, hosting = "managed", pipeline = model(id).realtime ? "native" : "cascade") =>
getRates(snapshot, model(id), hosting, pipeline).rates;
const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-12, `${actual} != ${expected}`);

test("normalizes Azure 1K and 1M meters, retains exact rates", () => {
Expand All @@ -21,14 +22,60 @@ test("normalizes Azure 1K and 1M meters, retains exact rates", () => {
assert.throws(() => meterRate({ meters: [{ meterName: "bad", unitOfMeasure: "Hour", retailPrice: 1 }] }, "bad"), /Unsupported/);
});

test("every known model/hosting combination resolves without a missing meter error", () => {
for (const m of MODELS) for (const hosting of ["managed", "byom"]) getRates(snapshot, m, hosting);
test("every supported model/hosting/pipeline combination resolves without a missing meter error", () => {
for (const m of MODELS) for (const hosting of ["managed", "byom"]) {
for (const pipeline of ["native", "hybrid", "cascade", "live"]) {
if (supportsScenario(m, hosting, pipeline)) getRates(snapshot, m, hosting, pipeline);
}
}
});

test("Azure Realtime uses Pro and Fast uses the explicit Lite assumption with native meters", () => {
for (const [id, tier, expected] of [
["azure-realtime", "Pro", [4, 0.4, 16, 32, 0.4, 64]],
["azure-realtime-fast", "Lite", [0.11, 0.04, 0.44, 4, 0.04, 12]],
]) {
const m = model(id);
assert.equal(m.tier, tier);
assert.equal(Boolean(m.tierAssumption), id === "azure-realtime-fast");
const { rates, sources, usesGlobalReference } = getRates(snapshot, m, "managed", "native");
assert.equal(usesGlobalReference, false);
const keys = ["textInput", "textCached", "textOutput", "audioInput", "audioCached", "audioOutput"];
keys.forEach((key, index) => {
close(rates[key], expected[index]);
assert.equal(sources[key].armRegionName, "centralindia");
assert.ok(sources[key].meterName.startsWith(`Voice Live API ${tier} - LLM `));
});
const result = calculate({ ...DEFAULTS, nativeText: 150 }, "native", rates);
const [textInput, textCached, textOutput, audioInput, audioCached, audioOutput] = expected;
close(result.list, (5000 * textInput + 15225 * textCached + 150 * textOutput +
360 * audioInput + 1620 * audioCached + 720 * audioOutput) / 1e6);
close(result.listPerMinute, result.list / 1.5);
assert.deepEqual(result.missing, []);
assert.ok(result.rows.every(r => !["speechInput", "speechOutput", "liveMinute"].includes(r.key)));
const noCache = calculate({ ...DEFAULTS, cachePercent: 0 }, "native", rates);
close(noCache.list, (20000 * textInput + 1980 * audioInput + 720 * audioOutput) / 1e6);
}
});

test("Azure models are selectable only for managed native audio, never substituted with GPT BYOM rates", () => {
for (const id of ["azure-realtime", "azure-realtime-fast"]) {
for (const hosting of ["managed", "byom"]) for (const pipeline of ["native", "hybrid", "cascade", "live"]) {
const supported = hosting === "managed" && pipeline === "native";
assert.equal(supportsScenario(model(id), hosting, pipeline), supported);
if (!supported) assert.throws(() => getRates(snapshot, model(id), hosting, pipeline), /no BYOM or alternative-pipeline rates/);
}
}
assert.equal(supportsScenario(model("gpt-realtime"), "byom", "hybrid"), true);
assert.equal(supportsScenario(model("gpt-4.1"), "managed", "cascade"), true);
assert.equal(supportsScenario(model("gpt-6-sol"), "byom", "live"), true);
});

test("BYOM models are prefilled from South India Global while speech remains Central India", () => {
assert.equal(snapshot.region, "southindia");
assert.equal(snapshot.voiceLiveRegion, "centralindia");
for (const m of MODELS) {
if (!supportsScenario(m, "byom", m.realtime ? "native" : "cascade")) continue;
const { rates, sources } = getRates(snapshot, m, "byom");
for (const key of ["textInput", "textCached", "textOutput", ...(m.realtime ? ["audioInput", "audioCached", "audioOutput"] : [])]) {
assert.notEqual(rates[key], null, `${m.id} ${key} should be prefilled`);
Expand Down Expand Up @@ -92,6 +139,7 @@ test("GPT-5.6 Luna BYOM uses exact Standard Global inference meters and BYO spee
test("all selectable models have priced scenarios across both hosting modes", () => {
for (const hosting of ["managed", "byom"]) for (const m of MODELS) {
const pipeline = m.realtime ? "native" : "cascade";
if (!supportsScenario(m, hosting, pipeline)) continue;
const result = calculate(DEFAULTS, pipeline, book(m.id, hosting));
assert.notEqual(result.list, null, `${hosting} / ${m.id}`);
assert.deepEqual(result.missing, []);
Expand Down Expand Up @@ -277,6 +325,7 @@ test("silence scales only audio rows, with zero preserving baseline and full sil
test("all five prompt sizes grow monotonically for every priced model", () => {
assert.deepEqual(PROMPTS, [0, 1000, 5000, 10000, 20000]);
for (const m of MODELS) for (const hosting of ["managed", "byom"]) {
if (!supportsScenario(m, hosting, m.realtime ? "native" : "cascade")) continue;
const costs = PROMPTS.map(prompt => calculate({ ...DEFAULTS, prompt }, m.realtime ? "native" : "cascade", book(m.id, hosting)).list);
if (costs.every(c => c !== null)) {
for (let i = 1; i < costs.length; i++) assert.ok(costs[i] >= costs[i - 1]);
Expand Down
Loading