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
11 changes: 7 additions & 4 deletions tools/cost_calculator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ The GPT Live meter is hourly and is divided by 60 for USD-per-minute pricing.
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
not a verified published Fast tariff. Azure Realtime uses managed native
speech-to-speech with no separate STT or Azure HD charge. Azure Realtime Fast
uses **Realtime text response + Azure HD**: Lite-tier native audio input,
text input/output, and standard-speech output meters, without native audio
output charges. Both use the existing Central India tier meters and appear
in their respective managed pipeline selection, comparison, and CSV export.
Neither adds a GPT Live session charge; 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
Expand Down
14 changes: 11 additions & 3 deletions tools/cost_calculator/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ 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.managedPipeline) {
$("price-note").textContent += model.managedPipeline === "hybrid" ?
" Azure Realtime Fast uses native audio input, text response, and Azure HD speech output. No native audio output, separate Azure STT, or GPT Live session charge is added." :
" Azure Realtime uses native audio only in this calculator; no separate Azure STT, HD voice, or GPT Live session charge is added.";
$("price-note").textContent += " 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.";
}

Expand All @@ -134,8 +139,11 @@ 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.`;
if (model.managedPipeline) {
const audioNote = model.managedPipeline === "hybrid" ?
"Native audio input + text response + Central India Azure HD output; no native audio output charge." :
"Native audio only; no separate STT/HD charge.";
$("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."} ${audioNote} Confirm regional availability and actual audio token rates.`;
}
}

Expand Down
12 changes: 6 additions & 6 deletions tools/cost_calculator/calculator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +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", name: "Azure Realtime", tier: "Pro", realtime: true, managedPipeline: "native" },
{ id: "azure-realtime-fast", name: "Azure Realtime Fast", tier: "Lite", realtime: true,
managedNativeOnly: true, tierAssumption: true },
managedPipeline: "hybrid", 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 @@ -31,7 +31,7 @@ export const MODELS = [
];

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

Expand Down Expand Up @@ -63,9 +63,9 @@ export function meterRate(snapshot, name, unit = "tokens") {
return meter.retailPrice * factor;
}

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.`);
export function getRates(snapshot, model, hosting, pipeline = model.managedPipeline || "cascade") {
if (model.managedPipeline && !supportsScenario(model, hosting, pipeline)) {
throw new Error(`${model.name} uses a managed ${model.managedPipeline} 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,
Expand Down
46 changes: 32 additions & 14 deletions tools/cost_calculator/calculator.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DEFAULTS, MODELS, PROMPTS, GPT_LIVE_METER, supportsScenario, getRates,

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 = model(id).realtime ? "native" : "cascade") =>
const book = (id, hosting = "managed", pipeline = model(id).managedPipeline || (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}`);

Expand All @@ -30,38 +30,55 @@ test("every supported model/hosting/pipeline combination resolves without a miss
}
});

test("Azure Realtime uses Pro and Fast uses the explicit Lite assumption with native meters", () => {
test("Azure Realtime uses Pro native audio and Fast uses Lite realtime text plus HD", () => {
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");
const pipeline = m.managedPipeline;
const { rates, sources, usesGlobalReference } = getRates(snapshot, m, "managed", pipeline);
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 result = calculate({ ...DEFAULTS, nativeText: 150 }, pipeline, 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);
if (pipeline === "hybrid") {
close(rates.speechOutput, 25);
assert.equal(sources.speechOutput.meterName, "Voice Live API Lite - Standard Speech Audio Output Tokens");
assert.equal(sources.speechOutput.armRegionName, "centralindia");
close(result.list, (5000 * textInput + 15225 * textCached + 150 * textOutput +
360 * audioInput + 540 * audioCached + 720 * 25) / 1e6);
assert.ok(result.rows.some(r => r.key === "speechOutput"));
assert.ok(result.rows.every(r => !["audioOutput", "speechInput", "liveMinute"].includes(r.key)));
const noCache = calculate({ ...DEFAULTS, cachePercent: 0 }, pipeline, rates);
close(noCache.list, (20225 * textInput + 900 * audioInput + 150 * textOutput + 720 * 25) / 1e6);
const reasoning = calculate({ ...DEFAULTS, reasoning: 100 }, pipeline, rates);
close(reasoning.list - result.list, 100 * textOutput / 1e6);
const silent = calculate({ ...DEFAULTS, silencePercent: 100 }, pipeline, rates);
close(silent.list, (5000 * textInput + 15225 * textCached + 150 * textOutput) / 1e6);
} else {
close(result.list, (5000 * textInput + 15225 * textCached + 150 * textOutput +
360 * audioInput + 1620 * audioCached + 720 * audioOutput) / 1e6);
assert.ok(result.rows.every(r => !["speechInput", "speechOutput", "liveMinute"].includes(r.key)));
const noCache = calculate({ ...DEFAULTS, cachePercent: 0 }, pipeline, rates);
close(noCache.list, (20000 * textInput + 1980 * audioInput + 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", () => {
test("Azure models use their correct managed pipeline, 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";
const supported = hosting === "managed" && pipeline === (id === "azure-realtime-fast" ? "hybrid" : "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/);
}
Expand Down Expand Up @@ -138,7 +155,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";
const pipeline = m.managedPipeline || (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}`);
Expand Down Expand Up @@ -325,8 +342,9 @@ 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);
const pipeline = m.managedPipeline || (m.realtime ? "native" : "cascade");
if (!supportsScenario(m, hosting, pipeline)) continue;
const costs = PROMPTS.map(prompt => calculate({ ...DEFAULTS, prompt }, pipeline, 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