diff --git a/.github/workflows/cost-calculator-pages.yml b/.github/workflows/cost-calculator-pages.yml index cccac68..a0ed978 100644 --- a/.github/workflows/cost-calculator-pages.yml +++ b/.github/workflows/cost-calculator-pages.yml @@ -57,9 +57,16 @@ jobs: $ErrorActionPreference = "Stop" $site = Join-Path $env:RUNNER_TEMP "cost-calculator-site" New-Item -ItemType Directory -Path $site | Out-Null - foreach ($file in @("index.html", "styles.css", "app.mjs", "calculator.mjs", "prices.json")) { + foreach ($file in @("index.html", "styles.css", "app.mjs", "calculator.mjs", "prices.json", "favicon.svg")) { Copy-Item -LiteralPath (Join-Path (Join-Path "tools" "cost_calculator") $file) -Destination $site } + $index = Join-Path $site "index.html" + $html = Get-Content -LiteralPath $index -Raw + $marker = 'Local preview (not deployed)' + if (-not $html.Contains($marker)) { throw "Missing deployment commit marker in index.html." } + $commitUrl = "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/commit/$env:GITHUB_SHA" + $commitLink = 'Commit {2}' -f $commitUrl, $env:GITHUB_SHA, $env:GITHUB_SHA.Substring(0, 7) + $html.Replace($marker, $commitLink) | Set-Content -LiteralPath $index -Encoding utf8 -NoNewline - uses: actions/upload-pages-artifact@v3 with: path: ${{ runner.temp }}/cost-calculator-site diff --git a/tools/cost_calculator/README.md b/tools/cost_calculator/README.md index e3026db..894a273 100644 --- a/tools/cost_calculator/README.md +++ b/tools/cost_calculator/README.md @@ -47,10 +47,14 @@ Actions tab. Pull requests and other branches cannot deploy. The repository must have **Settings > Pages > Source > GitHub Actions** enabled. The deployment publishes exactly `index.html`, `styles.css`, -`app.mjs`, `calculator.mjs`, and `prices.json` at the site's root, excluding +`app.mjs`, `calculator.mjs`, `prices.json`, and the voice-wave `favicon.svg` +at the site's root, excluding the local server, tests, documentation, and all other repository files. No dependency installation or build command is needed. All asset URLs are relative, so the page works under a repository subpath. +During deployment, the workflow stamps the footer with the short commit hash +and a link to that exact source commit. Local previews show +**Local preview (not deployed)** instead. For this repository, the published address is **https://microsoft.github.io/voice-agent/** after the first successful @@ -61,7 +65,7 @@ is required. Set the app location to this tool's directory, leave the API location empty, and deploy the existing assets without a build. Configure authentication and route access rules if the page must not be public. -Deploy only the five browser assets above; no server, credentials, or live +Deploy only the six browser assets above; no server, credentials, or live Azure connection is needed. Pricing updates require updating the snapshot and redeploying. diff --git a/tools/cost_calculator/favicon.svg b/tools/cost_calculator/favicon.svg new file mode 100644 index 0000000..d7a5edb --- /dev/null +++ b/tools/cost_calculator/favicon.svg @@ -0,0 +1,5 @@ + + Voice Agent Cost Lab + + + diff --git a/tools/cost_calculator/index.html b/tools/cost_calculator/index.html index d3062bd..63a00d0 100644 --- a/tools/cost_calculator/index.html +++ b/tools/cost_calculator/index.html @@ -4,6 +4,7 @@ Voice Agent Cost Lab + @@ -93,13 +94,13 @@

03 Call volume

Read the estimate, not an invoice

Each call starts with an uncached prompt. On later turns the prompt and prior conversation are reused at your cache-hit percentage. With equally sized turns, prior caller/agent tokens are each replayed (turns - 1) / 2 times. Cache misses use full input rates.

Native mode reuses audio history; text-response mode reuses caller audio and agent text; cascade and reasoning backends reuse text. Only newly generated speech is synthesized. Reasoning tokens are charged as text output, not retained as visible conversation history.

-

100% caching is an idealized assumption, not guaranteed prefix-cache eligibility. The supplied 20 input audio tokens/sec is preserved even though the public guide typically estimates ~10 for OpenAI input audio. Actual tokens, silence billing, tools, storage, telephony, hosting capacity, tax, and network charges can change the result and are excluded.

+

100% caching is an idealized assumption, not guaranteed prefix-cache eligibility. Audio defaults use the public guide's estimates of 10 input and 20 output audio tokens/sec for Azure OpenAI models. Actual tokens, silence billing, tools, storage, telephony, hosting capacity, tax, and network charges can change the result and are excluded.

Model selection is for economic modeling, not a guarantee of South India deployment availability or supported BYOM audio connections. South India Global is the reference for BYOM models and GPT Live. No South India Voice Live managed-tier or BYO speech meters were found, so those use approved Central India references. Azure HD speech output also deliberately uses Central India rates. GPT-6 is explicitly GPT-6 Sol, short-context Standard pricing. GPT Live voice uses the South India Global rate ($3/hour = $0.05/minute); backend model usage is additional and no Azure HD charge is added. GPT Live's delegation frequency and backend context differ in practice; this estimate uses the same turn-based backend assumptions and a session-minute rate.

If a model has no verified managed tier, its economic reference combines AOAI Global model rates with BYO speech rates and is labeled accordingly. It is not a managed Voice Live price or support claim.

Azure Speech pricing / Azure OpenAI pricing / Model tiers & metering / Voice Agent billing

- + diff --git a/tools/cost_calculator/server.mjs b/tools/cost_calculator/server.mjs index 6183318..7f0258f 100644 --- a/tools/cost_calculator/server.mjs +++ b/tools/cost_calculator/server.mjs @@ -2,8 +2,8 @@ import { createServer } from "node:http"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; -const allowed = new Set(["index.html", "styles.css", "app.mjs", "calculator.mjs", "prices.json"]); -const mime = { html: "text/html", css: "text/css", mjs: "text/javascript", json: "application/json" }; +const allowed = new Set(["index.html", "styles.css", "app.mjs", "calculator.mjs", "prices.json", "favicon.svg"]); +const mime = { html: "text/html", css: "text/css", mjs: "text/javascript", json: "application/json", svg: "image/svg+xml" }; const port = Number(process.env.PORT || 8095); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PORT must be an integer from 1 to 65535."); const server = createServer(async (req, res) => {