From 99631d67b0f5f43c37fa1d933435ad8c5959fa73 Mon Sep 17 00:00:00 2001 From: "Sheng Zhao (SPEECH)" Date: Tue, 27 Oct 2026 14:36:37 +0800 Subject: [PATCH 1/2] feat: add Azure Realtime models to cost calculator Add managed native-audio estimates for Azure Realtime using Pro meters and Azure Realtime Fast using an explicit Lite-tier assumption. Include comparison and CSV pricing provenance, prevent unverified BYOM substitutions, and cover pricing and scenario selection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/cost_calculator/README.md | 10 ++++ tools/cost_calculator/app.mjs | 22 ++++++--- tools/cost_calculator/calculator.mjs | 11 +++++ tools/cost_calculator/calculator.test.mjs | 57 +++++++++++++++++++++-- 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/tools/cost_calculator/README.md b/tools/cost_calculator/README.md index d5882f5..e036c54 100644 --- a/tools/cost_calculator/README.md +++ b/tools/cost_calculator/README.md @@ -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. diff --git a/tools/cost_calculator/app.mjs b/tools/cost_calculator/app.mjs index fb44251..c3ed50f 100644 --- a/tools/cost_calculator/app.mjs +++ b/tools/cost_calculator/app.mjs @@ -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"); @@ -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; @@ -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) { @@ -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 ? @@ -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) { @@ -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) { @@ -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); @@ -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("; "), @@ -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); diff --git a/tools/cost_calculator/calculator.mjs b/tools/cost_calculator/calculator.mjs index 4b023c2..46b6f9e 100644 --- a/tools/cost_calculator/calculator.mjs +++ b/tools/cost_calculator/calculator.mjs @@ -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" }, @@ -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) { @@ -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 }; diff --git a/tools/cost_calculator/calculator.test.mjs b/tools/cost_calculator/calculator.test.mjs index 0abbedd..2b256f4 100644 --- a/tools/cost_calculator/calculator.test.mjs +++ b/tools/cost_calculator/calculator.test.mjs @@ -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", () => { @@ -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`); @@ -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, []); @@ -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]); From cd4ec46e73b8134bae1646c399b1816e90c2d782 Mon Sep 17 00:00:00 2001 From: "Sheng Zhao (SPEECH)" Date: Wed, 28 Oct 2026 16:27:00 +0800 Subject: [PATCH 2/2] fix: use realtime text and Azure HD for Realtime Fast Move Azure Realtime Fast from native speech-to-speech to the managed realtime text response plus Azure HD pipeline. Retain the Lite-tier reference, update pricing notes and scenario guards, and cover hybrid costs and pipeline eligibility. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/cost_calculator/README.md | 11 ++++-- tools/cost_calculator/app.mjs | 14 +++++-- tools/cost_calculator/calculator.mjs | 12 +++--- tools/cost_calculator/calculator.test.mjs | 46 ++++++++++++++++------- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/tools/cost_calculator/README.md b/tools/cost_calculator/README.md index e036c54..b12812f 100644 --- a/tools/cost_calculator/README.md +++ b/tools/cost_calculator/README.md @@ -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 diff --git a/tools/cost_calculator/app.mjs b/tools/cost_calculator/app.mjs index c3ed50f..98854f9 100644 --- a/tools/cost_calculator/app.mjs +++ b/tools/cost_calculator/app.mjs @@ -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."; } @@ -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.`; } } diff --git a/tools/cost_calculator/calculator.mjs b/tools/cost_calculator/calculator.mjs index 46b6f9e..77f08c2 100644 --- a/tools/cost_calculator/calculator.mjs +++ b/tools/cost_calculator/calculator.mjs @@ -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" }, @@ -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; } @@ -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, diff --git a/tools/cost_calculator/calculator.test.mjs b/tools/cost_calculator/calculator.test.mjs index 2b256f4..b7e709a 100644 --- a/tools/cost_calculator/calculator.test.mjs +++ b/tools/cost_calculator/calculator.test.mjs @@ -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}`); @@ -30,7 +30,7 @@ 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]], @@ -38,7 +38,8 @@ test("Azure Realtime uses Pro and Fast uses the explicit Lite assumption with na 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) => { @@ -46,22 +47,38 @@ test("Azure Realtime uses Pro and Fast uses the explicit Lite assumption with na 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/); } @@ -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}`); @@ -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]); }