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]); }